您当前位置: 首页 ‣ 深入 Python 3 ‣
难度等级: ♦♦♦♦♦
❝ 我的专长是在其他人出错时保持正确。 ❞
— 乔治·伯纳德·肖
在本书中,您已经看到了“特殊方法”的示例——Python 在您使用某些语法时会调用某些“魔术”方法。使用特殊方法,您的类可以像集合、字典、函数、迭代器,甚至数字一样工作。本附录既是对我们已经看到的特殊方法的参考,也是对一些更为深奥的特殊方法的简要介绍。
如果您已经阅读了类介绍,那么您已经看到了最常见的特殊方法:__init__() 方法。我编写的绝大多数类最终都需要进行一些初始化。还有一些其他基本特殊方法,对于调试自定义类特别有用。
| 说明 | 您想要… | 因此您编写… | 然后 Python 调用… | 
|---|---|---|---|
| ① | 初始化实例 | x = MyClass() | x.__init__() | 
| ② | 作为字符串的“官方”表示 | repr(x) | x.__repr__() | 
| ③ | 作为字符串的“非正式”值 | str(x) | x.__str__() | 
| ④ | 作为字节数组的“非正式”值 | bytes(x) | x.__bytes__() | 
| ⑤ | 作为格式化字符串的值 | format(x, format_spec) | x.__format__(format_spec) | 
__init__() 方法在实例创建之后调用。如果您想控制实际的创建过程,请使用__new__() 方法。__repr__() 方法应返回一个有效的 Python 表达式的字符串。print(x) 时,也会调用__str__() 方法。bytes 类型。decimal.py 提供了自己的__format__() 方法。在迭代器章节中,您看到了如何使用__iter__() 和__next__() 方法从头开始构建迭代器。
| 说明 | 您想要… | 因此您编写… | 然后 Python 调用… | 
|---|---|---|---|
| ① | 遍历序列 | iter(seq) | seq.__iter__() | 
| ② | 从迭代器获取下一个值 | next(seq) | seq.__next__() | 
| ③ | 以相反顺序创建迭代器 | reversed(seq) | seq.__reversed__() | 
__iter__() 方法在您每次创建新迭代器时都会调用。它是一个用初始值初始化迭代器的良好位置。__next__() 方法在您每次从迭代器中检索下一个值时都会调用。__reversed__() 方法并不常见。它采用现有序列并返回一个迭代器,该迭代器以相反的顺序(从最后一个到第一个)生成序列中的项目。正如您在迭代器章节中所见,for 循环可以作用于迭代器。在这个循环中
for x in seq:
    print(x)Python 3 将调用seq.__iter__() 来创建迭代器,然后调用该迭代器上的__next__() 方法来获取x 的每个值。当__next__() 方法引发StopIteration 异常时,for 循环将优雅地结束。
| 说明 | 您想要… | 因此您编写… | 然后 Python 调用… | 
|---|---|---|---|
| ① | 获取计算属性(无条件) | x.my_property | x.__getattribute__('my_property') | 
| ② | 获取计算属性(回退) | x.my_property | x.__getattr__('my_property') | 
| ③ | 设置属性 | x.my_property = value | x.__setattr__('my_property', value) | 
| ④ | 删除属性 | del x.my_property | x.__delattr__('my_property') | 
| ⑤ | 列出所有属性和方法 | dir(x) | x.__dir__() | 
__getattribute__() 方法,Python 将在对任何属性或方法名的所有引用(特殊方法名除外,因为这会导致令人不快的无限循环)时调用它。__getattr__() 方法,Python 将仅在所有常规位置查找属性后才调用它。如果实例x 定义了属性color,则x.color不会调用x.__getattr__('color');它将只返回x.color 的已定义值。__setattr__() 方法在您将值赋给属性时调用。__delattr__() 方法在您删除属性时调用。__dir__() 方法在您定义__getattr__() 或__getattribute__() 方法时很有用。通常,调用dir(x) 只能列出常规属性和方法。如果您的__getattr__() 方法动态地处理color 属性,则dir(x) 不会将color 列为可用属性之一。覆盖__dir__() 方法允许您将color 列为可用属性,这对想要使用您的类而无需深入了解其内部机制的人来说很有帮助。__getattr__() 和__getattribute__() 方法之间的区别很细微,但很重要。我可以通过两个例子来解释它
class Dynamo:
    def __getattr__(self, key):
        if key == 'color':         ①
            return 'PapayaWhip'
        else:
            raise AttributeError   ②
