跳转到内容

Think Python/类和对象

来自 Wikibooks,开放世界中的开放书籍

用户定义类型

[edit | edit source]

我们已经使用了 Python 的许多内置类型;现在我们将定义一个新的类型。举个例子,我们将创建一个名为Point的类型,它代表二维空间中的一个点。

在数学符号中,点通常用括号括起来,用逗号分隔坐标。例如,(0, 0) 代表原点,(x, y) 代表从原点向右移动 x 个单位,向上移动 y 个单位的点。

在 Python 中,我们可以用几种方式表示点

  • 我们可以将坐标分别存储在两个变量中:xy.
  • 我们可以将坐标存储为列表或元组中的元素。
  • 我们可以创建一个新类型来表示点作为对象。

创建新类型比其他选项(稍微)更复杂,但它有一些很快就会显现的优势。

用户定义类型也称为。类定义看起来像这样

class Point(object):
    """represents a point in 2-D space"""

此标题表示新类是一种Point,它是object的一种,而它是内置类型。

主体是一个文档字符串,它解释了类的用途。你可以在类定义中定义变量和函数,但我们稍后会回到这一点。

定义名为Point的类会创建一个类对象。

>>> print Point
<class '__main__.Point'>

因为Point是在顶层定义的,所以它的“全名”是 __main__.Point

类对象就像一个用于创建对象的工厂。要创建一个 Point,你需要调用Point,就像它是一个函数一样。

>>> blank = Point()
>>> print blank
<__main__.Point instance at 0xb7e9d3ac>

返回值是对 Point 对象的引用,我们将它分配给空白。创建新对象称为实例化,而该对象是类的实例

当你打印一个实例时,Python 会告诉你它属于哪个类,以及它存储在内存中的位置(前缀0x表示后面的数字是十六进制的)。

属性

[edit | edit source]

你可以使用点表示法为实例分配值

>>> blank.x = 3.0
>>> blank.y = 4.0

此语法类似于从模块中选择变量的语法,例如math.pistring.whitespace。但是,在这种情况下,我们正在将值分配给对象的命名元素。这些元素称为属性

作为名词,“AT-trib-ute”的重音在第一个音节上,而“a-TRIB-ute”是动词。

下图显示了这些赋值的结果。显示对象及其属性的状态图称为对象图

File:Book022.png

变量空白引用一个 Point 对象,该对象包含两个属性。每个属性都引用一个浮点数。

你可以使用相同的语法读取属性的值

>>> print blank.y
4.0
>>> x = blank.x
>>> print x
3.0

表达式blank.x的意思是,“转到对象空白所引用的位置,并获取x的值”。在这种情况下,我们将该值分配给一个名为x的变量。变量x和属性x.

之间不存在冲突。你可以在任何表达式中使用点表示法。例如

>>> print '(%g, %g)' % (blank.x, blank.y)
(3.0, 4.0)
>>> distance = math.sqrt(blank.x**2 + blank.y**2)
>>> print distance
5.0

你可以按常规方式将实例作为参数传递。例如

def print_point(p):
    print '(%g, %g)' % (p.x, p.y)

print_point 接受一个点作为参数,并以数学符号显示它。要调用它,你可以传递空白作为参数

>>> print_point(blank)
(3.0, 4.0)

在函数内部,p空白的别名,因此如果函数修改了p, 空白,则会发生改变。

练习 1

[edit | edit source]

编写一个名为“distance”的函数,它接受两个 Point 作为参数,并返回它们之间的距离。

矩形

[edit | edit source]

有时,对象的属性是显而易见的,但有时你需要做出决定。例如,想象你正在设计一个类来表示矩形。你会使用哪些属性来指定矩形的位置和大小?你可以忽略角度;为了简化起见,假设矩形是垂直或水平的。

至少有两种可能性

  • 你可以指定矩形的一个角(或中心)、宽度和高度。
  • 你可以指定两个相对的角。

在这一点上,很难说哪一个比另一个更好,所以我们将实现第一个,仅仅作为示例。

这是类定义

class Rectangle(object):
    """represent a rectangle. 
       attributes: width, height, corner.
    """

文档字符串列出了属性widthheight是数字;corner是一个 Point 对象,它指定了左下角。

