您当前位置: 首页 ‣ 深入 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 found
fs
对象是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 马克·皮尔格林