跳转到内容

Think Python/类和方法

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

面向对象特性

[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对象,并将它们分配给名为kangaroo的变量,然后添加rookanga袋鼠袋内容中,来测试你的代码。

  • 下载 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.

进一步阅读

[edit | edit source]
华夏公益教科书