15. 第十五章 类和对象

15. 类和对象

handlebars 复制代码
到现在你已经知道如何使用函数组织代码, 以及如何使用内置类型来组织数据.
下一步将学习'面向对象编程', 面向对象编程使用自定义的类型同时组织代码和数据.
面向对象编程是一个很大的话题, 需要好几章来讨论.
handlebars 复制代码
本章的代码示例可以从↓下载,
https://github.com/AllenDowney/ThinkPython2/blob/master/code/Point1.py 
练习的解答可以在↓下载.
https://github.com/AllenDowney/ThinkPython2/blob/master/code/Point1_soln.py
15.1 用户定义类型
handlebars 复制代码
我们已经使用了很多Python的内置类型; 现在我们要定义一个新类型.
作为示例, 我们将会新建一个类型Point, 用来表示二维空间中的一个点.
在数学的表示法中, 点通常使用括号中逗号分割两个坐标表示.
例如, (0, 0)表示原点, 而(x, y)表示一个在圆点右侧x单位, 上方y单位的点.

在Python中, 有好几种方法可以表达点.
* 我们可以将两个坐标分别保存到变量x和y中.
* 我们可以将坐标作为列表或元组的元素存储.
* 我们可以新建一个类型用对象表达点.

新建一个类型比其他方法更复杂一些, 但它的优点很快就显现出来.
用户定义的类型也称为'类'(class). 类的定义如下:
python 复制代码
class Point:
	""" Represents a point in 2-D space. """
handlebars 复制代码
定义头表示新的类名为Point. 定义体是一个文档字符串, 解释这个类的用途.
可以在类定义中定义变量和函数, 我们会后面回到这个话题.
定义一个叫作Point的类会创建一个'对象类'(object class).
python 复制代码
>>> Point
<class '__main__.Point'>
handlebars 复制代码
因为Point是在程序顶层定义的, 它的'全名'是__main__.Point.
类对象项一个创建对象的工厂. 要新建一个Point对象, 可以把Point当作函数类调研:
python 复制代码
>>> blank = Point()
>>> blank
<__main__.Point object at 0x00000230EBFBB490>
handlebars 复制代码
返回值是一个Point对象的引用, 它们将它赋值给变量blank.
新建一个对象的过程称为'实例化'(instantiation), 而对象是这个类的一个实例.

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

每个对象都是某个类的实例, 所以'对象'和'实例'这个两个词很多情况下都可以互换,
但是在本章中我们使用'实例'来表示一个自定义类型的对象.
15.2 属性
handlebars 复制代码
可以使用句点表示法给实例赋值:
python 复制代码
>>> blank.x = 3.0
>>> blank.y = 4.0
handlebars 复制代码
这个语法和从模块中选择变量的语法类似, 如math.pi或者strings.whitespace.
但在种情况下, 我们是将值赋值给一个对象的有命名的元元素. 这些元素称为属性(attribute).
作为名词时, 'AT-trib-ute'发音的重音在第一个音节, 这与作为动词的'a-TRIB-ute'不同.

下面的图标展示了这些赋值的结果.
展示一个对象和其属性的状态图称为'对象图'(object diagram), 参见图15-1.
handlebars 复制代码
变量blank引用一个Point对象, 它包含了两个属性. 每个属性引用一个浮点数.
可以使用相同的语法来读取一个属性的值.
python 复制代码
>>> blank.y
4.0
>>> x = blank.x
>>> x
3.0
handlebars 复制代码
表达式blank.x表示, '找打blank引用的对象, 并取得它的x属性的值'.
在这个例子中, 我们将那个值 赋值给一个变量x. 变量x和属性x并不冲突.
可以在任意表达式中使用句点表示法. 例如:
python 复制代码
# %g用于打印浮点型数据时,会去掉多余的零,至多保留六位有效数字.
>>> '(%g, %g)' % (blank.x, blank.y)
'(3, 4)'
>>> import math
>>> distance = math.sqrt(blank.x ** 2 + blank.y ** 2)
>>> distance
5.0
handlebars 复制代码
可以将一个实例作为实参按通常的方式传递. 例如:
python 复制代码
def print_point(p):
	print('(%g, %g)' % (p.x, p.y))
    