>>> dyn = Dynamo()
>>> dyn.color ③
'PapayaWhip'
>>> dyn.color = 'LemonChiffon'
>>> dyn.color ④
'LemonChiffon'__getattr__() 方法。如果名称是'color',则该方法将返回一个值。(在这种情况下,它只是一个硬编码的字符串,但您通常会进行某种计算并返回结果。)__getattr__() 方法需要引发AttributeError 异常,否则您的代码在访问未定义属性时会静默失败。(从技术上讲,如果该方法没有引发异常或显式返回值,则它会返回None,即 Python 的空值。这意味着所有未显式定义的属性都将是None,这几乎肯定不是您想要的。)__getattr__() 方法来提供计算值。__getattr__() 方法将不再被调用来提供dyn.color 的值,因为dyn.color 已在实例上定义。另一方面,__getattribute__() 方法是绝对的和无条件的。
class SuperDynamo:
    def __getattribute__(self, key):
        if key == 'color':
            return 'PapayaWhip'
        else:
            raise AttributeError
>>> dyn = SuperDynamo()
>>> dyn.color ①
'PapayaWhip'
>>> dyn.color = 'LemonChiffon'
>>> dyn.color ②
'PapayaWhip'__getattribute__() 方法来提供dyn.color 的值。__getattribute__() 方法仍然被调用来提供dyn.color 的值。如果存在,则无条件地调用__getattribute__() 方法以进行每个属性和方法查找,即使对于在创建实例后显式设置的属性也是如此。☞如果您的类定义了
__getattribute__()方法,那么您可能还想定义__setattr__()方法并在它们之间进行协调以跟踪属性值。否则,您在创建实例后设置的任何属性都会消失到黑洞中。
您需要格外小心__getattribute__() 方法,因为它也在 Python 在您的类上查找方法名时调用。
class Rastan:
    def __getattribute__(self, key):
        raise AttributeError           ①
    def swim(self):
        pass
>>> hero = Rastan()
>>> hero.swim() ②
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in __getattribute__
AttributeError__getattribute__() 方法,该方法始终引发AttributeError 异常。没有属性或方法查找会成功。hero.swim() 时,Python 会在Rastan 类中查找swim() 方法。此查找会经过__getattribute__() 方法,因为所有属性和方法查找都会经过__getattribute__() 方法。在这种情况下,__getattribute__() 方法会引发AttributeError 异常,因此方法查找失败,因此方法调用失败。您可以使类的一个实例可调用——就像函数可调用一样——方法是定义__call__() 方法。
| 说明 | 您想要… | 因此您编写… | 然后 Python 调用… | 
|---|---|---|---|
| 像函数一样“调用”实例 | my_instance() | my_instance.__call__() | 
zipfile 模块使用此方法来定义一个类,该类可以使用给定的密码解密 加密 的zip 文件。zip 解密 算法要求您在解密期间存储状态。将解密器定义为类使您能够在解密器类的单个实例中维护此状态。状态在__init__() 方法中初始化,并在文件被解密 时更新。但由于该类也可以像函数一样“可调用”,因此您可以将该实例作为map() 函数的第一个参数传递,如下所示
# excerpt from zipfile.py
class _ZipDecrypter:
.
.
.
    def __init__(self, pwd):
        self.key0 = 305419896               ①
        self.key1 = 591751049
        self.key2 = 878082192
        for p in pwd:
            self._UpdateKeys(p)
    def __call__(self, c):                  ②
        assert isinstance(c, int)
        k = self.key2 | 2
        c = c ^ (((k * (k^1)) >> 8) & 255)
        self._UpdateKeys(c)
        return c
