09-特殊方法概述
约 5950 字大约 20 分钟
2026-09-28
Python 定义了大量特殊方法,用来连接自定义对象与语言语法、内置函数和标准库协议。
这些方法不适合按名称逐个背诵。更实用的理解方式是:先确定希望对象支持什么公开操作,再查找该操作对应的协议。
例如:
希望支持 repr(obj) -> 研究对象表示协议
希望支持 obj[key] -> 研究容器协议
希望支持 for item in obj -> 研究迭代协议
希望支持 left + right -> 研究数值运算协议
希望支持 with obj -> 研究上下文管理协议本节按使用场景梳理常见特殊方法。前面已经详细讲过的方法只做归类;尚未展开的高级协议会说明用途、调用入口和关键约定,但不会把每一种方法都实现一遍。
1. 阅读特殊方法名称时先看公开入口
特殊方法是协议的实现端,公开语法才是调用端。
len(obj) # 不优先写 obj.__len__()
obj[key] # 不优先写 obj.__getitem__(key)
left + right # 不优先写 left.__add__(right)
with resource: # 不手动拼接 __enter__ 和 __exit__
...这样使用有几个原因:
- 公开语法能够触发完整的查找和回退规则;
- 内置函数会执行必要的返回值检查;
- 二元运算可能需要尝试右侧操作数的反向方法;
- 上下文管理需要确保退出逻辑在异常情况下仍被调用;
- 解释器可能为内置类型采用优化路径。
下面每一组都会先给出调用者使用的入口,再说明对应方法。
2. 对象的字符串和字节表示
这一组方法决定对象如何转换成文本、格式化结果或字节数据。
__repr__ 与 __str__
常用入口是:
repr(obj)
str(obj)
print(obj)
f"{obj}"对应的方法是:
__repr__:面向开发者的明确表示;__str__:面向使用者的友好文本。
没有 __str__ 时,str(obj) 会回退到 __repr__。两者都必须返回 str。
__format__
入口是:
format(obj, specification)
f"{obj:specification}"__format__(self, format_spec) 负责解释格式说明。例如,日期可以选择不同日期格式,金额可以选择货币或纯数值显示。
格式说明是对象公开接口的一部分。对于未知说明,应明确抛出 ValueError,不要静默地产生含义不明的结果。
__bytes__
入口是:
bytes(obj)__bytes__ 必须返回 bytes,适合定义对象的标准二进制表示:
class Packet:
def __init__(self, payload):
self.payload = payload
def __bytes__(self):
return self.payload.encode("utf-8")
packet = Packet("ready")
print(bytes(packet))输出为:
b'ready'__bytes__ 不等同于通用序列化协议。若对象需要跨版本存储、网络传输或安全反序列化,还要明确数据格式、版本、字符编码和错误处理。
__fspath__
路径类对象可以实现文件系统路径协议:
import os
class ProjectFile:
def __init__(self, path):
self.path = path
def __fspath__(self):
return self.path
file = ProjectFile("reports/summary.txt")
print(os.fspath(file))reports/summary.txt__fspath__ 必须返回 str 或 bytes。支持路径协议的标准库 API 可以直接接收该对象,而不要求调用者先读取某个 .path 属性。
3. 转换、真假、索引与哈希
这一组方法让对象转换成基础值,或参与依赖基础值的语言操作。
数值转换
常见入口及对应方法包括:
int(obj)使用__int__;float(obj)使用__float__;complex(obj)使用__complex__。
例如:
class Percentage:
def __init__(self, value):
self.value = value
def __float__(self):
return self.value / 100
percentage = Percentage(25)
print(float(percentage))0.25转换结果必须符合相应类型协议。__float__ 应返回浮点数,不能返回字符串 "0.25"。
__index__ 与 __int__ 不是同一个承诺
__int__ 表示“对象可以转换成整数”,转换过程可能允许丢失信息。
__index__ 表示“对象本身就是一个精确整数值”,可用于切片、位运算和某些要求无损整数的标准库 API。
class Position:
def __init__(self, value):
self.value = value
def __index__(self):
return self.value
items = ["zero", "one", "two"]
print(items[Position(1)])one不要仅仅为了让对象能被 int() 转换,就实现 __index__。后者会向调用者承诺该对象可以安全地充当精确索引。
__bool__
入口包括 bool(obj)、if obj、while obj 和 not obj。
Python 优先使用 __bool__,缺少时回退到 __len__,两者都没有时对象默认为真。__bool__ 必须返回真正的 bool。
__hash__
入口是:
hash(obj)字典键和集合元素需要可哈希。哈希设计必须遵守核心不变量:
如果 a == b,那么 hash(a) 必须等于 hash(b)可哈希对象参与相等性判断的状态还必须保持稳定。一个对象放入集合后,如果用于哈希的字段发生改变,集合可能再也找不到它。
class Coordinate:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
if not isinstance(other, Coordinate):
return NotImplemented
return self.x == other.x and self.y == other.y
def __hash__(self):
return hash((self.x, self.y))这个类虽然实现了哈希,但 x 和 y 仍可修改,所以设计并不安全。真实值对象还应阻止参与哈希的字段变化,或直接使用冻结的数据类等不变结构。
当类实现 __eq__ 却不提供适当的 __hash__ 时,Python 通常会让实例不可哈希,从而避免违反上述约定。不要为了消除 TypeError 而随意恢复身份哈希。
4. 容器和位置访问协议
这一组方法决定对象如何表现为容器。
长度和下标
len(obj) -> __len__
obj[key] -> __getitem__
obj[key] = val -> __setitem__
del obj[key] -> __delitem__只实现 __getitem__ 可以提供读取能力;加入 __setitem__ 和 __delitem__ 后才形成相应的修改与删除能力。
这些方法的 key 不一定是整数:
- 序列通常接收整数和
slice; - 映射可以接收可哈希键;
- 领域对象可以设计其他明确的键类型。
无效序列位置通常抛出 IndexError,缺失映射键通常抛出 KeyError。标准异常是协议的一部分,会影响调用者和回退机制如何处理结果。
成员判断
item in obj -> __contains__
item not in obj -> 对成员判断结果取反没有 __contains__ 时,Python 可以回退到迭代,再在必要时回退到 __getitem__ 序列协议。
专用实现适合利用哈希索引、数据库索引或领域查询规则,避免逐项扫描。
缺失键处理
字典子类可以实现 __missing__(self, key)。当 dict.__getitem__ 找不到键时,它会调用该方法:
class ZeroDict(dict):
def __missing__(self, key):
return 0
counts = ZeroDict({"ok": 3})
print(counts["ok"])
print(counts["missing"])3
0__missing__ 是 dict 子类的特定扩展点,不是所有映射对象都会自动使用的通用特殊方法。自定义 Mapping 若想提供类似行为,应在自己的 __getitem__ 中明确实现。
5. 同步迭代协议
同步迭代涉及三个主要方法。
__iter__
iter(obj) 和 for item in obj 会请求迭代器。可迭代容器的 __iter__ 通常返回一个新的迭代器:
def __iter__(self):
return iter(self._items)__next__
迭代器通过 next(iterator) 返回下一项,耗尽时抛出 StopIteration。
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current == 0:
raise StopIteration
value = self.current
self.current -= 1
return value
print(list(Countdown(3)))[3, 2, 1]这个对象自身就是一次性迭代器。遍历结束后,它不能自动从头再来。普通容器通常不把自己同时设计成迭代器,而是每次返回独立迭代器。
__reversed__
reversed(obj) 优先使用 __reversed__。没有时,可以在对象支持长度和整数索引的情况下回退到序列协议。
专用 __reversed__ 适合不能高效随机访问、但能够自然反向遍历的数据结构。
6. 异步迭代和等待协议
异步协议与普通迭代相似,但每一步可以等待异步操作完成。
__aiter__ 与 __anext__
公开语法是:
async for item in async_iterable:
...异步可迭代对象通过 __aiter__ 返回异步迭代器。异步迭代器的 __anext__ 返回可等待对象,耗尽时抛出 StopAsyncIteration。
下面的示例不依赖网络,只用 asyncio.sleep(0) 模拟一次让出执行权:
import asyncio
class AsyncCountdown:
def __init__(self, start):
self.current = start
def __aiter__(self):
return self
async def __anext__(self):
if self.current == 0:
raise StopAsyncIteration
await asyncio.sleep(0)
value = self.current
self.current -= 1
return value
async def main():
async for value in AsyncCountdown(3):
print(value)
asyncio.run(main())3
2
1__await__
await obj 依赖对象的等待协议。普通应用代码通常通过 async def 创建协程,或使用异步库提供的任务和 Future,而不是手写 __await__。
只有在设计异步框架、任务对象或类似 Future 的基础设施时,才通常需要直接实现它。
7. 可调用对象
实现 __call__ 后,实例可以像函数一样调用:
class Prefixer:
def __init__(self, prefix):
self.prefix = prefix
def __call__(self, value):
return f"{self.prefix}{value}"
add_error = Prefixer("ERROR: ")
print(add_error("connection failed"))ERROR: connection failed可调用对象适合“函数行为 + 可配置状态”的组合,例如:
- 保存参数的校验器;
- 带缓存的计算器;
- 可配置的转换器;
- 框架中的处理器对象。
如果对象没有需要保存的状态,普通函数通常更简单。
8. 上下文管理协议
上下文管理器负责围绕一段代码成对执行进入和退出逻辑。
同步上下文管理
公开语法是:
with resource as value:
...对应方法是:
__enter__:进入上下文,返回绑定给as后变量的值;__exit__:退出上下文,接收异常类型、异常对象和回溯对象。
class ManagedConnection:
def __enter__(self):
print("open")
return self
def execute(self, command):
print(f"execute: {command}")
def __exit__(self, exc_type, exc_value, traceback):
print("close")
return False
with ManagedConnection() as connection:
connection.execute("SELECT 1")open
execute: SELECT 1
close即使代码块抛出异常,__exit__ 通常仍会执行。返回真值表示异常已经被处理,不再向外传播;返回假值或 None 表示继续传播。
不要无条件返回 True,否则程序可能悄悄吞掉本应暴露的错误。
异步上下文管理
异步资源使用:
async with resource:
...对应 __aenter__ 和 __aexit__。它们适合连接建立、关闭等需要等待的操作。普通同步 with 不会自动等待协程。
9. 实例创建、初始化与销毁
这一组方法控制对象的生命周期。
__new__
__new__ 负责创建并返回实例。它接收类作为第一个参数,常见于:
- 自定义不可变类型;
- 控制实例创建;
- 元类和底层框架。
普通可变类通常不需要覆盖它。
__init__
__init__ 在实例创建后初始化对象状态:
class User:
def __init__(self, name):
self.name = name它必须返回 None。它不是创建对象的构造函数本体,而是创建流程中的初始化阶段。
概念过程是:
User("Lin")
-> User.__new__(User, "Lin") 创建实例
-> 如果返回的是 User 实例,再调用 __init__(instance, "Lin")
-> 返回实例__del__
__del__ 在对象被回收前可能被调用,但不适合承担可靠的资源释放:
- 调用时机依赖实现和引用关系;
- 解释器退出时环境可能已经部分销毁;
- 循环引用和异常会让行为更复杂;
- 程序崩溃时不能保证执行。
文件、锁、连接等需要确定释放的资源,应优先使用 with 上下文管理器或显式关闭方法。
10. 属性访问协议
属性协议可以拦截或定制 obj.name 的读取、赋值和删除。
__getattribute__
几乎所有实例属性读取都会经过它:
obj.name -> obj.__getattribute__("name")覆盖时必须非常谨慎,通常应调用 object.__getattribute__ 获取真实属性,否则容易无限递归:
class Traced:
def __getattribute__(self, name):
print(f"read {name}")
return object.__getattribute__(self, name)__getattr__
只有普通属性查找失败后,Python 才调用 __getattr__:
class Settings:
def __init__(self, values):
self._values = dict(values)
def __getattr__(self, name):
try:
return self._values[name]
except KeyError as error:
raise AttributeError(name) from error缺失属性必须以 AttributeError 表示。hasattr()、getattr() 默认值和许多框架都依赖这一约定。
__setattr__ 与 __delattr__
它们分别拦截:
obj.name = value
del obj.name覆盖 __setattr__ 时,内部赋值如果再次写 self.name = value,会递归调用自己。通常需要委托:
object.__setattr__(self, name, value)__dir__
dir(obj) 可以通过 __dir__ 定制可发现的属性列表。动态代理对象有时会使用它改善交互式补全。
属性协议会影响对象最基础的访问行为。除代理、ORM、延迟加载和框架基础设施等明确需求外,普通业务类通常不需要覆盖 __getattribute__。
11. 描述符协议
描述符把属性访问逻辑放在类属性对象中,核心方法包括:
__get__:读取属性;__set__:设置属性;__delete__:删除属性;__set_name__:类创建时通知描述符其属性名称。
property、普通方法、classmethod 和许多 ORM 字段都建立在描述符机制上。
下面实现一个只接受正数的字段:
class Positive:
def __set_name__(self, owner, name):
self.storage_name = f"_{name}"
def __get__(self, instance, owner):
if instance is None:
return self
return getattr(instance, self.storage_name)
def __set__(self, instance, value):
if value <= 0:
raise ValueError("value must be positive")
setattr(instance, self.storage_name, value)
class Product:
price = Positive()
def __init__(self, price):
self.price = price
product = Product(19.9)
print(product.price)19.9描述符适合把一套属性规则复用于多个类和字段。只有单个属性需要简单校验时,property 往往更容易维护。
12. 实例和子类检查
通常使用:
isinstance(obj, SomeType)
issubclass(Child, Parent)在元类层面,__instancecheck__ 和 __subclasscheck__ 可以定制这些检查。collections.abc 的虚拟子类和结构化识别就利用了相关机制。
普通业务类很少需要直接实现它们。滥用会让类型关系与继承树不一致,增加调试难度。
13. 类创建和类级协议
Python 还提供多种类创建钩子。
__init_subclass__
每当类被继承时,父类可以验证或配置新子类:
class Handler:
def __init_subclass__(cls, *, event, **kwargs):
super().__init_subclass__(**kwargs)
cls.event = event
class CreatedHandler(Handler, event="created"):
pass
print(CreatedHandler.event)created它适合轻量的插件注册或子类约束,通常比自定义元类更容易理解。
__class_getitem__
类级下标语法:
Container[Item]可以由 __class_getitem__ 定制。现代 Python 的泛型类型标注广泛使用该协议。普通业务类如果没有泛型或类级参数化需求,不应把它当作实例下标的替代品。
元类相关钩子
__prepare__、元类的 __new__ 和 __init__ 可以控制类命名空间及类对象的创建。__mro_entries__ 可以参与基类解析。
这些机制主要服务于框架、ORM、声明式 API 和高级类型系统。解决普通对象行为时,应优先考虑类装饰器、__init_subclass__、描述符或简单组合。
14. 一元运算符
一元运算只涉及一个主要操作数。
常见对应关系是:
-obj使用__neg__;+obj使用__pos__;abs(obj)使用__abs__;~obj使用__invert__。
一元 + 不代表“什么都不做”。自定义数值类型可以通过 __pos__ 进行规范化或返回副本,但这种语义必须与领域直觉一致。
下面的计数对象支持取负:
class Count:
def __init__(self, value):
self.value = value
def __repr__(self):
return f"Count({self.value})"
def __neg__(self):
return Count(-self.value)
count = Count(5)
print(-count)
print(count)Count(-5)
Count(5)取负返回新对象,没有修改原对象。
15. 比较运算符
富比较方法包括:
<对应__lt__;<=对应__le__;==对应__eq__;!=对应__ne__;>对应__gt__;>=对应__ge__。
对于不支持的类型,比较方法应返回 NotImplemented,让解释器尝试另一侧的配对操作。
配对关系是:
left < right 可以尝试 right > left
left <= right 可以尝试 right >= left
left == right 可以尝试 right == leftPython 不会根据 __lt__ 自动推导全部其他排序关系。可以根据真实语义实现所需方法,或使用 functools.total_ordering 从 __eq__ 和一个排序方法生成其余方法,但生成方法可能更慢,堆栈也更复杂。
如果双方的相等性方法都不支持该比较,== 和 != 最终有基于对象身份的默认结果。排序比较没有类似的通用默认顺序,通常会抛出 TypeError。
不要用 id() 或对象地址人为制造业务对象的大小顺序,除非身份顺序本身确实有领域意义。
16. 正向二元运算方法
算术和位运算的正向方法由左操作数负责。常见方法包括:
__add__:+;__sub__:-;__mul__:*;__matmul__:@;__truediv__:/;__floordiv__://;__mod__:%;__divmod__:divmod();__pow__:**或pow();__lshift__、__rshift__:<<、>>;__and__、__xor__、__or__:&、^、|。
注意,and 和 or 是短路布尔运算,不能通过 __and__ 和 __or__ 重载。后两者对应的是按位运算符 & 和 |。
@ 最初为矩阵乘法等领域引入,但对象可以根据清晰的领域语义实现它。标准 Python 本身不为普通列表定义矩阵乘法。
pow(base, exponent, modulus) 的三参数形式也会进入幂运算协议。若类型支持取模幂,应正确处理第三个参数,而不是假设 __pow__ 永远只有一个显式操作数。
17. 反向二元运算方法
每个主要二元运算通常还有 r 前缀的反向版本,例如:
__radd__
__rsub__
__rmul__
__rmatmul__
__rtruediv__
__rfloordiv__
__rmod__
__rpow__
__rand__
__rxor__
__ror__对于:
left + right可以先用下面的简化流程理解:
尝试 type(left).__add__(left, right)
-> 如果能够处理,返回结果
-> 如果返回 NotImplemented,考虑 type(right).__radd__(right, left)
-> 双方都不支持,抛出 TypeError实际优先级还会考虑左右类型是否相同,以及右侧类型是否是左侧类型的更具体子类。
反向方法不是简单交换参数名。对于乘法这类可能满足交换律的操作,可以复用正向实现:
def __rmul__(self, scalar):
return self * scalar但减法和除法不满足交换律:
10 - obj 与 obj - 10 含义不同
10 / obj 与 obj / 10 含义不同它们需要独立的反向逻辑。
18. 增量赋值方法
增量赋值对应 i 前缀方法:
__iadd__ +=
__isub__ -=
__imul__ *=
__imatmul__ @=
__itruediv__ /=
__ifloordiv__ //=
__imod__ %=
__ipow__ **=
__iand__ &=
__ixor__ ^=
__ior__ |=如果 __iadd__ 不存在或返回 NotImplemented,a += b 通常会回退到普通加法,再把结果重新绑定给 a:
a += b
大致回退为:a = a + b对不可变对象,这种返回新对象的行为很自然。可变对象可以让 __iadd__ 修改自身并返回 self,但必须让调用者能够预料这种可变语义。
下面对比列表和元组:
items = [1, 2]
original_items = items
items += [3]
print(items is original_items)
values = (1, 2)
original_values = values
values += (3,)
print(values is original_values)True
False列表的 += 原地扩展现有对象;元组不可变,只能生成新元组并重新绑定变量。
19. 运算符方法的实现原则
设计运算符时,应遵守几条原则。
语义应当自然
Vector + Vector 容易理解;User + DatabaseConnection 通常没有稳定直觉。不要为了展示语言能力而给对象添加令人意外的运算符。
不支持的类型返回 NotImplemented
二元运算和比较方法遇到不支持的类型时,优先返回 NotImplemented,让解释器尝试反向方法或生成合适的 TypeError。
普通运算通常返回新对象
+、-、* 等普通运算一般不修改参与运算的对象。原地修改应明确放在 += 等协议中,并符合类型的可变性设计。
保持相关协议一致
- 相等对象必须具有相同哈希;
- 比较关系应保持传递性和反对称性;
- 正向和反向操作应表达正确的操作数顺序;
- 原地运算不应无意间改变共享对象;
- 返回类型应让连续运算保持可理解。
20. 如何选择真正需要的方法
面对大量特殊方法,最稳妥的策略不是尽可能多地实现,而是从对象职责反推最小协议。
如果对象要表现为只读序列,可以从这些问题开始:
是否需要长度? -> __len__
是否需要按位置读取? -> __getitem__
是否需要高效迭代? -> __iter__
是否需要专用成员判断? -> __contains__
是否承诺完整序列语义? -> 考虑继承 Sequence如果对象要表现为数值,可以继续问:
哪些运算具有自然含义?
是否支持不同操作数类型?
是否需要反向运算?
对象是可变值还是不可变值?
相等性和哈希是否一致?如果对象管理资源:
资源是否需要确定释放?
进入和退出是否可能异步?
异常是否应该继续传播?这样得到的方法集合通常比照着清单逐项实现更小,也更符合调用者预期。
21. 一个组合多项协议的完整示例
下面的 Registry 是一个只读、可调用的名称注册表。它组合了对象表示、容器和调用协议:
from collections.abc import Mapping
class Registry(Mapping):
def __init__(self, entries):
self._entries = dict(entries)
def __getitem__(self, name):
return self._entries[name]
def __iter__(self):
return iter(self._entries)
def __len__(self):
return len(self._entries)
def __call__(self, name):
return self[name]
def __repr__(self):
names = list(self._entries)
return f"Registry(names={names!r})"
if __name__ == "__main__":
registry = Registry({
"double": lambda value: value * 2,
"square": lambda value: value ** 2,
})
print(registry)
print(len(registry))
print(list(registry))
print("square" in registry)
print(registry["double"](5))
print(registry("square")(4))输出为:
Registry(names=['double', 'square'])
2
['double', 'square']
True
10
16这段代码只实现确实属于注册表的语义:
- 继承
Mapping,承诺只读映射行为; __getitem__、__iter__和__len__提供映射核心;__call__把注册表本身作为按名称解析对象的快捷入口;__repr__展示已注册名称,但不展开难以阅读的函数对象。
它没有实现加法、排序或上下文管理,因为这些能力对当前对象没有自然意义。
22. 本节小结
特殊方法覆盖了 Python 对象的主要行为边界:
__repr__、__str__、__format__和__bytes__负责表示与转换;__bool__、__index__、数值转换和__hash__让对象接入基础值协议;- 容器方法负责长度、下标、修改、删除和成员判断;
- 同步与异步迭代协议分别服务
for和async for; __call__让带状态对象表现为可调用对象;- 上下文协议为资源建立可靠的进入和退出边界;
__new__、__init__和__del__参与对象生命周期;- 属性和描述符协议控制属性访问;
- 类创建钩子服务于框架和声明式 API;
- 运算符方法分为一元、正向、反向和增量赋值几类;
- 对不支持的二元运算返回
NotImplemented,不要用NotImplementedError代替; - 方法的选择应由对象语义驱动,而不是由特殊方法清单驱动。
下一节将集中回答本章开头留下的问题:既然 Python 是面向对象语言,为什么获取长度采用 len(obj),而不是统一采用 obj.len()?
拓展阅读与查阅
以下资料用于进一步研究或日后查阅。理解本节内容不依赖这些链接。
