您当前位置: 首页 深入 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)
  1. __init__() 方法在实例创建之后调用。如果您想控制实际的创建过程,请使用__new__() 方法
  2. 按照惯例,__repr__() 方法应返回一个有效的 Python 表达式的字符串。
  3. 当您print(x) 时,也会调用__str__() 方法。
  4. Python 3 中的新增内容,因为引入了bytes 类型。
  5. 按照惯例,format_spec 应符合格式规范迷你语言。Python 标准库中的decimal.py 提供了自己的__format__() 方法。

充当迭代器的类

迭代器章节中,您看到了如何使用__iter__()__next__() 方法从头开始构建迭代器。

说明 您想要… 因此您编写… 然后 Python 调用…
遍历序列 iter(seq) seq.__iter__()
从迭代器获取下一个值 next(seq) seq.__next__()
以相反顺序创建迭代器 reversed(seq) seq.__reversed__()
  1. __iter__() 方法在您每次创建新迭代器时都会调用。它是一个用初始值初始化迭代器的良好位置。
  2. __next__() 方法在您每次从迭代器中检索下一个值时都会调用。
  3. __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__()
  1. 如果您的类定义了__getattribute__() 方法,Python 将在对任何属性或方法名的所有引用(特殊方法名除外,因为这会导致令人不快的无限循环)时调用它。
  2. 如果您的类定义了__getattr__() 方法,Python 将仅在所有常规位置查找属性后才调用它。如果实例x 定义了属性color,则x.color不会调用x.__getattr__('color');它将只返回x.color 的已定义值。
  3. __setattr__() 方法在您将值赋给属性时调用。
  4. __delattr__() 方法在您删除属性时调用。
  5. __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'
  1. 属性名称作为字符串传递给__getattr__() 方法。如果名称是'color',则该方法将返回一个值。(在这种情况下,它只是一个硬编码的字符串,但您通常会进行某种计算并返回结果。)
  2. 如果属性名称未知,则__getattr__() 方法需要引发AttributeError 异常,否则您的代码在访问未定义属性时会静默失败。(从技术上讲,如果该方法没有引发异常或显式返回值,则它会返回None,即 Python 的空值。这意味着所有未显式定义的属性都将是None,这几乎肯定不是您想要的。)
  3. dyn 实例没有名为color 的属性,因此调用__getattr__() 方法来提供计算值。
  4. 在显式设置dyn.color 之后,__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'
  1. 调用__getattribute__() 方法来提供dyn.color 的值。
  2. 即使在显式设置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
  1. 此类定义了__getattribute__() 方法,该方法始终引发AttributeError 异常。没有属性或方法查找会成功。
  2. 当您调用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]))             
  1. _ZipDecryptor 类以三个旋转键的形式维护状态,这些键稍后将在_UpdateKeys() 方法(此处未显示)中更新。
  2. 该类定义了__call__() 方法,该方法使类实例像函数一样可调用。在这种情况下,__call__() 方法会解密 zip 文件中的单个字节,然后根据已解密的字节更新旋转键。
  3. zd_ZipDecryptor 类的实例。将pwd 变量传递给__init__() 方法,该方法会存储该变量并将其用于首次更新旋转键。
  4. 给定 zip 文件的前 12 个字节,通过将这些字节映射到zd 来对其进行解密,实际上是“调用”zd 12 次,这会调用__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())                             
  1. 创建cgi.FieldStorage 类的实例后,可以使用“in”运算符检查查询字符串中是否包含特定参数。
  2. __contains__() 方法是实现此功能的魔法。当您说if 'q' in fs 时,Python 会在fs 对象上查找__contains__() 方法,该方法在cgi.py 中定义。值'q' 作为key 参数传递给__contains__() 方法。
  3. any() 函数采用生成器表达式,如果生成器吐出任何项目,则返回Trueany() 函数足够智能,可以一找到匹配项就停止。
  4. 相同的FieldStorage 类还支持返回其长度,因此您可以说len(fs),它将在FieldStorage 类上调用__len__() 方法以返回它识别的查询参数数量。
  5. 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
  1. fs对象是cgi.FieldStorage的实例,但您仍然可以评估类似fs['q']的表达式。
  2. 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),有两种方法可以执行它

  1. 告诉xy除以它本身,或者
  2. 告诉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 将

  1. 尝试调用x.__itruediv__(y)。如果该方法被定义并返回了除NotImplemented以外的值,则我们完成了。
  2. 尝试调用x.__truediv__(y)。如果该方法被定义并返回了除NotImplemented以外的值,则x的旧值将被丢弃并替换为返回值,就像您使用x = x / y代替一样。
  3. 尝试调用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()                                       
  1. 文件对象定义了__enter__()__exit__()方法。__enter__()方法检查文件是否已打开;如果未打开,_checkClosed()方法将引发异常。
  2. __enter__()方法几乎总是应该返回self——这是with块将用于调度属性和方法的对象。
  3. 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 模块的知识,以备不时之需。

进一步阅读

本附录中提到的模块

其他轻量级阅读

© 2001–11 马克·皮尔格林