.
.
.
zd = _ZipDecrypter(pwd)                    ③
bytes = zef_file.read(12)
h = list(map(zd, bytes[0:12]))             ④_ZipDecryptor 类以三个旋转键的形式维护状态,这些键稍后将在_UpdateKeys() 方法(此处未显示)中更新。__call__() 方法,该方法使类实例像函数一样可调用。在这种情况下,__call__() 方法会解密 zip 文件中的单个字节,然后根据已解密的字节更新旋转键。_ZipDecryptor 类的实例。将pwd 变量传递给__init__() 方法,该方法会存储该变量并将其用于首次更新旋转键。__call__() 方法 12 次,该方法会更新其内部状态并返回 12 次结果字节。如果您的类充当一组值的容器——也就是说,如果询问您的类是否“包含”一个值是有意义的——那么它可能应该定义以下特殊方法,使其像集合一样工作。
| 说明 | 您想要… | 因此您编写… | 然后 Python 调用… | 
|---|---|---|---|
| 项目的数量 | len(s) | s.__len__() | |
| 了解其是否包含特定值 | x in s | s.__contains__(x) | 
cgi 模块在其FieldStorage 类中使用这些方法,该类表示提交给动态网页的所有表单字段或查询参数。
# A script which responds to http://example.com/search?q=cgi
import cgi
fs = cgi.FieldStorage()
if 'q' in fs:                                               ①
  do_search()
# An excerpt from cgi.py that explains how that works
class FieldStorage:
.
.
.
    def __contains__(self, key):                            ②
        if self.list is None:
            raise TypeError('not indexable')
        return any(item.name == key for item in self.list)  ③
    def __len__(self):                                      ④
        return len(self.keys())                             ⑤cgi.FieldStorage 类的实例后,可以使用“in”运算符检查查询字符串中是否包含特定参数。__contains__() 方法是实现此功能的魔法。当您说if 'q' in fs 时,Python 会在fs 对象上查找__contains__() 方法,该方法在cgi.py 中定义。值'q' 作为key 参数传递给__contains__() 方法。any() 函数采用生成器表达式,如果生成器吐出任何项目,则返回True。any() 函数足够智能,可以一找到匹配项就停止。FieldStorage 类还支持返回其长度,因此您可以说len(fs),它将在FieldStorage 类上调用__len__() 方法以返回它识别的查询参数数量。self.keys() 方法会检查self.list 是否为None,因此__len__ 方法不需要重复此错误检查。扩展上一节的内容,您可以定义一些类,它们不仅响应“in”运算符和len()函数,而且像完整的字典一样工作,根据键返回值。
| 说明 | 您想要… | 因此您编写… | 然后 Python 调用… | 
|---|---|---|---|
| 通过键获取值 | x[key] | x.__getitem__(key) | |
| 通过键设置值 | x[key] = value | x.__setitem__(key, value) | |
| 删除键值对 | del x[key] | x.__delitem__(key) | |
| 为缺失的键提供默认值 | x[nonexistent_key] | x.__missing__(nonexistent_key) | 
来自cgi模块的FieldStorage类也定义了这些特殊方法,这意味着您可以执行类似的操作
# A script which responds to http://example.com/search?q=cgi
import cgi
fs = cgi.FieldStorage()
if 'q' in fs:
  do_search(fs['q'])                              ①
# An excerpt from cgi.py that shows how it works
class FieldStorage:
.
.
.
    def __getitem__(self, key):                   ②
        if self.list is None:
            raise TypeError('not indexable')
        found = []
        for item in self.list:
            if item.name == key: found.append(item)
        if not found:
            raise KeyError(key)
        if len(found) == 1:
            return found[0]
        else:
            return foundfs对象是cgi.FieldStorage的实例,但您仍然可以评估类似fs['q']的表达式。fs['q']调用__getitem__()方法,并将key参数设置为'q'。然后,它在其内部维护的查询参数列表(self.list)中查找.name与给定键匹配的项目。使用适当的特殊方法,您可以定义自己的像数字一样工作的类。也就是说,您可以将它们相加、相减,并在它们上执行其他数学运算。这就是分数的实现方式——Fraction类实现了这些特殊方法,然后您可以执行以下操作