handlebars 复制代码
print_point接收一个点作为形参, 并按照属性表达式展示它.
可以传入blank作为实参来调用它:
python 复制代码
>>> print_point(blank)
(3, 4)
handlebars 复制代码
在函数中, p是blank的一个别名, 所以如果函数修改了p, 则blank也会被修改.
handlebars 复制代码
作为练习, 编写一个叫作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
python 复制代码
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 math.sqrt(px ** 2 + py ** 2)
    return ((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2) ** 0.5


res = distance_between_points(blank1, blank2)
print(res)  # 2.8284271247461903
15.3 矩形
handlebars 复制代码
有时候对象因该有哪些属性非常明显, 但也有时候需要你来做决定,
例如, 假设你子啊设计一个表达矩形的类. 你会用什么属性来指定一个矩形的位置和尺寸呢?
可以忽视角度, 为了简单起见, 假定矩形不是垂直的就是水平的.
最少有以下两种可能.
* 可以指定一个矩形的一个角落(或者中心点), 宽度以及高度.
* 可以指定两个相对的角落.
现在还很难说哪一种方案更好, 所以作为示例, 我们仅限实现第一个.
python 复制代码
class Rectangle:
    """Represents a rectangle.
    attributes: width, height, corner.
    """
    
handlebars 复制代码
文档字符列出了属性: width和height是数字, 用来指定左下角的顶点.
要表达一个矩形, 需要实例化一个Rectangle对象, 并对其属性赋值:
python 复制代码
box = Rectangle()
# 设置宽
box.width = 100.0
# 设置高
box.height = 200.0
# 坐标对象 (0, 0)
box.corner = Ponint()
box.corner.x = 0.0
box.corner.y = 0.0
handlebars 复制代码
表达式box.corner.x表示, '去往box引用的对象, 并选择属性corner; 接着去往过那个对象, 并选择属性x'.
handlebars 复制代码
图15-2展示了这个对象的状态. 作为另一个对象的属性存在的对象是'内嵌'的.
15.4 作为返回值得示例
handlebars 复制代码
函数可以返回实例. 
例如, find_center接收一个Rectangle对象作为参数, 并返回一个Point对象,
包含这个Rectangle的中心点的坐标:
python 复制代码
# 计算矩形的中心点.
def find_center(rect):
	p = Point()
	p.x = rect.corner.x + rect.width / 2
	p.y = rect.corner.y + rect.height / 2
	return p
	
handlebars 复制代码
下面是一个示例, 传入box作为实参, 并将结果的point对象赋值给center:
python 复制代码
>>> center = find_center(box)
>>> print_point(center)
(50, 100)
python 复制代码
# 完整代码

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)  # (50, 100)
15.5 对象是可变的
handlebars 复制代码
可以通过一个对象的某个属性赋值来修改它的状态.
例如, 要修改一个矩形的尺寸而保持它的位置不变(左下角坐标为0, 0不变.),
可以修改属性width和height的值:
python 复制代码
box.width = box.width + 50
box.height = box.width.height + 100
handlebars 复制代码
也可以编写函数来修改对象.
例如, grow_rectangle接收一个Rectangle对象和两个数, dwidth, dheight,
并把这些数加到矩形的宽度和高度上:
python 复制代码
def grow_revtangle(rect, dwidth, dheight):
	rect.width += dwight
	rect.height += dheight
	
handlebars 复制代码
下面是展示这个函数效果的实例:
python 复制代码
>>> box.width, box.height
(150.0, 300.0)
>>> grow_rectangle(box, 50, 100)
>>> box.width, box.height
(200.0, 400.0)
handlebars 复制代码
在函数中, rect是box的别名, 所以如果当修改了revt时, box也改变.

作为练习, 编写一个名为move_rectangle的函数, 接收一个Rectangle对象和两个分别名为dx和dy的数值.
它应当通过将dx添加到corner的x坐标和将dy添加到corner的y坐标来改变矩形的位置.
python 复制代码
# 打印坐标
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)  # (0, 0)
    # 移动坐标
    move_rectangle(box, 100, 100)
    print_point(box.corner)  # (100, 100)
