Python中的特殊方法

常见特殊方法

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特有.
  5. format_spec需要符合Format Specification Mini-Language语法规则。

迭代器行为特殊方法

备注 欲实现… 具体实现 Python内部调用…
迭代一个序列 iter(seq) seq.__iter__()
回去迭代器的下一个值 next(seq) seq.__next__()
让迭代器逆序 reversed(seq) seq.__reversed__()
  1. 无论何时创建一个新的迭代器,__iter__()方法都会被调用。
  2. 每获取一次迭代器的值,__next__()会被执行一次。
  3. __reversed__() 方法不常见,它接收一个序列,然后返回一个迭代器,迭代器生成的元素与原序列的顺序是相反的.

计算属性

备注 欲实现… 具体实现… Python内部调用…
获取计算属性(无条件的) x.my_property x.__getattribute__('my_property')
获取计算属性 (fallback) 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__()

如果定义了__getattr__()__getattribute__()方法,则__dir__() 方法是有用的。通常,dir(x) 只会列出常规的属性和方法。如果__getattr__()方法动态处理颜色属性,dir(x)不会将颜色列为可用属性之一。覆盖__dir__()方法允许你将颜色作为一个可用属性列出,这对那些希望使用你的类而不深入其内部的人很有帮助。

__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的空值。这意味着没有显式定义的all属性将是None。)
dyn实例没有一个名为color的属性,因此调用__getattr__()方法来提供一个计算值。
显式设置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'
调用__getattribute__()方法来为dyn.color提供一个值。
即使显式地设置了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文件的单个字节,然后根据被解密的字节更新旋转密钥。
zd_ZipDecryptor 类的一个实例。pwd变量被传递给__init__()方法,在那里它被存储并用于第一次更新旋转键。
给定zip文件的前12个字节,通过将字节映射到zd来解密它们,实际上调用zd 12次,这将调用__call__() 12次,这将更新其内部状态并返回结果字节12次。

集合行为特殊方法

如果充当一组值的容器——也就是说,查询是否“包含”一个值——那么它可能应该定义以下特殊方法。

备注 欲实现… 具体实现… Python内部调用…
获取元素数量 len(s) s.__len__()
是否包含特殊指定值 x in s s.__contains__(x)

cgi 模块在其FieldStorage 类中使用这些方法,该类表示提交到动态web页面的所有表单字段或查询参数。

# 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() 函数接受一个generator表达式,如果生成器输出任何项,则返回True any() 函数足够智能,只要找到第一个匹配就会停止。
同样的FieldStorage 类也支持返回它的长度,所以可以使用len(fs),它会调用FieldStorage类上的__len__() 方法来返回它标识的查询参数的数量。
self.keys()方法检self.list查是否为None,因此__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__()方法,参数设置为'q' 。然后在其内部维护的查询参数列表(self.list)中查找.name与给定键匹配的项。

数行为特殊方法

使用适当的特殊方法,可以定义自己的类,使它们的行为类似于数字。也就是说,可以对它们进行加、减和其他数学运算。这就是分数是如何实现的- Fraction 类实现了这些特殊的方法,然后你可以这样做:

from fractions import Fraction
>>> x = Fraction(1, 3) # 分子为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是实现这些方法的类的实例,那就很好了。但如果它没有实现其中的一个呢?或者更糟,如果它实现了它,但不能处理某些类型的参数怎么办?例如:

from fractions import Fraction
>>> x = Fraction(1, 3)
>>> 1 / x
Fraction(3, 1)

这并非取一个分数除以一个整数的情况(如前面的例子)。这种情况很简单:x / 3调用x.__truediv__(3),而Fraction 类的__truediv__() 方法处理所有的数学运算。但是整数不知道如何用分数做算术运算。那么为什么这个例子行得通呢?

第二组算术特殊方法具有反射操作数。给定一个需要两个操作数的算术运算(例如:x / y),有两种方法:

  1. x除以y, 或者
  2. 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`

但是等等! 还有更多!如果正在执行原位操作,比如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`

如果想对原位操作数做一些特殊的优化,只需要定义原位方法,比如__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__()
四舍五入并设置小数点 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 &lt;= y, Python并不会依次调用__lt__()__eq__() 。它只会调用__le__()方法。😺

序列化行为特殊方法

Python支持序列化和反序列化任意对象。(大多数Python教程将此过程称为picklingunpickling。)这对于将状态保存到文件并在稍后恢复文件非常有用。所有的原生数据类型都已经支持pickle。如果创建了一个希望能够pickle的自定义类,那么请阅读pickle协议,了解何时以及如何调用以下特殊方法。

备注 欲实现… 具体实现… Python内部调用…
自定义对象复制 copy.copy(x) x.__copy__()
深度复制 copy.deepcopy(x) x.__deepcopy__()
* pickling之前获取对象状态 pickle.dump(x, file) x.__getstate__()
* 序列化对象 pickle.dump(x, file) x.__reduce__()
* 序列化对象 pickle.dump(x, file, protocol_version) x.__reduce_ex__(protocol_version)
* 加载序列化对象 x = pickle.load(file) x.__getnewargs__()
* unpickling之后恢复对象的状态 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)

举例:

# 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()                                       ③
file对象同时定义了__enter__() __exit__() 方法。__enter__()方法检查文件是否已打开;如果不是,_checkClosed()方法将引发异常。
__enter__()方法应该几乎总是返回self -这是with块将用来分派属性和方法的对象。
with块之后,file对象自动关闭。怎么做到的?在__exit__() 方法中,它调用self.close()

__exit__() 方法将始终被调用,即使在with块内引发异常。事实上,如果异常被引发,异常信息将被传递给'__exit__() 方法。

更多关于上下文管理的详情, 见 Closing Files AutomaticallyRedirecting Standard Output.

一些深奥的东西

如果你够牛,那么就可以完全控制的实现,比如定义属性,决定哪些类属于你的子类等等。

备注 欲实现… 具体实现… Python内部调用…
类构造器 x = MyClass() x.__new__()
* 销毁类 del x x.__del__()
定义一组特定属性 x.__slots__()
自定义hash 值 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的内存回收机制等,我也不是很懂,就不多说了。


posted @ 2021-02-18 14:04  polyAI  阅读(353)  评论(0编辑  收藏  举报