>>> from fractions import Fraction >>> x = Fraction(1, 3) >>> x / 3 Fraction(1, 9)
以下是实现类似数字的类所需的特殊方法的完整列表。
| 说明 | 您想要… | 因此您编写… | 然后 Python 调用… | 
|---|---|---|---|
| 加法 | x + y | x.__add__(y) | |
| 减法 | x - y | x.__sub__(y) | |
| 乘法 | x * y | x.__mul__(y) | |
| 除法 | x / y | x.__truediv__(y) | |
| 向下取整除法 | x // y | x.__floordiv__(y) | |
| 取模(余数) | x % y | x.__mod__(y) | |
| 向下取整除法 & 取模 | divmod(x, y) | x.__divmod__(y) | |
| 乘方 | x ** y | x.__pow__(y) | |
| 左移位 | x << y | x.__lshift__(y) | |
| 右移位 | x >> y | x.__rshift__(y) | |
| 按位 and | x & y | x.__and__(y) | |
| 按位 xor | x ^ y | x.__xor__(y) | |
| 按位 or | x | y | x.__or__(y) | 
如果x是实现这些方法的类的实例,那么一切都很好。但是,如果它没有实现其中一个方法怎么办?更糟糕的是,如果它实现了它,但不能处理某些类型的参数怎么办?例如
>>> from fractions import Fraction >>> x = Fraction(1, 3) >>> 1 / x Fraction(3, 1)
这不是将Fraction除以整数(如前面的示例)的情况。这种情况很简单:x / 3调用x.__truediv__(3),而Fraction类的__truediv__()方法处理所有数学运算。但整数不知道如何对分数进行算术运算。那么为什么这个示例可以工作呢?
还有一组带有反射操作数的算术特殊方法。给定一个采用两个操作数的算术运算(例如 x / y),有两种方法可以执行它
x用y除以它本身,或者y用它本身除以x上面的特殊方法组采用第一种方法:给定x / y,它们提供了一种方法让x说“我知道如何用y除以我自己”。以下特殊方法组解决了第二种方法:它们提供了一种方法让y说“我知道如何作为分母用我自己除以x。”
| 说明 | 您想要… | 因此您编写… | 然后 Python 调用… | 
|---|---|---|---|
| 加法 | x + y | y.__radd__(x) | |
| 减法 | x - y | y.__rsub__(x) | |
| 乘法 | x * y | y.__rmul__(x) | |
| 除法 | x / y | y.__rtruediv__(x) | |
| 向下取整除法 | x // y | y.__rfloordiv__(x) | |
| 取模(余数) | x % y | y.__rmod__(x) | |
| 向下取整除法 & 取模 | divmod(x, y) | y.__rdivmod__(x) | |
| 乘方 | x ** y | y.__rpow__(x) | |
| 左移位 | x << y | y.__rlshift__(x) | |
| 右移位 | x >> y | y.__rrshift__(x) | |
| 按位 and | x & y | y.__rand__(x) | |
| 按位 xor | x ^ y | y.__rxor__(x) | |
| 按位 or | x | y | y.__ror__(x) | 
但是,等等!还有更多!如果您正在执行“就地”操作,例如x /= 3,则可以定义更多特殊方法。
| 说明 | 您想要… | 因此您编写… | 然后 Python 调用… | 
|---|---|---|---|
| 就地加法 | x += y | x.__iadd__(y) | |
| 就地减法 | x -= y | x.__isub__(y) | |
| 就地乘法 | x *= y | x.__imul__(y) | |
| 就地除法 | x /= y | x.__itruediv__(y) | |
| 就地向下取整除法 | x //= y | x.__ifloordiv__(y) | |
| 就地取模 | x %= y | x.__imod__(y) | |
| 就地乘方 | x **= y | x.__ipow__(y) | |
| 就地左移位 | x <<= y | x.__ilshift__(y) | |
| 就地右移位 | x >>= y | x.__irshift__(y) | |
| 就地按位 and | x &= y | x.__iand__(y) | |
| 就地按位 xor | x ^= y | x.__ixor__(y) | |
| 就地按位 or | x |= y | x.__ior__(y) | 
注意:在大多数情况下,不需要就地操作方法。如果您没有为特定操作定义就地方法,Python 将尝试这些方法。例如,要执行表达式x /= y,Python 将
x.__itruediv__(y)。如果该方法被定义并返回了除NotImplemented以外的值,则我们完成了。x.__truediv__(y)。如果该方法被定义并返回了除NotImplemented以外的值,则x的旧值将被丢弃并替换为返回值,就像您使用x = x / y代替一样。y.__rtruediv__(x)。如果该方法被定义并返回了除NotImplemented以外的值,则x的旧值将被丢弃并替换为返回值。因此,只有在您想对就地操作数进行一些特殊优化时,才需要定义像__itruediv__()方法这样的就地方法。否则,Python 将本质上重新制定就地操作数以使用常规操作数 + 变量赋值。
还有一些“一元”数学运算,您可以对类似数字的对象本身执行。
| 说明 | 您想要… | 因此您编写… | 然后 Python 调用… | 
|---|---|---|---|
| 负数 | -x | x.__neg__() | |
| 正数 | +x | x.__pos__() | |
| 绝对值 | abs(x) | x.__abs__() | |
| 反转 | ~x | x.__invert__() | |
| 复数 | complex(x) | x.__complex__() | |
| 整数 | int(x) | x.__int__() | |
| 浮点数 | float(x) | x.__float__() | |
| 四舍五入到最接近的整数 | round(x) | x.__round__() | |
| 四舍五入到最接近的n位数字 | round(x, n) | x.__round__(n) | |
| 最小的整数 >= x | math.ceil(x) | x.__ceil__() | |
| 最大的整数 <= x | math.floor(x) | x.__floor__() | |
| 将 x截断到最接近的整数,朝向 0 | math.trunc(x) | x.__trunc__() | |
| PEP 357 | 数字作为列表索引 | a_list[x] | a_list[x.__index__()] | 
我将此节从上一节中分离出来,因为比较并不严格地属于数字的范畴。许多数据类型可以比较——字符串、列表,甚至字典。如果您正在创建自己的类,并且有意义地将您的对象与其他对象进行比较,您可以使用以下特殊方法来实现比较。
| 说明 | 您想要… | 因此您编写… | 然后 Python 调用… | 
|---|---|---|---|
| 相等性 | x == y | x.__eq__(y) | |
| 不相等性 | x != y | x.__ne__(y) | |
| 小于 | x < y | x.__lt__(y) | |
| 小于或等于 | x <= y | x.__le__(y) | |
| 大于 | x > y | x.__gt__(y) | |
| 大于或等于 | x >= y | x.__ge__(y) | |
| 在布尔上下文中为真值 | if x | x.__bool__() | 
☞如果您定义了
__lt__()方法但没有定义__gt__()方法,Python 将使用__lt__()方法,并将操作数交换。但是,Python 不会组合方法。例如,如果您定义了__lt__()方法和__eq__()方法,并尝试测试x <= y是否为真,Python 不会按顺序调用__lt__()和__eq__()。它只会调用__le__()方法。
Python 支持序列化和反序列化任意对象。(大多数 Python 参考将此过程称为“pickle”和“unpickle”。)这对于将状态保存到文件并在以后恢复状态很有用。所有本机数据类型都已支持 pickle。如果您创建了一个想要能够 pickle 的自定义类,请阅读有关 pickle 协议的信息,以了解何时以及如何调用以下特殊方法。
| 说明 | 您想要… | 因此您编写… | 然后 Python 调用… | 
|---|---|---|---|
| 自定义对象副本 | copy.copy(x) | x.__copy__() | |
| 自定义对象深层副本 | copy.deepcopy(x) | x.__deepcopy__() | |
| * | 在 pickle 之前获取对象的 state | pickle.dump(x, file) | x.__getstate__() | 
| * | 序列化对象 | pickle.dump(x, file) | x.__reduce__() | 
| * | 序列化对象(新的 pickle 协议) | pickle.dump(x, file, protocol_version) | x.__reduce_ex__(protocol_version) | 
| * | 控制对象在反序列化期间的创建方式 | x = pickle.load(file) | x.__getnewargs__() | 
| * | 在反序列化后恢复对象的 state | x = pickle.load(file) | x.__setstate__() | 
* 要重新创建序列化对象,Python 需要创建一个看起来像序列化对象的新对象,然后设置新对象上所有属性的值。__getnewargs__()方法控制如何创建对象,然后__setstate__()方法控制如何恢复属性值。
with块中使用的类with块定义了一个运行时上下文;当您执行with语句时,您“进入”上下文;当您执行块中的最后一个语句后,您“退出”上下文。
| 说明 | 您想要… | 因此您编写… | 然后 Python 调用… | 
|---|---|---|---|
| 在进入 with块时执行一些特殊操作 | with x | x.__enter__() | |
| 在离开 with块时执行一些特殊操作 | with x | x.__exit__(exc_type, exc_value, traceback) | 
这就是with file 习惯用法的工作原理。
# excerpt from io.py:
def _checkClosed(self, msg=None):
    '''Internal: raise an ValueError if file is closed
    '''
    if self.closed:
        raise ValueError('I/O operation on closed file.'
                         if msg is None else msg)