要表示一个矩形,你需要实例化一个 Rectangle 对象,并为属性分配值

box = Rectangle()
box.width = 100.0
box.height = 200.0
box.corner = Point()
box.corner.x = 0.0
box.corner.y = 0.0

表达式box.corner.x的意思是,“转到对象box所引用的位置,并选择名为corner的属性;然后转到该对象,并选择名为x的属性。”

该图显示了该对象的状态

File:Book023.png 作为另一个对象属性的对象是嵌入的。

实例作为返回值

[edit | edit source]

函数可以返回实例。例如,find_center 接受一个Rectangle作为参数,并返回一个Point,其中包含Rectangle:

def find_center(box):
    p = Point()
    p.x = box.corner.x + box.width/2.0
    p.y = box.corner.y + box.height/2.0
    return p

中心的坐标。以下是一个将box作为参数传递并将生成的 Point 分配给center:

>>> center = find_center(box)
>>> print_point(center)
(50.0, 100.0)

对象的例子。

对象是可变的

[edit | edit source]widthheight:

box.width = box.width + 50
box.height = box.height + 100

你可以通过对对象的某个属性进行赋值来改变对象的状态。例如,要改变矩形的大小而不改变其位置,你可以修改的值。你也可以编写修改对象的函数。例如,grow_rectangle 接受一个 Rectangle 对象和两个数字,dwidthdheight

def grow_rectangle(rect, dwidth, dheight) :
    rect.width += dwidth
    rect.height += dheight

,并将这些数字添加到矩形的宽度和高度

>>> print box.width
100.0
>>> print box.height
200.0
>>> grow_rectangle(box, 50, 100)
>>> print box.width
150.0
>>> print box.height
300.0

在函数内部,以下是一个演示效果的例子box的别名,因此如果函数修改了以下是一个演示效果的例子, box,则会发生改变。

rect

练习 2

[edit | edit source]编写一个名为 move_rectangle 的函数,它接受一个 Rectangle 和两个名为dxdy编写一个名为 move_rectangle 的函数,它接受一个 Rectangle 和两个名为的数字。它应该通过将x添加到corner的坐标和将dx的数字。它应该通过将y添加到corner.

添加到

的坐标来改变矩形的位置。

复制

[edit | edit source]别名会使程序难以阅读,因为在一个地方的更改可能会在另一个地方产生意想不到的影响。很难跟踪所有可能引用给定对象的变量。复制对象通常是别名的替代方案。该别名会使程序难以阅读,因为在一个地方的更改可能会在另一个地方产生意想不到的影响。很难跟踪所有可能引用给定对象的变量。copy

>>> p1 = Point()
>>> p1.x = 3.0
>>> p1.y = 4.0

>>> import copy
>>> p2 = copy.copy(p1)

模块包含一个名为的函数,它可以复制任何对象p1

>>> print_point(p1)
(3.0, 4.0)
>>> print_point(p2)
(3.0, 4.0)
>>> p1 is p2
False
>>> p1 == p2
False

p2包含相同的数据,但它们不是同一个 Point。模块包含一个名为的函数,它可以复制任何对象运算符表示==不是同一个对象,这正如我们所料。但你可能期望产生True==,因为这些点包含相同的数据。在这种情况下,你会很失望地发现,对于实例,该包含相同的数据,但它们不是同一个 Point。运算符的默认行为与该

运算符相同;它检查对象标识,而不是对象等效性。此行为可以更改——我们稍后会看到如何更改。如果你使用要复制一个矩形,你会发现它复制了矩形对象,但没有复制嵌入的点对象。

>>> box2 = copy.copy(box)
>>> box2 is box
False
>>> box2.corner is box.corner
True

以下是对象图的样子:

<IMG SRC="book024.png">

此操作称为浅拷贝,因为它复制了对象及其包含的任何引用,但不复制嵌入的对象。

对于大多数应用程序来说,这不是你想要的。在这个例子中,对其中一个矩形调用grow_rectangle不会影响另一个矩形,但是对任何一个矩形调用move_rectangle都会影响两个矩形!这种行为令人困惑且容易出错。

