15. 类和对象
到现在你已经知道如何使用函数组织代码 , 以及如何使用内置类型来组织数据 .
下一步将学习 '面向对象编程' , 面向对象编程使用自定义的类型同时组织代码和数据 .
面向对象编程是一个很大的话题 , 需要好几章来讨论 .
本章的代码示例可以从↓下载 ,
https : / / github . com / AllenDowney / ThinkPython2 / blob / master / code / Point1 . py
练习的解答可以在↓下载 .
https : / / github . com / AllenDowney / ThinkPython2 / blob / master / code / Point1_soln . py
15.1 用户定义类型
我们已经使用了很多Python的内置类型 ; 现在我们要定义一个新类型 .
作为示例 , 我们将会新建一个类型Point , 用来表示二维空间中的一个点 .
在数学的表示法中 , 点通常使用括号中逗号分割两个坐标表示 .
例如 , ( 0 , 0 ) 表示原点 , 而 ( x , y ) 表示一个在圆点右侧x单位 , 上方y单位的点 .
在Python中 , 有好几种方法可以表达点 .
* 我们可以将两个坐标分别保存到变量x和y中 .
* 我们可以将坐标作为列表或元组的元素存储 .
* 我们可以新建一个类型用对象表达点 .
新建一个类型比其他方法更复杂一些 , 但它的优点很快就显现出来 .
用户定义的类型也称为 '类' ( class ) . 类的定义如下 :
class Point :
""" Represents a point in 2-D space. """
定义头表示新的类名为Point . 定义体是一个文档字符串 , 解释这个类的用途 .
可以在类定义中定义变量和函数 , 我们会后面回到这个话题 .
定义一个叫作Point的类会创建一个 '对象类' ( object class ) .
>> > Point
< class '__main__.Point' >
因为Point是在程序顶层定义的 , 它的 '全名' 是__main__ . Point .
类对象项一个创建对象的工厂 . 要新建一个Point对象 , 可以把Point当作函数类调研 :
>> > blank = Point( )
>> > blank
< __main__. Point object at 0x00000230EBFBB490 >
返回值是一个Point对象的引用 , 它们将它赋值给变量blank .
新建一个对象的过程称为 '实例化' ( instantiation ) , 而对象是这个类的一个实例 .
在打印一个实例时 , Python会告诉你它所属的类型 ,
以及存在内存中的位置 ( 前缀 0 x表示后面的数字是十六进制的 ) .
每个对象都是某个类的实例 , 所以 '对象' 和 '实例' 这个两个词很多情况下都可以互换 ,
但是在本章中我们使用 '实例' 来表示一个自定义类型的对象 .
15.2 属性
可以使用句点表示法给实例赋值 :
>> > blank. x = 3.0
>> > blank. y = 4.0
这个语法和从模块中选择变量的语法类似 , 如math . pi或者strings . whitespace .
但在种情况下 , 我们是将值赋值给一个对象的有命名的元元素 . 这些元素称为属性 ( attribute ) .
作为名词时 , 'AT-trib-ute' 发音的重音在第一个音节 , 这与作为动词的 'a-TRIB-ute' 不同 .
下面的图标展示了这些赋值的结果 .
展示一个对象和其属性的状态图称为 '对象图' ( object diagram ) , 参见图 15 - 1.
变量blank引用一个Point对象 , 它包含了两个属性 . 每个属性引用一个浮点数 .
可以使用相同的语法来读取一个属性的值 .
>> > blank. y
4.0
>> > x = blank. x
>> > x
3.0
表达式blank . x表示 , '找打blank引用的对象, 并取得它的x属性的值' .
在这个例子中 , 我们将那个值 赋值给一个变量x . 变量x和属性x并不冲突 .
可以在任意表达式中使用句点表示法 . 例如 :
>> > '(%g, %g)' % ( blank. x, blank. y)
'(3, 4)'
>> > import math
>> > distance = math. sqrt( blank. x ** 2 + blank. y ** 2 )
>> > distance
5.0
可以将一个实例作为实参按通常的方式传递 . 例如 :
def print_point ( p) :
print ( '(%g, %g)' % ( p. x, p. y) )
print_point接收一个点作为形参 , 并按照属性表达式展示它 .
可以传入blank作为实参来调用它 :
>> > print_point( blank)
( 3 , 4 )
在函数中 , p是blank的一个别名 , 所以如果函数修改了p , 则blank也会被修改 .
作为练习 , 编写一个叫作distance_between_points的函数 ,
接收两个Point对象作为形参 , 并返回它们之间的距离 .
第一个坐标 ( x1 = 3 , y1 = 4 )
第二个坐标 ( x2 = 5 , y2 = 6 )
计算公式 : ( ✓根号 )
| AB | = ✓ ( x1 - x2 ) * * 2 + ( y1 - y2 ) * * 2
或 :
| AB | = ( x1 - x2 ) * * 2 + ( y1 - y2 ) * * 2 * * 0.5
import math
class Point :
"""自定义对象"""
x = None
y = None
blank1 = Point( )
blank1. x = 3.0
blank1. y = 4.0
blank2 = Point( )
blank2. x = 5.0
blank2. y = 6.0
def distance_between_points ( p1, p2) :
"""
计算两个Point对象之间的距离,
:param p1: 第一个Point对象.
:param p2: 第二个Point对象.
:return: 两个Point对象之间的距离.
"""
px = p1. x - p2. x
py = p1. y - p2. y
return ( ( p1. x - p2. x) ** 2 + ( p1. y - p2. y) ** 2 ) ** 0.5
res = distance_between_points( blank1, blank2)
print ( res)
15.3 矩形
有时候对象因该有哪些属性非常明显 , 但也有时候需要你来做决定 ,
例如 , 假设你子啊设计一个表达矩形的类 . 你会用什么属性来指定一个矩形的位置和尺寸呢?
可以忽视角度 , 为了简单起见 , 假定矩形不是垂直的就是水平的 .
最少有以下两种可能 .
* 可以指定一个矩形的一个角落 ( 或者中心点 ) , 宽度以及高度 .
* 可以指定两个相对的角落 .
现在还很难说哪一种方案更好 , 所以作为示例 , 我们仅限实现第一个 .
class Rectangle :
"""Represents a rectangle.
attributes: width, height, corner.
"""
文档字符列出了属性 : width和height是数字 , 用来指定左下角的顶点 .
要表达一个矩形 , 需要实例化一个Rectangle对象 , 并对其属性赋值 :
box = Rectangle( )
box. width = 100.0
box. height = 200.0
box. corner = Ponint( )
box. corner. x = 0.0
box. corner. y = 0.0
表达式box . corner . x表示 , '去往box引用的对象, 并选择属性corner; 接着去往过那个对象, 并选择属性x' .
图 15 - 2 展示了这个对象的状态 . 作为另一个对象的属性存在的对象是 '内嵌' 的 .
15.4 作为返回值得示例
函数可以返回实例 .
例如 , find_center接收一个Rectangle对象作为参数 , 并返回一个Point对象 ,
包含这个Rectangle的中心点的坐标 :
def find_center ( rect) :
p = Point( )
p. x = rect. corner. x + rect. width / 2
p. y = rect. corner. y + rect. height / 2
return p
下面是一个示例 , 传入box作为实参 , 并将结果的point对象赋值给center :
>> > center = find_center( box)
>> > print_point( center)
( 50 , 100 )
def print_point ( p) :
print ( '(%g, %g)' % ( p. x, p. y) )
def find_center ( rect) :
p = Point( )
p. x = rect. corner. x + rect. width / 2
p. y = rect. corner. y + rect. height / 2
return p
class Point :
""" Represents a point in 2-D space. """
class Rectangle :
"""Represents a rectangle.
attributes: width, height, corner.
"""
if __name__ == '__main__' :
box = Rectangle( )
box. width = 100.0
box. height = 200.0
box. corner = Point( )
box. corner. x = 0.0
box. corner. y = 0.0
center = find_center( box)
print_point( center)
15.5 对象是可变的
可以通过一个对象的某个属性赋值来修改它的状态 .
例如 , 要修改一个矩形的尺寸而保持它的位置不变 ( 左下角坐标为 0 , 0 不变 . ) ,
可以修改属性width和height的值 :
box. width = box. width + 50
box. height = box. width. height + 100
也可以编写函数来修改对象 .
例如 , grow_rectangle接收一个Rectangle对象和两个数 , dwidth , dheight ,
并把这些数加到矩形的宽度和高度上 :
def grow_revtangle ( rect, dwidth, dheight) :
rect. width += dwight
rect. height += dheight
下面是展示这个函数效果的实例 :
>> > box. width, box. height
( 150.0 , 300.0 )
>> > grow_rectangle( box, 50 , 100 )
>> > box. width, box. height
( 200.0 , 400.0 )
在函数中 , rect是box的别名 , 所以如果当修改了revt时 , box也改变 .
作为练习 , 编写一个名为move_rectangle的函数 , 接收一个Rectangle对象和两个分别名为dx和dy的数值 .
它应当通过将dx添加到corner的x坐标和将dy添加到corner的y坐标来改变矩形的位置 .
def print_point ( p) :
print ( '(%g, %g)' % ( p. x, p. y) )
def find_center ( rect) :
p = Point( )
p. x = rect. corner. x + rect. width / 2
p. y = rect. corner. y + rect. height / 2
return p
def move_rectangle ( rect, dx, dy) :
rect. corner. x += dx
rect. corner. y += dy
class Point :
""" Represents a point in 2-D space. """
class Rectangle :
"""Represents a rectangle.
attributes: width, height, corner.
"""
if __name__ == '__main__' :
box = Rectangle( )
box. width = 100.0
box. height = 200.0
box. corner = Point( )
box. corner. x = 0.0
box. corner. y = 0.0
print_point( box. corner)
move_rectangle( box, 100 , 100 )
print_point( box. corner)
15.6 复制
别名的使用有时候会让程序更难阅读 , 因为一个地方的修改可能会给其他地方带来意想不到的变化 .
要跟踪掌握所有引用到一个给定对象的变量非常困难 .
使用别名的常用替代方案是复制对象 . copy模块里有一个函数copy可以复制任何对象 :
>> > p1 = Point( )
>> > p1. x = 3.0
>> > p1. y = 4.0
>> > import copy
>> > p2 = copy. copy( p1)
p1和p2包含相同的数据 , 但是它们不是同一个Point对象 .
>> > print_point( p1)
( 3 , 4 )
>> > print_point( p2)
( 3 , 4 )
>> > p1 is p2
False
>> > p1 == p2
False
正如我们预料 , is操作符告诉我们p1和p2不是同一个对象 .
但你可能会预料 = = 能得到True值 , 因为这两个点 ( 坐标点 ) 包含相同的数据 .
如果那样 , 你会失望地发现对于实例来说 , = = 操作符的默认行为和is操作符相同 ,
它会检查对象同一性 , 而不是对象相等性 .
这是因为对于用户自定义类型 , Python并不知道怎么才算相等 . 至少现在还不行 .
对象同一性 ( object identity ) : 当两个引用类型的变量存储的地址相同时 , 它们引用的是同一个对象 .
在Python中 , = = 操作符的默认行为是检查两个对象的值是否相等 .
而is操作符则检查两个对象是否是同一个对象 , 即它们是否具有相同的内存地址。
对于内置类型 ( 例如整数、浮点数、字符串等 ) Python已经定义了如何判断相等性 .
但对于自定义类型 , Python不会自动判断相等性 , 因为它不知道如何判断两个对象是否相等 .
因此 , 如果你定义了自己的类 , 你需要自己定义__eq__ ( ) 方法来定义该类的相等性行为 .
在这种情况下 , = = 操作符将使用您定义的__eq__ ( ) 方法进行比较 .
需要注意的是 , 即使您定义了__eq__ ( ) 方法 , 使用is操作符也不会调用该方法 ,
因为is操作符只检查两个对象是否具有相同的内存地址 .
如果使用copy . copy复制一个Rectangle , 你会发现它复制了Rectangle对象但并不复制内嵌的Point对象 :
>> > box2 = copy. copy( box)
>> > box2 is box
False
>> > box2. corner is box. corner
True
图 15 - 3 展示了这个操作的对象图 .
这个操作成为浅复制 ( shallow copy ) , 因为它复制对象及其包含的任何引用 , 但不复制内嵌对象 .
( 复制了内嵌对象的引用 , 两个Recrangle对象的corner属性共用一个内嵌对象的引用 ,
哪个Recrangle对象对内嵌对象做了改动都会影响另一个Recrangle对象 . )
对大多数应用 , 这并不是你所想要的 .
在这个例子里 , 对一个Recrangle对象调用grow_rectangle并不影响其他对象 ,
当对任何Recrangle对象调用move_rectangle都会影响全部两个对象 !
这种行为即混乱不清 , 又容易导致错误 .
幸好 , copy模块还提供了一个名为deepcopy的方法 , 它不但赋值对象 , 还会复制对象中引用的对象 ,
甚至它们引用的对象 , 以此类推 .
所以你并不会惊讶这个操作为何称为深复制 ( deep copy ) .
>> > box3 = copy. deepcopy( box)
>> > box3 is box
False
>> > box3. corner is box. corner
False
box3个box是两个完全分开的对象 .
作为练习 , 编写move_rectangle的另一个版本 , 它会新建并返回一个Rectangle对象 , 而不是直接修改旧对象 .
import copy
def print_point ( p) :
print ( '(%g, %g)' % ( p. x, p. y) )
def find_center ( rect) :
p = Point( )
p. x = rect. corner. x + rect. width / 2
p. y = rect. corner. y + rect. height / 2
return p
def move_rectangle ( rect, dx, dy) :
box2 = copy. deepcopy( rect)
box2. corner. x += dx
box2. corner. y += dy
return box2
class Point :
""" Represents a point in 2-D space. """
class Rectangle :
"""Represents a rectangle.
attributes: width, height, corner.
"""
if __name__ == '__main__' :
box = Rectangle( )
box. width = 100.0
box. height = 200.0
box. corner = Point( )
box. corner. x = 0.0
box. corner. y = 0.0
box2 = move_rectangle( box, 100 , 100 )
print_point( box2. corner)
print_point( box. corner)
15.7 调试
开始操作对象时 , 可能会遇到一些新的异常 .
如果试图访问一个并不存在的属性 , 会得到AttrbuteErroe :
>> > p = Point( )
>> > p. x = 3
>> > p. y = 4
>> > p. z
AttributeError: 'Point' object has no attribute 'z'
属性错误: 'Point' 对象没有属性'z'
AttributeError: Point instance has no attribute 'z'
属性错误: 'Point' 实例没有属性“z”
如果不清楚一个对象是什么类型 , 可以问 :
>> > type ( p)
< class '__main__.Point' >
< type 'instance' >
如果不确定一个对象是否拥有某个特定的属性 , 可以使用内置函数hasatter :
>> > hasattr ( p, 'x' )
True
>> > hasattr ( p, 'z' )
False
第一个情形可以是任何对象 , 第二个形参是一个包含属性名称的字符串 .
也可以使用try语句来尝试对象是否拥有你需要的属性 :
try :
x = p. x
except AttributeEeeor:
X = 0
这种方法可以使编写适用于不同类型的函数更加容易 .
关于这一主题的更多内容参见 17.9 节 .
15.8 术语表
类 ( class ) : 一个用户定义的类型 . 类定义会新建一个类对象 .
类对象 ( class object ) : 一个包含用户定义类的信息的对象 . 类对象可以用来创建改类型的实例 .
实例 ( instance ) : 属于某个类的一个对象 .
属性 ( sttribute ) : 一个对象中关联的有命名的值 .
内嵌对象 ( embedded object ) : 作为一个对象的属性存储的对象 .
浅复制 ( shallow copy ) : 复制对象的内容 , 包括内嵌对象的引用 ; copy模块中的copy函数实现了这个功能 .
深复制 ( deep copy ) : 复制对象的内容 , 也包括内嵌对象 , 以及它们内嵌的对象 , 依次类推 ;
copy模块中的deepcopy函数实现了这个功能 .
对象图 ( object diagram ) : 一个展示对象 , 对象的属性以及属性的值的图 .
15.9 练习
1. 练习1
定义一个新的名为Circle的类表示圆形 , 它的属性有center和radius ,
其中center ( 中心坐标 ) 是一个Point对象 , 而radius ( 半径 ) 是一个数 .
实例化一个Circle对象来代表一个圆心在 ( 150 , 100 ) , 半径为 75 的圆形 .
编写一个函数point_in_circle , 接收一个Circle对象和一个Point对象 ,
并当Point处于Circle的边界或其内时返回True .
编写一个函数cert_in_circle , 接收一个Circle对象和一个Rectangle对象 ,
并在Rectangle的任何一个角落在Circle之内是返回True .
另外 , 还有一个更难的版本 , 需要在Rectangle的任何部分都落在圆圈之内时返回True .
解答 : https : / / github . com / AllenDowney / ThinkPython2 / blob / master / code / Circle . py
如何判断一个坐标是否在圆内 :
1. 圆心到这个点的距离小于圆的半径 , 则这个点在圆内 .
2. 圆心到这个点的距离等于圆的半径 , 则这个点在圆周上 .
3. 圆心到这个点的距离大于圆的半径 , 则这个点在圆外 .
计算两个坐标的距离 :
| AB | = ( x1 - x2 ) * * 2 + ( y1 - y2 ) * * 2 * * 0.5
class Point :
"""坐标"""
x = None
y = None
class Circle :
"""圆"""
class Rectangle :
"""矩形"""
def distance_between_points ( p1, p2) :
"""
计算两个Point对象之间的距离,
:param p1: 第一个Point对象.
:param p2: 第二个Point对象.
:return: 两个Point对象之间的距离.
"""
return ( ( p1. x - p2. x) ** 2 + ( p1. y - p2. y) ** 2 ) ** 0.5
def point_in_circle ( cir, p) :
distance = distance_between_points( cir. center, p)
print ( '输入的坐标离圆的距离为:%d,' % distance, end= '\t' )
if distance <= cir. radius:
return True
def cert_in_circle ( cir, rect) :
bottom_left = rect. corner
upper_left = Point( )
upper_left. x = rect. width
upper_left. y = bottom_left. y + rect. height
bottom_right = Point( )
bottom_right. y = rect. height
bottom_right. x = bottom_left. x + rect. width
top_right = Point( )
top_right. x = upper_left. x + rect. width
top_right. y = upper_left. y
corner_list = [ ( '左下角' , bottom_left) , ( '左上角' , upper_left) ,
( '右下角' , bottom_right) , ( '右上角' , top_right) ]
count = 0
for corner_name, corner_point in corner_list:
distance = distance_between_points( cir. center, corner_point)
print ( '矩形的%s离圆心的距离为: %d.' % ( corner_name, distance) , end= ' ' )
if distance <= cir. radius:
count += 1
print ( '矩形这个角在圆内!' )
else :
print ( '矩形这个角不在圆内!' )
if count == 4 :
print ( '圆的任何部分都在圆内' )
def main ( p2_obj) :
round1 = Circle( )
round1. center = Point( )
round1. center. x = 150
round1. center. y = 100
round1. radius = 75
is_inside_circle = point_in_circle( round1, p2_obj)
if is_inside_circle:
print ( '坐标在圆内!' )
else :
print ( '坐标不在圆内!' )
rect = Rectangle( )
rect. width = 100
rect. height = 200
rect. corner = p2
cert_in_circle( round1, rect)
if __name__ == '__main__' :
p2 = Point
p2. x = 300
p2. y = 300
main( p2)
2. 练习2
编写一个名为draw_rect的函数 , 接收一个Turtle对象 , 和一个Rectangle对象组我形参 ,
并使用Turtle来绘制这个Rectangle . 如何使用Turtle对象的示例参见第 4 章 .
编写一个draw_cect的函数 , 接收一个Turtle对象和一个Circle对象 , 并绘制出Circle .
解答 : https : / / github . com / AllenDowney / ThinkPython2 / blob / master / code / polygon . py
import turtle
from Point1 import Point, Rectangle
import polygon
class Circle :
""""""
def draw_circle ( t, circle) :
t. pu( )
t. goto( circle. center. x, circle. center. y)
t. fd( circle. radius)
t. lt( 90 )
t. pd( )
polygon. circle( t, circle. radius)
def draw_rect ( t, rect) :
t. pu( )
t. goto( rect. corner. x, rect. corner. y)
t. setheading( 0 )
t. pd( )
for length in rect. width, rect. height, rect. width, rect. height:
t. fd( length)
t. rt( 90 )
if __name__ == '__main__' :
bob = turtle. Turtle( )
length = 400
bob. fd( length)
bob. bk( length)
bob. lt( 90 )
bob. fd( length)
bob. bk( length)
box = Rectangle( )
box. width = 100.0
box. height = 200.0
box. corner = Point( )
box. corner. x = 50.0
box. corner. y = 50.0
draw_rect( bob, box)
circle = Circle
circle. center = Point( )
circle. center. x = 150.0
circle. center. y = 100.0
circle. radius = 75.0
draw_circle( bob, circle)
turtle. mainloop( )