Think Python/类和方法
面向对象特性
[edit | edit source]Python 是一种面向对象编程语言,这意味着它提供了支持面向对象编程的功能。
面向对象编程不容易定义,但我们已经看到了一些它的特性。
- 程序由对象定义和函数定义组成,大多数计算都用对象上的操作来表达。
- 每个对象定义对应于现实世界中的一些对象或概念,而作用于该对象上的函数对应于现实世界对象交互的方式。
例如,时间第 16 章中定义的类对应于人们记录一天中时间的做法,我们定义的函数对应于人们对时间进行的操作类型。类似地,点和矩形类对应于点和矩形的数学概念。
到目前为止,我们还没有利用 Python 提供的支持面向对象编程的功能。这些功能并非严格必要;它们中的大多数为我们已经做过的事情提供了替代语法。但在许多情况下,替代方法更简洁,更准确地传达了程序的结构。
例如,在时间程序中,类定义和随后的函数定义之间没有明显的联系。经过一些检查,很明显每个函数至少接受一个时间对象作为参数。
这个观察结果是方法的动机;方法是与特定类相关联的函数。我们已经看到了字符串、列表、字典和元组的方法。在本节中,我们将为用户定义类型定义方法。
方法在语义上与函数相同,但有两个语法差异
- 方法在类定义内定义,以使类和方法之间的关系显式。
- 调用方法的语法不同于调用函数的语法。
在接下来的几节中,我们将从前两节中获取函数,并将它们转换为方法。这种转换纯粹是机械的;你只需按照一系列步骤即可完成。如果你习惯于从一种形式转换到另一种形式,你将能够为你的任何操作选择最佳形式。
打印对象
[edit | edit source]在第 16 章中,我们定义了一个名为时间的类,在练习 16.1 中,你编写了一个名为 print_time
的函数
class Time(object):
"""represents the time of day.
attributes: hour, minute, second"""
def print_time(time):
print '%.2d:%.2d:%.2d' % (time.hour, time.minute, time.second)
要调用此函数,你必须传递一个时间对象作为参数
>>> start = Time()
>>> start.hour = 9
>>> start.minute = 45
>>> start.second = 00
>>> print_time(start)
09:45:00
要使 print_time
成为方法,我们只需将函数定义移动到类定义内。请注意缩进的改变。
class Time(object):
def print_time(time):
print '%.2d:%.2d:%.2d' % (time.hour, time.minute, time.second)
现在有两种方法可以调用 print_time
。第一种(也是不太常见的一种)方法是使用函数语法
>>> Time.print_time(start)
09:45:00
在此使用点表示法中,时间是类的名称,print_time
是方法的名称。开始作为参数传递。
第二种(也是更简洁的)方法是使用方法语法
>>> start.print_time()
09:45:00
在此使用点表示法中,print_time
是方法的名称(同样),并且开始是调用方法的对象,称为主体。就像句子的主体是句子所指的对象一样,方法调用的主体是方法所指的对象。
在方法内部,主体被分配给第一个参数,因此在这种情况下开始被分配给时间.
按照惯例,方法的第一个参数称为自己,因此更常见的是这样写 print_time
class Time(object):
def print_time(self):
print '%.2d:%.2d:%.2d' % (self.hour, self.minute, self.second)
这样做的原因是隐含的隐喻
- 函数调用的语法,
print_time(start)
,
表明函数是活动代理。它类似于说,“嘿 print_time
!这里有一个你要打印的对象。”
- 在面向对象编程中,对象是活动代理。
类似于 start.print_time()
这样的方法调用表示“嘿开始!请打印你自己。”
这种视角的改变可能更加礼貌,但它并非明显有用。在我们目前看到的示例中,它可能不是。但有时将责任从函数转移到对象可以使编写更通用的函数成为可能,并且使代码更易于维护和重用。
练习 1
[edit | edit source]将 time_to_int
(来自第 '16.4' 节)改写为方法。改写 int_to_time
为方法可能不合适;不清楚你要在哪个对象上调用它!
另一个例子
[edit | edit source]以下是增加(来自第 16.3 节)改写为方法的版本
# inside class Time:
def increment(self, seconds):
seconds += self.time_to_int()
return int_to_time(seconds)
此版本假设 time_to_int
作为方法编写,如练习 17.1 中所示。此外,请注意它是一个纯函数,而不是一个修饰符。
以下是调用增加:
>>> start.print_time()
09:45:00
>>> end = start.increment(1337)
>>> end.print_time()
10:07:17
主体,开始,被分配给第一个参数,自己。参数,1337,被分配给第二个参数,秒.
此机制可能会令人困惑,尤其是在你犯错的情况下。例如,如果你调用增加带有两个参数,你会得到
>>> end = start.increment(1337, 460)
TypeError: increment() takes exactly 2 arguments (3 given)
错误消息最初令人困惑,因为括号中只有两个参数。但主体也被认为是一个参数,因此总共是三个。
更复杂的例子
[edit | edit source]is_after
(来自练习 16.2)稍微复杂一些,因为它接受两个 Time 对象作为参数。在这种情况下,通常将第一个参数命名为自己,第二个参数命名为其他:
# inside class Time:
def is_after(self, other):
return self.time_to_int() > other.time_to_int()
要使用此方法,你必须在一个对象上调用它,并将另一个对象作为参数传递
>>> end.is_after(start)
True
这种语法的一个优点是它几乎像英语一样易懂:“结束在开始之后吗?”
init 方法
[edit | edit source]init 方法(初始化的缩写)是一个特殊方法,在实例化对象时被调用。它的全名是 __init__
(两个下划线字符,后跟初始化,然后是另外两个下划线)。的 init 方法时间类可能如下所示
# inside class Time:
def __init__(self, hour=0, minute=0, second=0):
self.hour = hour
self.minute = minute
self.second = second
__init__
的参数通常与属性具有相同的名称。语句
self.hour = hour
将参数的值小时存储为的属性自己.
参数是可选的,因此如果你调用时间不带任何参数,你将获得默认值。
>>> time = Time()
>>> time.print_time()
00:00:00
如果你提供一个参数,它将覆盖小时:
>>> time = Time (9)
>>> time.print_time()
09:00:00
如果你提供两个参数,它们将覆盖小时和分钟.
>>> time = Time(9, 45)
>>> time.print_time()
09:45:00
如果你提供三个参数,它们将覆盖所有三个默认值。
练习 2
[edit | edit source]为 Point 类编写一个 init 方法,该方法接受 x 和 y 作为可选参数,并将它们分配给相应的属性。
这__str__方法
[edit | edit source]__str__
是一种特殊方法,类似于 __init__
,用于返回对象的字符串表示形式。
例如,这里有一个str用于 Time 对象的方法
# inside class Time:
def __str__(self):
return '%.2d:%.2d:%.2d' % (self.hour, self.minute, self.second)
当你print一个对象时,Python 会调用str方法
>>> time = Time(9, 45)
>>> print time
09:45:00
当我编写新类时,我几乎总是先编写 __init__
,它使实例化对象更容易,以及 __str__
,它对调试很有用。
为 Point 类编写一个 str 方法。创建一个 Point 对象并打印它。
通过定义其他特殊方法,你可以指定运算符对用户定义类型的行为。例如,如果你为时间类定义了一个名为 __add__
的方法,你就可以使用+运算符对 Time 对象。
定义可能如下所示
# inside class Time:
def __add__(self, other):
seconds = self.time_to_int() + other.time_to_int()
return int_to_time(seconds)
使用方法如下
>>> start = Time(9, 45)
>>> duration = Time(1, 35)
>>> print start + duration
11:20:00
当你将+运算符应用于 Time 对象时,Python 会调用 __add__
。当你打印结果时,Python 会调用 __str__
。所以幕后发生了很多事情!
更改运算符的行为使其适用于用户定义的类型被称为 运算符重载。对于 Python 中的每个运算符,都存在一个对应的特殊方法,如 __add__
。有关更多详细信息,请参阅docs.python.org/ref/specialnames.html.
为 Point 类编写一个 'add' 方法。
在上一节中,我们添加了两个 Time 对象,但你可能还想将一个整数添加到一个 Time 对象。以下是 __add__
的一个版本,它检查其他的类型,并调用 add_time
或增加:
# inside class Time:
def __add__(self, other):
if isinstance(other, Time):
return self.add_time(other)
else:
return self.increment(other)
def add_time(self, other):
seconds = self.time_to_int() + other.time_to_int()
return int_to_time(seconds)
def increment(self, seconds):
seconds += self.time_to_int()
return int_to_time(seconds)
内置函数isinstance接受一个值和一个类对象,如果该值是该类的实例,则返回True。
如果其他是 Time 对象,则 __add__
会调用 add_time
。否则,它会假设参数是一个数字,并调用增加。此操作称为 基于类型的分派,因为它根据参数的类型将计算分派到不同的方法。
以下是一些使用+运算符和不同类型的示例
>>> start = Time(9, 45)
>>> duration = Time(1, 35)
>>> print start + duration
11:20:00
>>> print start + 1337
10:07:17
不幸的是,这种加法实现不是可交换的。如果整数是第一个操作数,你将得到
>>> print 1337 + start
TypeError: unsupported operand type(s) for +: 'int' and 'instance'
问题是,Python 不是要求 Time 对象添加一个整数,而是要求一个整数添加一个 Time 对象,它不知道该怎么做。但是,这个问题有一个巧妙的解决方案:特殊方法 __radd__
,它代表“右侧添加”。当 Time 对象出现在+运算符的右侧时,会调用此方法。以下是定义
# inside class Time:
def __radd__(self, other):
return self.__add__(other)
使用方法如下
>>> print 1337 + start
10:07:17
为 Point 编写一个 add 方法,该方法适用于 Point 对象或元组:
- 如果第二个操作数是 Point,则该方法应返回一个新的 Point,其 x 坐标为操作数的 x 坐标之和, y 坐标亦然。
- 如果第二个操作数是元组,则该方法应将元组的第一个元素添加到 x 坐标,将第二个元素添加到 y 坐标,并返回一个具有结果的新 Point。
基于类型的分派在必要时很有用,但(幸运的是)并不总是必要。通常,你可以通过编写对具有不同类型参数有效的功能来避免它。
我们为字符串编写的许多函数实际上适用于任何类型的序列。例如,在第 11.1 节中,我们使用了histogram来计算每个字母在一个词中出现的次数。
def histogram(s):
d = dict()
for c in s:
if c not in d:
d[c] = 1
else:
d[c] = d[c]+1
return d
此函数也适用于列表、元组,甚至字典,只要s的元素是可散列的,以便它们可以用作d.
>>> t = ['spam', 'egg', 'spam', 'spam', 'bacon', 'spam']
>>> histogram(t)
{'bacon': 1, 'egg': 1, 'spam': 4}
中的键。能够处理多种类型的函数称为 多态的。多态性可以促进代码重用。例如,内置函数sum
,它将序列的元素相加,只要序列的元素支持加法,它就有效。由于 Time 对象提供了一个add能够处理多种类型的函数称为 多态的。多态性可以促进代码重用。例如,内置函数:
>>> t1 = Time(7, 43)
>>> t2 = Time(7, 41)
>>> t3 = Time(7, 37)
>>> total = sum([t1, t2, t3])
>>> print total
23:01:00
方法,因此它们适用于
一般来说,如果函数内部的所有操作都适用于给定类型,那么该函数就适用于该类型。
最好的多态性是无意中的那种,即你发现你已经编写的一个函数可以应用于你从未计划过的类型。
调试在程序执行的任何时候,向对象添加属性都是合法的,但是如果你是一个类型理论的死忠,那么拥有具有不同属性集的相同类型的对象是一种可疑的做法。通常,最好在 init 方法中初始化所有对象的属性。如果你不确定对象是否具有特定属性,可以使用内置函数hasattr
(参见第 15.7 节)。
>>> p = Point(3, 4)
>>> print p.__dict__
{'y': 4, 'x': 3}
另一种访问对象属性的方法是通过特殊属性 __dict__
,它是一个将属性名称(作为字符串)和值映射起来的字典
def print_attributes(obj):
for attr in obj.__dict__:
print attr, getattr(obj, attr)
出于调试目的,你可能会发现保留此函数很方便
内置函数print_attributes
遍历对象字典中的项目,并打印每个属性名称及其对应值。getattr
接受一个对象和一个属性名称(作为字符串),并返回属性的值。
- 面向对象语言
- 一种提供诸如用户定义类和方法语法之类的功能的语言,这些功能有助于面向对象编程。
- 面向对象编程
- 一种编程风格,其中数据及其操作被组织成类和方法。
- 方法
- 在类定义内部定义并在该类的实例上调用的函数。
- 主题
- 方法被调用的对象。
- 运算符重载
- 更改运算符的行为,如+,使其适用于用户定义的类型。
- 基于类型的分派
- 一种编程模式,它检查操作数的类型,并为不同的类型调用不同的函数。
- 多态的
- 与能够处理多种类型的函数有关。
本练习是关于 Python 中最常见且最难发现的错误之一的警示故事。
- 为名为Kangaroo的类编写定义,该类具有以下方法
- 一个
__init__
方法,它将名为pouch_contents
的属性初始化为一个空列表。
- 一个名为
put_in_pouch
的方法,它接受任何类型的对象并将其添加到pouch_contents
中。
- 一个
__str__
方法,它返回 Kangaroo 对象及其袋鼠袋内容的字符串表示形式。
通过创建两个Kangaroo对象,并将它们分配给名为kanga和roo的变量,然后添加roo到kanga袋鼠袋内容中,来测试你的代码。
- 下载 thinkpython.com/code/BadKangaroo.py。它包含对之前问题的解决方案,但有一个非常严重的错误。找到并修复该错误。
如果你卡住了,可以下载 thinkpython.com/code/GoodKangaroo.py ,它解释了问题并展示了一个解决方案。
Visual 是一个提供 3-D 图形的 Python 模块。它并不总是包含在 Python 安装中,因此你可能需要从你的软件库中安装它,或者如果它不存在,则从 'vpython.org' 安装。
以下示例创建一个 256 单位宽、长和高的 3-D 空间,并将“中心”设置为点 '(128, 128, 128)'。然后它绘制一个蓝色球体。
from visual import *
scene.range = (256, 256, 256)
scene.center = (128, 128, 128)
color = (0.1, 0.1, 0.9) # mostly blue
sphere(pos=scene.center, radius=128, color=color)
color是一个 RGB 元组;也就是说,元素是 0.0 到 1.0 之间的红-绿-蓝级别(参见wikipedia.org/wiki/RGB_color_model).
如果您运行这段代码,您应该会看到一个黑色背景和蓝色球体的窗口。如果您上下拖动中间按钮,您可以放大和缩小。您还可以通过拖动右键来旋转场景,但由于世界中只有一个球体,因此很难区分。
以下循环创建一个球体立方体
''t = range(0, 256, 51)
for x in t:
for y in t:
for z in t:
pos = x, y, z
sphere(pos=pos, radius=10, color=color)
- 将这段代码放在脚本中并确保它对您有效。
- 修改程序,使立方体中的每个球体都具有与其在 RGB 空间中的位置相对应的颜色。请注意,坐标范围是 0-255,但 RGB 元组范围是 0.0-1.0。
- 下载thinkpython.com/code/color_list.py并使用 `read_colors` 函数生成系统上可用颜色的列表,包括它们的名称和 RGB 值。对于每种命名颜色,在与其 RGB 值相对应的位置绘制一个球体。
您可以在以下位置看到我的解决方案:thinkpython.com/code/color_space.py.