def __enter__(self):
    '''Context management protocol.  Returns self.'''
    self._checkClosed()                                ①
    return self                                        ②
def __exit__(self, *args):
    '''Context management protocol.  Calls close()'''
    self.close()                                       ③__enter__()和__exit__()方法。__enter__()方法检查文件是否已打开;如果未打开,_checkClosed()方法将引发异常。__enter__()方法几乎总是应该返回self——这是with块将用于调度属性和方法的对象。with块之后,文件对象会自动关闭。如何关闭?在__exit__()方法中,它调用self.close()。☞
__exit__()方法总是会被调用,即使在with代码块内部抛出异常。事实上,如果抛出异常,异常信息将会传递给__exit__()方法。更多细节请参见 With 语句上下文管理器。
有关上下文管理器的更多信息,请参见 自动关闭文件 和 重定向标准输出。
如果你知道自己在做什么,你可以几乎完全控制如何比较类,如何定义属性以及哪些类被认为是你的类的子类。
| 说明 | 您想要… | 因此您编写… | 然后 Python 调用… | 
|---|---|---|---|
| 类构造函数 | x = MyClass() | x.__new__() | |
| * | 类析构函数 | del x | x.__del__() | 
| 仅定义一组特定的属性 | x.__slots__() | ||
| 自定义哈希值 | hash(x) | x.__hash__() | |
| 获取属性的值 | x.color | type(x).__dict__['color'].__get__(x, type(x)) | |
| 设置属性的值 | x.color = 'PapayaWhip' | type(x).__dict__['color'].__set__(x, 'PapayaWhip') | |
| 删除属性 | del x.color | type(x).__dict__['color'].__del__(x) | |
| 控制对象是否是你的类的实例 | isinstance(x, MyClass) | MyClass.__instancecheck__(x) | |
| 控制类是否是你的类的子类 | issubclass(C, MyClass) | MyClass.__subclasscheck__(C) | |
| 控制类是否是你的抽象基类的子类 | issubclass(C, MyABC) | MyABC.__subclasshook__(C) | 
* Python 在何时调用 __del__() 特殊方法非常复杂。要完全理解它,你需要了解 Python 如何跟踪内存中的对象。这是一篇关于 Python 垃圾回收和类析构函数的优秀文章。你还应该阅读关于弱引用、weakref 模块以及可能 gc 模块的知识,以备不时之需。
本附录中提到的模块
zipfile 模块cgi 模块collections 模块math 模块pickle 模块copy 模块abc(“抽象基类”)模块其他轻量级阅读
© 2001–11 马克·皮尔格林