通过实现特殊方法,自定义数据类型可以表现得跟内置类型一样,从而让我们写出更具表达力的代码——或者说,更具 Python 风格的代码。
功能 | 协议接口 |
---|---|
+ | __add__ |
* | __mul__ |
str() | 先查找是否实现 __str__ 协议,没有查找是否实现 __repr__ |
bool() | 默认情况下,我们自己定义的类的实例总被认为是真的,除非这个类对 __bool__ 或者 __len__ 函数有自己的实现。bool(x) 的背后是调用 x.__bool__() 的结果;如果不存在 __bool__ 方法,那么 bool(x) 会尝试调用 x.__len__()。若返回 0,则 bool 会返回 False;否则返回 True |
字符串 / 字节序列 表示形式 | __repr__、__str__、__format__、__bytes__ |
数值转换 | __abs__、__bool__、__complex__、__int__、__float__、__hash__、__index__ |
集合模拟 | __len__、__getitem__、__setitem__、__delitem__、__contains__ |
迭代枚举 | __iter__、__reversed__、__next__ |
可调用模拟 | __call__ |
上下文管理 | __enter__、__exit__ |
实例创建和销毁 | __new__、__init__、__del__ |
属性管理 | __getattr__、__getattribute__、__setattr__、__delattr__、__dir__ |
属性描述符 | __get__、__set__、__delete__ |
跟类相关的服务 | __prepare__、__instancecheck__、__subclasscheck__ |
一元运算符 | __neg__ -、__pos__ +、__abs__ abs() |
众多比较运算符 | __lt__ <、__le__ <=、__eq__ ==、__ne__ !=、__gt__ >、__ge__ >= |
算术运算符 | __add__ +、__sub__ -、__mul__ *、__truediv__ /、__floordiv__ //、__mod__ %、__divmod__ divmod()、__pow__ ** 或 pow()、__round__ round() |
反向算术运算符 | __radd__、__rsub__、__rmul__、__rtruediv__、__rfloordiv__、__rmod__、__rdivmod__、__rpow__ |
增量赋值算术运算符 | __iadd__、__isub__、__imul__、__itruediv__、__ifloordiv__、__imod__、__ipow__ |
位运算符 | __invert__ ~、__lshift__ <<、__rshift__ >>、__and__ &、__or__ |、__xor__ ^ |
反向位运算符 | __rlshift__、__rrshift__、__rand__、__rxor__、__ror__ |
增量赋值位运算符 | __ilshift__、__irshift__、__iand__、__ixor__、__ior__ |
更具体细致的表格总结如下
1.基础知识
- 对
__init__()
方法的调用发生在实例被创建 之后 。如果要控制实际创建进程,请使用__new__()
方法 - 按照约定,
__repr__()
方法所返回的字符串为合法的 Python表达式。 - 在调用
print(x)
的同时也调用了__str__()
方法。 - 由于
bytes
类型的引入而从Python 3
开始出现 - 按照约定,format_spec 应当遵循
迷你语言格式规范
Python 标准类库中的decimal.py
提供了自己的__format__()
方法。
2.行为方式与迭代器类似的类
- 无论何时创建
迭代器
都将调用__iter__()
方法。这是用初始值对迭代器进行初始化的绝佳之处。 - 无论何时从迭代器中获取下一个值都将调用
__next__()
方法。 __reversed__()
方法并不常用。它以一个现有
序列为参数,并将该序列中所有元素从尾到头以逆序排列
生成一个新的迭代器
3.计算属性
- 如果某个类定义了
__getattribute__()
方法,在 每次引用属性或方法名称时Python
都调用它(特殊方法名称除外,因为那样将会导致讨厌的无限循环) - 如果某个类定义了
__getattr__()
方法,Python
将只在正常的位置查询属性时才会调用它。如果实例x
定义了属性color
,x.color
将 不会 调用x.__getattr__('color');
而只会返回x.color
已定义好的值。 - 无论何时给
属性赋值
,都会调用__setattr__()
方法。 - 无论何时
删除
一个属性,都将调用__delattr__()
方法。 - 如果定义了
__getattr__()
或__getattribute__()
方法,__dir__()
方法将非常有用。通常,调用dir(x)
将只显示正常的属性和方法。如果__getattr()__
方法动态
处理color
属性,dir(x)
将不会将color
列为可用属性。可通过覆盖__dir__()
方法允许将color
列为可用属性,对于想使用你的类但却不想深入其内部的人来说,该方法非常有益。
__getattr__()
和 __getattribute__()
方法的区别非常细微,但非常重要。可以用两个例子来解释一下
# -*- coding: utf-8 -*-
class Dynamo:
# 属性名称以字符串的形式传入 __getattr()__ 方法。如果名
# 称为 'color',该方法返回一个值。(在此情况下,它只是一个
# 硬编码的字符串,但可以正常地进行某些计算并返回结果。)
def __getattr__(self, key):
if key == 'color':
return 'PapayaWhip'
else:
# 如果属性名称未知, __getattr()__ 方法必须引发一个
# AttributeError 例外,否则在访问未定义属性时,代码将只会
# 默默地失败。(从技术角度而言,如果方法不引发例外或显式
# 地返回一个值,它将返回 None ——Python 的空值。这意味着 所
# 有 未显式定义的属性将为 None,几乎可以肯定这不是你想看到
# 的。)
raise AttributeError
if __name__ == '__main__':
dyn = Dynamo()
# dyn 实例没有名为 color 的属性,因此在提供计算值时将调用
# __getattr__() 。
print(dyn.color)
# PapayaWhip
dyn.color = 'LemonChiffon'
# 在显式地设置 dyn.color 之后,将不再为提供 dyn.color 的
# 值而调用 __getattr__() 方法,因为 dyn.color 已在该实例中定
# 义。
print(dyn.color)
# LemonChiffon
另一方面,__getattribute__()
方法是绝对的、无条件的。
# -*- coding: utf-8 -*-
class SuperDynamo:
def __getattribute__(self, key):
if key == 'color':
return 'PapayaWhip'
else:
raise AttributeError
if __name__ == '__main__':
dyn = SuperDynamo()
# 在获取 dyn.color 的值时将调用 __getattribute__() 方法
print(dyn.color)
# PapayaWhip
dyn.color = 'LemonChiffon'
# 即便已经显式地设置 dyn.color,在获取 dyn.color 的值时,
# 仍将调用 __getattribute__() 方法。如果存在
# __getattribute__() 方法,将在每次查找属性和方法时 无条件
# 地调用 它,哪怕在创建实例之后已经显式地设置了属性。
print(dyn.color)
# PapayaWhip
tips:
如果定义了类的 __getattribute__()
方法,你可能还想定义一个 __setattr__()
方法,并在两者之间进行协同
,以跟踪属
性的值。否则,在创建实例之后所设置的值将会消失在黑洞
中。
必须特别小心 __getattribute__()
方法,因为 Python
在查找
类的方法名称
时也将对其进行调用。
# -*- coding: utf-8 -*-
class Rastan:
def __getattribute__(self, key):
# 该类定义了一个总是引发 AttributeError 例外的
# __getattribute__() 方法。没有属性或方法的查询会成功。
raise AttributeError
def swim(self):
return "swim"
if __name__ == '__main__':
hero = Rastan()
# 调用 hero.swim() 时,Python 将在 Rastan 类中查找 swim()
# 方法。该查找将执行整个 __getattribute__() 方法,因为所有
# 的属性和方法查找都通过 __getattribute__() 方法。在此例
# 中, __getattribute__() 方法引发 AttributeError 例外,因此
# 该方法查找过程将会失败,而方法调用也将失败。
hero.swim()
# Traceback (most recent call last):
# File "C:/myFiles/company_project/xbot/my_test/getattribute_test.py", line 22, in <module>
# hero.swim()
# File "C:/myFiles/company_project/xbot/my_test/getattribute_test.py", line 9, in __getattribute__
# raise AttributeError
# AttributeError
4.行为方式与函数类似的类
可以让类
的实例变得可调用
——就像函数可以调用一样——通过定义 __call__()
方法。
5.行为方式与序列类似的类
如果类
作为一系列值的容器
出现——也就是说如果对某个类来说,是否“包含
”某值是件有意义的事情——那么它也许应该定义
下面的特殊方法已,让它的行为
方式与序列
类似。
6.行为方式与字典类似的类
7.行为方式与数值类似的类
使用适当的特殊方法,可以将类的行为
方式定义为与数字相仿
。也就是说,可以进行相加、相减
,并进行其它数学运算
。
之前提到的特殊方法集合采用了第一种
方式:对于给定 x / y
,它们为 x
提供了一种途径来表述“我知道如何将自己除以 y
。”下
面的特殊方法集合采用了第二种
方法:它们向 y
提供了一种途径来表述“我知道如何成为分母,并用自己去除 x
。
对于复合运算
(原地操作符)
注意:多数情况下,并不需要
原地操作方法。如果未对特定运算定义“就地
”方法,Python
将会试着使用(普通)方法。例
如,为执行表达式 x /= y
,Python
将会:
- 试着调用
x.__itruediv__(y)
。如果该方法已经定义,并返回了NotImplemented
之外的值,那已经大功告成了。 - 试图调用
x.__truediv__(y)
。如果该方法已定义并返回一个NotImplemented
之外的值,x
的旧值将被丢弃,并将所返回的
值替代它,就像是进行了x = x / y
运算。
试图调用y.__rtruediv__(x)
。如果该方法已定义并返回了一个NotImplemented
之外的值,x
的旧值将被丢弃,并用所返回值进行替换
因此如果想对原地运算进行优化
,仅需像 __itruediv__()
方法一样定义“原地
”方法。否则,基本上 Python
将会重新生成原地
运算公式,以使用常规的运算及变量赋值。
还有一些“一元
”数学运算,可以对“类‐数字
”对象自己执行
8.可比较的类
如果要创建自己的类,且对象之间的比较有意义,可以使用下面的特殊方法来实现比较。
tips:
如果定义了 __lt__()
方法但没有定义__gt__()
方法,Python
将通过经交换的算子
调用 __lt__()
方法。然而,Python
并不
会组合方法。例如,如果定义了 __lt__()
方法和 __eq()__
方法,并试图测试是否 x<= y
,Python
不会按顺序调用 __lt__()
和__eq()__
。它将只调用 __le__()
方法。
9.可序列化的类
Python
支持 任意对象
的序列化
和反序列化
。(多数 Python
参考资料称该过程为 “pickling
” 和 “unpickling
”)。该技术对与将状态保存为文件并在稍后恢复它非常有意义。所有的 内置数据类型 均已支持 pickling
。如果创建了自定义类,且希望它能够pickle
,阅读 pickle
协议 了解下列特殊方法何时以及如何被调用。
要重建
序列化对象,Python
需要创建
一个和被序列化的对象看起来一样的新对象
,然后设置
新对象的所有属性。__getnewargs__()
方法控制新对象的创建过程
,而__setstate__()
方法控制属性值的还原方式
。
10.可在WITH语块中使用的类
with
语块定义了 运行时刻上下文环境;在执行 with
语句时将“进入
”该上下文环境,而执行该语块中的最后一条语句将“退出
”
该上下文环境。
该 __exit__()
方法将总是被调用,哪怕是在 with
语块中引发了例外。实际上,如果引发了例外,该例外信息将会被传递给 __exit__()
方法。查阅 With 状态上下文环境管理器 了解更多细节。
11.真正神奇的东西
如果知道自己在干什么,你几乎可以完全控制类是如何比较的、属性如何定义,以及类的子类是何种类型。
tips: 确切掌握 Python
何时调用 __del__()
特别方法 是件难以置信
的复杂事情。要想完全理解它,必须清楚 Python
如何在内存中跟踪对象。了解 Python 垃圾收集和类析构器
。还可以阅读 《弱引用
》、《weakref 模块
》,还可以将 《gc模块
》 当作补充阅读材料。