15.6 复制
handlebars 复制代码
别名的使用有时候会让程序更难阅读, 因为一个地方的修改可能会给其他地方带来意想不到的变化.
要跟踪掌握所有引用到一个给定对象的变量非常困难.
使用别名的常用替代方案是复制对象. copy模块里有一个函数copy可以复制任何对象:
python 复制代码
>>> p1 = Point()
>>> p1.x = 3.0
>>> p1.y = 4.0
>>> import copy
>>> p2 = copy.copy(p1)
handlebars 复制代码
p1和p2包含相同的数据, 但是它们不是同一个Point对象.
python 复制代码
>>> print_point(p1)
(3, 4)
>>> print_point(p2)
(3, 4)
>>> p1 is p2
False
>>> p1 == p2
False
handlebars 复制代码
正如我们预料, is操作符告诉我们p1和p2不是同一个对象.
但你可能会预料==能得到True值, 因为这两个点(坐标点)包含相同的数据.
如果那样, 你会失望地发现对于实例来说, ==操作符的默认行为和is操作符相同,
它会检查对象同一性, 而不是对象相等性.
这是因为对于用户自定义类型, Python并不知道怎么才算相等. 至少现在还不行.


对象同一性(object identity): 当两个引用类型的变量存储的地址相同时, 它们引用的是同一个对象.
在Python中, ==操作符的默认行为是检查两个对象的值是否相等.
而is操作符则检查两个对象是否是同一个对象,即它们是否具有相同的内存地址。

对于内置类型(例如整数、浮点数、字符串等)Python已经定义了如何判断相等性.
但对于自定义类型, Python不会自动判断相等性, 因为它不知道如何判断两个对象是否相等.
因此, 如果你定义了自己的类, 你需要自己定义__eq__()方法来定义该类的相等性行为.
在这种情况下, ==操作符将使用您定义的__eq__()方法进行比较.

需要注意的是, 即使您定义了__eq__()方法, 使用is操作符也不会调用该方法,
因为is操作符只检查两个对象是否具有相同的内存地址.
handlebars 复制代码
如果使用copy.copy复制一个Rectangle, 你会发现它复制了Rectangle对象但并不复制内嵌的Point对象:
python 复制代码
>>> box2 = copy.copy(box)
>>> box2 is box
False
# corner引用内嵌对象
>>> box2.corner is box.corner
True
handlebars 复制代码
图15-3展示了这个操作的对象图.
这个操作成为浅复制(shallow copy), 因为它复制对象及其包含的任何引用, 但不复制内嵌对象.
(复制了内嵌对象的引用, 两个Recrangle对象的corner属性共用一个内嵌对象的引用,
哪个Recrangle对象对内嵌对象做了改动都会影响另一个Recrangle对象.)
handlebars 复制代码
对大多数应用, 这并不是你所想要的.
在这个例子里, 对一个Recrangle对象调用grow_rectangle并不影响其他对象,
当对任何Recrangle对象调用move_rectangle都会影响全部两个对象!
这种行为即混乱不清, 又容易导致错误.
handlebars 复制代码
幸好, copy模块还提供了一个名为deepcopy的方法, 它不但赋值对象, 还会复制对象中引用的对象,
甚至它们引用的对象, 以此类推.
所以你并不会惊讶这个操作为何称为深复制(deep copy).
python 复制代码
>>> box3 = copy.deepcopy(box)
>>> box3 is box
False
>>> box3.corner is box.corner
False
handlebars 复制代码
box3个box是两个完全分开的对象.
handlebars 复制代码
作为练习, 编写move_rectangle的另一个版本, 它会新建并返回一个Rectangle对象, 而不是直接修改旧对象.
python 复制代码
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):
    # 深复制一个Rectangle对象
    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)  # (100, 100)
    # 旧矩形对象不变
    print_point(box.corner)  # (0, 0)
15.7 调试
handlebars 复制代码
开始操作对象时, 可能会遇到一些新的异常.
如果试图访问一个并不存在的属性, 会得到AttrbuteErroe:
python 复制代码
>>> 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"
handlebars 复制代码
如果不清楚一个对象是什么类型, 可以问:
python 复制代码
>>> type(p)
<class '__main__.Point'>