幸运的是,别名会使程序难以阅读,因为在一个地方的更改可能会在另一个地方产生意想不到的影响。很难跟踪所有可能引用给定对象的变量。模块包含一个名为deepcopy的方法,它不仅复制了对象,还复制了它引用的对象,以及它们引用的对象,等等。你不会惊讶地发现这个操作被称为深拷贝

>>> box3 = copy.deepcopy(box)
>>> box3 is box
False
>>> box3.corner is box.corner
False

box3box是完全独立的对象。

编写一个move_rectangle版本,它创建并返回一个新的矩形,而不是修改旧的矩形。

当你开始使用对象时,你可能会遇到一些新的异常。如果你尝试访问不存在的属性,你会得到一个AttributeError:

>>> p = Point()
>>> print p.z
AttributeError: Point instance has no attribute 'z'

如果你不确定对象的类型,你可以询问

>>> type(p)
&lt;type '__main__.Point'>

如果你不确定对象是否具有特定属性,可以使用内置函数hasattr:

>>> hasattr(p, 'x')
True
>>> hasattr(p, 'z')
False

第一个参数可以是任何对象;第二个参数是一个字符串,包含属性的名称。

词汇表

[编辑 | 编辑源代码]
class
用户定义的类型。类定义创建新的类对象。
类对象
包含用户定义类型信息的对象。类对象可以用来创建该类型的实例。
实例
属于某个类的对象。
属性
与对象关联的命名值之一。
嵌入(对象)
作为另一个对象属性存储的对象。
浅拷贝
复制对象的内容,包括对嵌入对象的任何引用;由别名会使程序难以阅读,因为在一个地方的更改可能会在另一个地方产生意想不到的影响。很难跟踪所有可能引用给定对象的变量。函数在别名会使程序难以阅读,因为在一个地方的更改可能会在另一个地方产生意想不到的影响。很难跟踪所有可能引用给定对象的变量。模块中实现。
深拷贝
复制对象的内容以及任何嵌入的对象,以及它们嵌入的任何对象,等等;由deepcopy函数在别名会使程序难以阅读,因为在一个地方的更改可能会在另一个地方产生意想不到的影响。很难跟踪所有可能引用给定对象的变量。模块中实现。
对象图
一个图,它显示了对象、它们的属性和属性的值。

World.py,它是 Swampy 的一部分(见第 '4' 章),包含一个用于称为 World 的用户定义类型的类定义。如果你运行这段代码: from World import * world = World() wait_for_user() 应该会弹出一个带标题栏和一个空正方形的窗口。在本练习中,我们将使用此窗口绘制点、矩形和其他形状。在wait_for_user之前添加以下几行,然后再次运行程序

''canvas = world.ca(width=500, height=500, background='white')
bbox = [[-150,-100], [150, 100]]
canvas.rectangle(bbox, outline='black', width=2, fill='green4')

你应该看到一个带有黑色边框的绿色矩形。第一行创建了一个 Canvas,它在窗口中显示为一个白色正方形。Canvas 对象提供了像rectangle这样的方法,用于绘制各种形状。

bbox是一个列表的列表,它表示矩形的“边界框”。第一对坐标是矩形的左下角;第二对坐标是右上角。

你可以这样绘制一个圆形

canvas.circle([-25,0], 70, outline=None, fill='red')

第一个参数是圆心坐标对;第二个参数是半径。

如果你在程序中添加这行代码,结果应该类似于孟加拉国国旗(见wikipedia.org/wiki/Gallery_of_sovereign-state_flags).

  • 编写一个名为draw_rectangle的函数,它接收一个 Canvas 和一个矩形作为参数,并在 Canvas 上绘制矩形的表示。
  • 在你的矩形对象中添加一个名为color的属性,并修改draw_rectangle,以便它使用 color 属性作为填充颜色。
  • 编写一个名为draw_point的函数,它接收一个 Canvas 和一个点作为参数,并在 Canvas 上绘制点的表示。
  • 定义一个名为 Circle 的新类,它具有适当的属性,并实例化几个 Circle 对象。编写一个名为draw_circle的函数,它在画布上绘制圆形。
points = [[-150,-100], [150, 100], [150, -100]]
canvas.polygon(points, fill='blue')

我写了一个小程序,它列出了可用的颜色;你可以从thinkpython.com/code/color_list.py.

华夏公益教科书