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