# 2.7版本, 显示 instance实例
<type 'instance'>
handlebars 复制代码
如果不确定一个对象是否拥有某个特定的属性, 可以使用内置函数hasatter:
python 复制代码
>>> hasattr(p, 'x')
True
>>> hasattr(p, 'z')
False
handlebars 复制代码
第一个情形可以是任何对象, 第二个形参是一个包含属性名称的字符串.
也可以使用try语句来尝试对象是否拥有你需要的属性:
python 复制代码
try:
	x = p.x
except AttributeEeeor:
	X = 0
	
handlebars 复制代码
这种方法可以使编写适用于不同类型的函数更加容易. 
关于这一主题的更多内容参见17.9节.
15.8 术语表
handlebars 复制代码
类(class): 一个用户定义的类型. 类定义会新建一个类对象.

类对象(class object): 一个包含用户定义类的信息的对象. 类对象可以用来创建改类型的实例.

实例(instance): 属于某个类的一个对象.

属性(sttribute): 一个对象中关联的有命名的值.

内嵌对象(embedded object): 作为一个对象的属性存储的对象.

浅复制(shallow copy): 复制对象的内容, 包括内嵌对象的引用; copy模块中的copy函数实现了这个功能.

深复制(deep copy): 复制对象的内容, 也包括内嵌对象, 以及它们内嵌的对象, 依次类推;
	copy模块中的deepcopy函数实现了这个功能.

对象图(object diagram): 一个展示对象, 对象的属性以及属性的值的图.
15.9 练习
1. 练习1
handlebars 复制代码
定义一个新的名为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
handlebars 复制代码
如何判断一个坐标是否在圆内:
1. 圆心到这个点的距离小于圆的半径, 则这个点在圆内.
2. 圆心到这个点的距离等于圆的半径, 则这个点在圆周上.
3. 圆心到这个点的距离大于圆的半径, 则这个点在圆外.


计算两个坐标的距离:
|AB| = (x1 - x2) ** 2 + (y1 - y2) ** 2 ** 0.5
python 复制代码
# 自定义对象
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
    # 左上角, y + height
    upper_left = Point()
    upper_left.x = rect.width
    upper_left.y = bottom_left.y + rect.height
    # 右下角 x + width
    bottom_right = Point()
    bottom_right.y = rect.height
    bottom_right.x = bottom_left.x + rect.width

    # 右上角
    top_right = Point()
    # x = 左上角加width
    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()
    # 设置圆的坐标150.0
    round1.center = Point()
    round1.center.x = 150
    round1.center.y = 100
    # 设置圆的半径75
    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
handlebars 复制代码
编写一个名为draw_rect的函数, 接收一个Turtle对象, 和一个Rectangle对象组我形参,
并使用Turtle来绘制这个Rectangle. 如何使用Turtle对象的示例参见第4章.

编写一个draw_cect的函数, 接收一个Turtle对象和一个Circle对象, 并绘制出Circle.
解答: https://github.com/AllenDowney/ThinkPython2/blob/master/code/polygon.py
python 复制代码
import turtle

from Point1 import Point, Rectangle
# polygon.py 文件以及写的练习文件.
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()
相关推荐
算法小白(真小白)2 小时前
低代码软件搭建自学第二天——构建拖拽功能
python·低代码·pyqt
唐小旭2 小时前
服务器建立-错误:pyenv环境建立后python版本不对
运维·服务器·python
007php0072 小时前
Go语言zero项目部署后启动失败问题分析与解决
java·服务器·网络·python·golang·php·ai编程
Chinese Red Guest3 小时前
python
开发语言·python·pygame
骑个小蜗牛3 小时前
Python 标准库:string——字符串操作
python
黄公子学安全5 小时前
Java的基础概念(一)
java·开发语言·python
程序员一诺6 小时前
【Python使用】嘿马python高级进阶全体系教程第10篇:静态Web服务器-返回固定页面数据,1. 开发自己的静态Web服务器【附代码文档】
后端·python
小木_.6 小时前
【Python 图片下载器】一款专门为爬虫制作的图片下载器,多线程下载,速度快,支持续传/图片缩放/图片压缩/图片转换
爬虫·python·学习·分享·批量下载·图片下载器
Jiude7 小时前
算法题题解记录——双变量问题的 “枚举右,维护左”
python·算法·面试