Think Python/类和对象
用户定义类型
[edit | edit source]我们已经使用了 Python 的许多内置类型;现在我们将定义一个新的类型。举个例子,我们将创建一个名为Point的类型,它代表二维空间中的一个点。
在数学符号中,点通常用括号括起来,用逗号分隔坐标。例如,(0, 0) 代表原点,(x, y) 代表从原点向右移动 x 个单位,向上移动 y 个单位的点。
在 Python 中,我们可以用几种方式表示点
- 我们可以将坐标分别存储在两个变量中:x和y.
- 我们可以将坐标存储为列表或元组中的元素。
- 我们可以创建一个新类型来表示点作为对象。
创建新类型比其他选项(稍微)更复杂,但它有一些很快就会显现的优势。
用户定义类型也称为类。类定义看起来像这样
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.pi或string.whitespace。但是,在这种情况下,我们正在将值分配给对象的命名元素。这些元素称为属性。
作为名词,“AT-trib-ute”的重音在第一个音节上,而“a-TRIB-ute”是动词。
下图显示了这些赋值的结果。显示对象及其属性的状态图称为对象图
变量空白引用一个 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.
"""
文档字符串列出了属性width和height是数字;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]width和height:
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
以下是对象图的样子:
此操作称为浅拷贝,因为它复制了对象及其包含的任何引用,但不复制嵌入的对象。
对于大多数应用程序来说,这不是你想要的。在这个例子中,对其中一个矩形调用grow_rectangle
不会影响另一个矩形,但是对任何一个矩形调用move_rectangle
都会影响两个矩形!这种行为令人困惑且容易出错。
幸运的是,别名会使程序难以阅读,因为在一个地方的更改可能会在另一个地方产生意想不到的影响。很难跟踪所有可能引用给定对象的变量。模块包含一个名为deepcopy的方法,它不仅复制了对象,还复制了它引用的对象,以及它们引用的对象,等等。你不会惊讶地发现这个操作被称为深拷贝。
>>> box3 = copy.deepcopy(box)
>>> box3 is box
False
>>> box3.corner is box.corner
False
box3和box是完全独立的对象。
编写一个move_rectangle
版本,它创建并返回一个新的矩形,而不是修改旧的矩形。
当你开始使用对象时,你可能会遇到一些新的异常。如果你尝试访问不存在的属性,你会得到一个AttributeError:
>>> p = Point()
>>> print p.z
AttributeError: Point instance has no attribute 'z'
如果你不确定对象的类型,你可以询问
>>> type(p)
<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.