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) |
- 在实例对象创建之后会立马执行
__init__()
方法。如果想控制实例的创建过程,那么需要使用__new__()
方法。 - 依照惯例,
__repr__()
应该返回一个合法的Python表达式字符串。 print(x)
时会调用__str__()
方法。- Python 3特有.
format_spec
需要符合Format Specification Mini-Language
语法规则。
迭代器行为特殊方法
备注 | 欲实现… | 具体实现 | Python内部调用… |
---|---|---|---|
① | 迭代一个序列 | iter(seq) |
seq.__iter__() |
② | 回去迭代器的下一个值 | next(seq) |
seq.__next__() |
③ | 让迭代器逆序 | reversed(seq) |
seq.__reversed__() |
- 无论何时创建一个新的迭代器,
__iter__()
方法都会被调用。 - 每获取一次迭代器的值,
__next__()
会被执行一次。 __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
),有两种方法:
- 让
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` |
但是等等! 还有更多!如果正在执行原位操作,比如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 <= y
, Python并不会依次调用__lt__()
和__eq__()
。它只会调用__le__()
方法。😺
序列化行为特殊方法
Python支持序列化和反序列化任意对象。(大多数Python教程将此过程称为pickling
和unpickling
。)这对于将状态保存到文件并在稍后恢复文件非常有用。所有的原生数据类型都已经支持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 Automatically 和 Redirecting 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的内存回收机制等,我也不是很懂,就不多说了。