Python 基础体系 · 第 28/112 篇。示例统一以 Python 3.14 为语言基线;第三方库使用与其兼容的现代稳定版本,版本敏感行为会单独说明。
Python 特殊方法:容器、运算符、调用、表示与上下文协议
Python 对象的许多语言级行为,并不是由某个普通函数直接实现的,而是由对象类型上预留的一组特殊方法参与完成。例如:
items[0]
len(items)
left + right
callable_object(1, 2)
repr(value)
with resource:
...
这些语法背后分别可能触发 __getitem__()、__len__()、__add__()、__call__()、__repr__()、__enter__() 和 __exit__()。
特殊方法通常写成双下划线包围的名称,也常被称为 dunder method。它们不是“语法糖形式的普通方法”这么简单:解释器会针对不同操作,按照特定规则从对象的类型、基类和相关协议中查找并调用它们。因此,理解特殊方法,必须同时理解三个层次:
- 语法层:某种表达式或语句表示什么操作;
- 协议层:该操作要求对象提供哪些方法;
- 类型查找层:解释器从哪里查找这些方法,以及失败时如何处理。
Python 3.14 的语言参考将对象定义为具有身份、类型和值的实体;对象的类型决定它支持哪些操作。特殊方法正是类型把这些操作暴露给语言运行时的主要方式。(docs.python.org)
一、特殊方法的本质:类型协议,而不是实例属性
先看一个最容易产生误解的例子:
class Box:
pass
box = Box()
box.__len__ = lambda: 3
print(box.__len__()) # 3
print(len(box)) # TypeError
显式访问 box.__len__() 能够成功,因为这只是一次普通属性查找:解释器在实例字典中找到了 __len__。
但 len(box) 并不会简单地执行:
box.__len__()
对于自定义类,隐式特殊方法调用通常要求特殊方法定义在对象的类型上,而不是定义在实例字典中。解释器会绕过实例属性查找,直接从类型及其继承体系中寻找对应的特殊方法。这样既保证了行为一致,也避免了对 int、str 等类型对象进行隐式调用时出现绑定歧义。(docs.python.org)
正确写法是:
class Box:
def __len__(self):
return 3
box = Box()
print(len(box)) # 3
可以把下面两种形式区分开:
box.__len__() # 显式属性访问,遵循普通属性查找
type(box).__len__(box) # 显式从类型获取并传入实例
len(box) # 隐式特殊方法查找
len(box) 在概念上接近:
type(box).__len__(box)
但“接近”不等于语言规范要求解释器真的执行这段 Python 代码。实际实现会使用类型槽位,例如 tp_as_sequence->sq_length 或其他内部结构。类型槽位是 CPython 的实现机制;从 Python 代码角度,应该依赖语言定义的协议,而不是依赖某个具体 C 字段。
1. 特殊方法设置为 None 的含义
如果一个特殊方法在类型上被设置为 None,通常表示该协议被明确禁用:
class NotIterable:
__iter__ = None
value = NotIterable()
iter(value) # TypeError
这与“没有定义 __iter__”不同。某些协议存在后备机制,例如可迭代对象可能通过 __getitem__() 提供旧式迭代;但将 __iter__ 设置为 None 会明确阻止这类后备。(docs.python.org)
2. 特殊方法仍然可以被显式调用
特殊方法不是私有方法:
class Counter:
def __len__(self):
return 2
counter = Counter()
print(counter.__len__()) # 2
print(type(counter).__len__(counter)) # 2
不过,工程代码通常应优先使用对应的语言操作:
len(counter)
原因有两个:
- 语言操作会使用完整的协议规则;
- 直接调用特殊方法可能绕过运算符的反射、后备和结果校验逻辑。
二、容器协议:从 len()、索引到迭代
“容器”不是某个单一的基类,而是一组对象行为的统称。一个对象是否像容器,取决于它支持哪些操作:
- 是否有大小:
len(obj); - 是否支持索引或键访问:
obj[key]; - 是否支持赋值和删除:
obj[key] = value、del obj[key]; - 是否支持成员检测:
item in obj; - 是否可以迭代:
for item in obj; - 是否支持反向迭代:
reversed(obj); - 是否具有布尔真值:
bool(obj)。
序列通常使用非负整数索引,映射则使用任意可哈希键。Python 数据模型将序列、集合和映射视为不同的对象类别;例如,序列有位置顺序,映射通过键选择值,集合通常不提供索引。(docs.python.org)
1. 最小可用容器
下面实现一个只读序列:
class Vector:
def __init__(self, values):
self._values = tuple(values)
def __len__(self):
return len(self._values)
def __getitem__(self, index):
return self._values[index]
def __repr__(self):
return f"Vector({self._values!r})"
vector = Vector([10, 20, 30])
print(len(vector)) # 3
print(vector[1]) # 20
print(vector[-1]) # 30
print(vector[1:]) # (20, 30)
print(vector) # Vector((10, 20, 30))
这里的调用关系如下:
| 表达式 | 主要特殊方法 |
|---|---|
len(vector) |
vector.__len__() |
vector[index] |
vector.__getitem__(index) |
vector[start:stop] |
vector.__getitem__(slice(start, stop)) |
repr(vector) |
vector.__repr__() |
切片并不是传入三个普通参数,而是先构造一个 slice 对象:
index = slice(1, None)
print(index.start) # 1
print(index.stop) # None
print(index.step) # None
因此,__getitem__() 往往需要区分整数索引和切片:
class Vector:
def __init__(self, values):
self._values = tuple(values)
def __getitem__(self, key):
if isinstance(key, slice):
return type(self)(self._values[key])
if isinstance(key, int):
return self._values[key]
raise TypeError(f"索引必须是 int 或 slice,而不是 {type(key).__name__}")
def __len__(self):
return len(self._values)
def __repr__(self):
return f"Vector({self._values!r})"
print(Vector([1, 2, 3, 4])[1:3]) # Vector((2, 3))
关键点是:切片操作的语义由 slice 对象表达,是否返回同类对象则由你的协议设计决定。
2. 可变序列:赋值与删除
如果要支持:
items[index] = value
del items[index]
需要实现:
__setitem__();__delitem__()。
class MutableVector:
def __init__(self, values=()):
self._values = list(values)
def __len__(self):
return len(self._values)
def __getitem__(self, key):
return self._values[key]
def __setitem__(self, key, value):
self._values[key] = value
def __delitem__(self, key):
del self._values[key]
def __repr__(self):
return f"MutableVector({self._values!r})"
vector = MutableVector([10, 20, 30, 40])
vector[1] = 200
print(vector) # MutableVector([10, 200, 30, 40])
del vector[2]
print(vector) # MutableVector([10, 200, 40])
对于切片赋值,底层 list 会执行长度检查。例如:
vector[1:3] = [7, 8, 9]
是否允许替换为不同长度的数据,取决于你的类是否接受这种序列语义。如果只是简单转发给内部列表,通常会允许;如果对象要求固定长度,就必须显式校验。
3. 映射协议
映射通过键访问值:
class Config:
def __init__(self, values=None):
self._values = dict(values or {})
def __getitem__(self, key):
return self._values[key]
def __setitem__(self, key, value):
self._values[key] = value
def __delitem__(self, key):
del self._values[key]
def __len__(self):
return len(self._values)
def __iter__(self):
return iter(self._values)
def __repr__(self):
return f"Config({self._values!r})"
config = Config({"host": "localhost"})
print(config["host"]) # localhost
config["port"] = 5432
print(list(config)) # ['host', 'port']
del config["port"]
print(config) # Config({'host': 'localhost'})
这里 __iter__() 返回的是字典键的迭代器,因此:
list(config)
得到的是键列表,而不是键值对。这是映射对象的常见语义,但不是所有自定义容器都必须如此;关键在于 __iter__() 的约定必须稳定、可预测。
4. __contains__() 与成员检测
表达式:
item in container
优先使用 __contains__():
class Permissions:
def __init__(self, names):
self._names = set(names)
def __contains__(self, name):
return name in self._names
permissions = Permissions({"read", "write"})
print("read" in permissions) # True
print("admin" in permissions) # False
如果没有 __contains__(),Python 可能通过迭代对象检查成员;如果连迭代也不可用,还可能尝试基于从 0 开始的 __getitem__() 进行旧式迭代。因此,想要明确、高效地定义成员检测,应直接实现 __contains__()。
一个重要边界是:成员检测返回值应当能够转换为布尔值。如果返回一个不合理对象,错误可能延迟到条件判断阶段,而不是发生在 in 表达式内部。
5. __iter__()、__next__() 与迭代器状态
可迭代对象和迭代器不是同一个概念。
- 可迭代对象:可以通过
iter(obj)获得迭代器; - 迭代器:自身实现
__next__(),并且通常让iter(iterator)返回自身。
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
counter = Countdown(3)
print(next(counter)) # 3
print(next(counter)) # 2
print(next(counter)) # 1
try:
next(counter)
except StopIteration:
print("iteration finished") # iteration finished
状态变化是:
初始:current = 3
第一次 next:返回 3,current = 2
第二次 next:返回 2,current = 1
第三次 next:返回 1,current = 0
第四次 next:抛出 StopIteration
for 循环大致执行以下过程:
iterator = iter(counter)
while True:
try:
item = next(iterator)
except StopIteration:
break
else:
print(item)
因此,StopIteration 不是普通的业务错误,而是迭代结束信号。若在生成器内部直接抛出 StopIteration,还会受到生成器语义的额外约束;通常应使用 return 表示生成器结束。
更常见的设计是让容器每次生成一个新的迭代器:
class Numbers:
def __init__(self, values):
self._values = tuple(values)
def __iter__(self):
return iter(self._values)
numbers = Numbers([1, 2, 3])
print(list(numbers)) # [1, 2, 3]
print(list(numbers)) # [1, 2, 3]
如果把容器本身同时设计成迭代器,那么它通常是一次性的:
class OneShot:
def __init__(self, values):
self._values = iter(values)
def __iter__(self):
return self
def __next__(self):
return next(self._values)
value = OneShot([1, 2, 3])
print(list(value)) # [1, 2, 3]
print(list(value)) # []
这不是错误,而是对象状态模型不同。容器通常可重复迭代;迭代器通常保存当前位置并逐步消耗。
6. __reversed__() 与反向遍历
reversed(obj) 的优先协议是 __reversed__():
class Log:
def __init__(self, records):
self.records = list(records)
def __reversed__(self):
print("custom reverse")
return iter(self.records[::-1])
log = Log(["a", "b", "c"])
print(list(reversed(log)))
# custom reverse
# ['c', 'b', 'a']
如果没有 __reversed__(),Python 可能根据 __len__() 和整数索引执行反向访问。因此,一个支持索引的序列往往也能被 reversed() 使用,但自定义 __reversed__() 可以提供不同的存储策略或更高效的实现。
三、长度、真值和布尔上下文
Python 在下列位置需要判断对象真假:
if value:
...
while value:
...
not value
bool(value)
对象的真值判断主要遵循:
- 如果类型定义了
__bool__(),调用它; - 否则如果定义了
__len__(),长度为零表示假,非零表示真; - 否则对象为真。
class QueryResult:
def __init__(self, rows):
self.rows = list(rows)
def __len__(self):
return len(self.rows)
result = QueryResult([])
print(bool(result)) # False
实现 __bool__() 时,返回值必须是布尔值:
class ExplicitState:
def __bool__(self):
return True
错误示例:
class InvalidState:
def __bool__(self):
return 1 # TypeError: __bool__ should return bool
当对象同时定义 __bool__() 和 __len__() 时,__bool__() 优先:
class Collection:
def __init__(self, values):
self.values = list(values)
def __len__(self):
return len(self.values)
def __bool__(self):
return False
value = Collection([1, 2, 3])
print(len(value)) # 3
print(bool(value)) # False
这说明“非空”和“逻辑上有效”不是同一个概念。一个查询结果对象可能有缓存记录,但仍然因为连接失效、状态未确认等原因被设计为假。是否这样设计,应当基于领域语义,而不是为了少写一个判断。
长度返回值的限制
__len__() 必须返回非负整数:
class InvalidLength:
def __len__(self):
return -1
len(InvalidLength()) # ValueError
__index__() 与 __int__() 也不能混为一谈。需要“整数索引语义”的场景,例如序列下标、部分内置函数参数,通常要求对象实现 __index__(),而不是仅仅实现 __int__()。
四、运算符协议:正向、反向与原地操作
运算符重载不是把符号变成任意函数,而是让对象参与 Python 规定的二元操作协议。
常见对应关系如下:
| 运算符 | 正向方法 | 反向方法 | 原地方法 |
|---|---|---|---|
+ |
__add__ |
__radd__ |
__iadd__ |
- |
__sub__ |
__rsub__ |
__isub__ |
* |
__mul__ |
__rmul__ |
__imul__ |
/ |
__truediv__ |
__rtruediv__ |
__itruediv__ |
// |
__floordiv__ |
__rfloordiv__ |
__ifloordiv__ |
% |
__mod__ |
__rmod__ |
__imod__ |
** |
__pow__ |
__rpow__ |
__ipow__ |
@ |
__matmul__ |
__rmatmul__ |
__imatmul__ |
& |
__and__ |
__rand__ |
__iand__ |
| ` | ` | __or__ |
__ror__ |
^ |
__xor__ |
__rxor__ |
__ixor__ |
<< |
__lshift__ |
__rlshift__ |
__ilshift__ |
>> |
__rshift__ |
__rrshift__ |
__irshift__ |
一元运算符对应:
| 运算符 | 方法 |
|---|---|
+x |
__pos__ |
-x |
__neg__ |
~x |
__invert__ |
1. NotImplemented 的作用
当一个操作数不知道如何处理另一个类型时,特殊方法应返回 NotImplemented,而不是直接返回 False,也不是随意抛出 TypeError。
class Money:
def __init__(self, amount, currency):
self.amount = amount
self.currency = currency
def __add__(self, other):
if not isinstance(other, Money):
return NotImplemented
if self.currency != other.currency:
raise ValueError("货币单位不同")
return Money(self.amount + other.amount, self.currency)
def __repr__(self):
return f"Money({self.amount!r}, {self.currency!r})"
print(Money(10, "CNY") + Money(5, "CNY"))
# Money(15, 'CNY')
当左操作数的 __add__() 返回 NotImplemented 时,Python 可能尝试右操作数的 __radd__(),最后才决定是否报错。Python 3.14 中,NotImplemented 不能再直接用于布尔判断;对它执行 bool(NotImplemented) 会抛出 TypeError。(docs.python.org)
错误写法:
def __add__(self, other):
result = self._try_add(other)
if not result:
return NotImplemented
return result
如果 result 恰好就是 NotImplemented,这里会触发错误。应使用身份判断:
if result is NotImplemented:
...
2. 反向运算不是简单的“交换参数”
看下面的类型:
class Celsius:
def __init__(self, value):
self.value = value
def __radd__(self, other):
if isinstance(other, (int, float)):
return Celsius(other + self.value)
return NotImplemented
def __repr__(self):
return f"Celsius({self.value})"
print(10 + Celsius(5)) # Celsius(15)
表达式:
10 + Celsius(5)
首先尝试整数的加法逻辑。如果整数无法处理 Celsius,Python 再尝试:
Celsius(5).__radd__(10)
对于有继承关系的类型,右侧类型可能因为更具体而获得优先机会。不能简单假设执行顺序永远是“左边先、右边后”,也不能把 __rsub__() 实现成普通的 __sub__(),因为减法参数顺序不同:
a - b
右操作数的反向方法应表达:
b.__rsub__(a)
其数学含义通常是 a - b,而不是 b - a。
3. 原地运算不一定修改原对象
表达式:
x += y
会优先尝试 x.__iadd__(y)。如果没有可用的原地方法,通常会退回到普通加法,再把结果重新绑定给 x:
x = x + y
可变对象可以原地修改:
class MutableNumber:
def __init__(self, value):
self.value = value
def __iadd__(self, other):
self.value += other
return self
def __repr__(self):
return f"MutableNumber({self.value})"
x = MutableNumber(10)
alias = x
x += 5
print(x) # MutableNumber(15)
print(alias) # MutableNumber(15)
print(x is alias) # True
不可变对象则不能真正修改自身:
class ImmutableNumber:
def __init__(self, value):
self.value = value
def __add__(self, other):
return type(self)(self.value + other)
def __repr__(self):
return f"ImmutableNumber({self.value})"
x = ImmutableNumber(10)
alias = x
x += 5
print(x) # ImmutableNumber(15)
print(alias) # ImmutableNumber(10)
print(x is alias) # False
所以 += 的“原地”是协议优先级,不是对象必然原地变化的保证。
4. 比较运算与哈希的一致性
丰富比较方法包括:
__lt__ # <
__le__ # <=
__eq__ # ==
__ne__ # !=
__gt__ # >
__ge__ # >=
比较方法也可以返回 NotImplemented:
class Version:
def __init__(self, major, minor):
self.major = major
self.minor = minor
def __eq__(self, other):
if not isinstance(other, Version):
return NotImplemented
return (self.major, self.minor) == (other.major, other.minor)
def __lt__(self, other):
if not isinstance(other, Version):
return NotImplemented
return (self.major, self.minor) < (other.major, other.minor)
def __repr__(self):
return f"Version({self.major}, {self.minor})"
print(Version(3, 14) < Version(3, 15)) # True
如果对象实现了值相等关系并且希望作为字典键,必须同时考虑哈希一致性:
a == b 为真
则必须满足 hash(a) == hash(b)
反过来,哈希相同并不意味着对象相等,因为哈希碰撞是允许的。
例如:
class UserId:
def __init__(self, value):
self.value = value
def __eq__(self, other):
if not isinstance(other, UserId):
return NotImplemented
return self.value == other.value
def __hash__(self):
return hash(self.value)
如果定义了 __eq__() 却不定义兼容的 __hash__(),Python 通常会将该类设为不可哈希,以避免可变或不一致的对象进入字典和集合。
更严重的错误是对象进入集合后,参与哈希的状态发生变化:
class BadKey:
def __init__(self, value):
self.value = value
def __hash__(self):
return hash(self.value)
def __eq__(self, other):
return isinstance(other, BadKey) and self.value == other.value
key = BadKey("a")
mapping = {key: "value"}
key.value = "b"
print(mapping.get(key)) # 可能得到 None
字典内部仍然保存着这个对象,但它现在按照新的哈希值查找,无法定位原来的槽位。因此,作为字典键的对象必须保证哈希相关状态在生命周期内稳定。
五、表示协议:repr、str、格式化与字节转换
对象的“表示”至少涉及四个常用协议:
__repr__():面向开发者、调试器和容器显示;__str__():面向用户的自然语言表示;__format__():处理format(obj, spec)和格式化字符串;__bytes__():处理bytes(obj)。
1. __repr__() 与 __str__() 的区别
class User:
def __init__(self, name, active):
self.name = name
self.active = active
def __repr__(self):
return f"User(name={self.name!r}, active={self.active!r})"
def __str__(self):
status = "active" if self.active else "inactive"
return f"{self.name} ({status})"
user = User("alice", True)
print(repr(user)) # User(name='alice', active=True)
print(str(user)) # alice (active)
print(user) # alice (active)
print(user) 使用 str(user);如果没有定义 __str__(),通常会退回到 __repr__()。
在容器中,元素通常使用 repr():
print([user])
# [User(name='alice', active=True)]
因此,__repr__() 应尽量:
- 包含足够的类型和状态信息;
- 对字符串、路径等值使用
!r,保留边界; - 不执行网络请求、数据库查询等副作用操作;
- 不依赖可能失效的外部资源。
2. repr() 不保证一定可执行
很多内置类型的表示形式接近可重新构造的 Python 表达式:
value = (1, "x")
print(repr(value)) # (1, 'x')
但对自定义类型而言,“repr(x) 可执行并恢复 x”只是有用的设计目标,不是所有对象的语言级强制要求。包含文件句柄、线程锁、数据库连接的对象通常无法通过一段字面量重建。
3. 格式化协议
表达式:
format(value, spec)
f"{value:spec}"
会使用 __format__():
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
def __format__(self, spec):
if spec == "c":
return f"{self.celsius:.1f} °C"
if spec == "f":
fahrenheit = self.celsius * 9 / 5 + 32
return f"{fahrenheit:.1f} °F"
if spec == "":
return str(self.celsius)
raise ValueError(f"不支持的格式说明符: {spec!r}")
def __repr__(self):
return f"Temperature({self.celsius!r})"
temperature = Temperature(25)
print(f"{temperature:c}") # 25.0 °C
print(f"{temperature:f}") # 77.0 °F
格式说明符的语法由对象定义。__format__() 不应默默接受拼写错误的格式,否则调用方难以发现输出已经偏离预期。
4. 字节表示
如果对象有明确的二进制编码,可以实现 __bytes__():
class Packet:
def __init__(self, version, payload):
self.version = version
self.payload = bytes(payload)
def __bytes__(self):
if not 0 <= self.version <= 255:
raise ValueError("version 必须在 0 到 255 之间")
return bytes([self.version]) + self.payload
packet = Packet(1, b"OK")
print(bytes(packet)) # b'\x01OK'
这里需要明确字节格式:
第 1 字节:版本号
后续字节:负载
如果协议涉及长度、校验和、字节序或字符编码,应在 __bytes__() 中明确处理,而不是依赖调用方猜测。
六、调用协议:让实例像函数一样工作
实现 __call__() 后,实例可以使用函数调用语法:
class Multiplier:
def __init__(self, factor):
self.factor = factor
def __call__(self, value):
return value * self.factor
double = Multiplier(2)
print(double(5)) # 10
print(callable(double)) # True
调用表达式:
double(5)
会把 double 作为可调用对象处理,而不是要求它必须是函数。
1. 有状态的可调用对象
可调用对象可以携带配置和状态:
class RetryPolicy:
def __init__(self, max_attempts):
self.max_attempts = max_attempts
self.attempts = 0
def __call__(self, operation):
self.attempts += 1
if self.attempts > self.max_attempts:
raise RuntimeError("超过最大尝试次数")
return operation()
policy = RetryPolicy(2)
print(policy(lambda: "success")) # success
print(policy.attempts) # 1
这种对象与闭包、函数对象、类实例之间的区别在于:状态显式存放在实例字段中,配置也可以通过属性或构造参数查看。
2. 可调用对象的生命周期
class Endpoint:
def __init__(self, base_url):
self.base_url = base_url
def __call__(self, path):
return self.base_url.rstrip("/") + "/" + path.lstrip("/")
api = Endpoint("https://example.test")
print(api("/users"))
# https://example.test/users
调用发生时,实例已经完成构造;__call__() 负责的是“执行行为”,不是初始化对象。不要把 __call__() 当成第二个 __init__():
__init__()建立对象状态;__call__()使用或改变已有状态;__new__()决定实例是否创建以及创建何种对象。
类本身也是可调用对象。调用类时,通常会先进入 __new__() 创建实例,再进入 __init__() 初始化实例:
class Product:
def __new__(cls, name):
print("1. __new__")
return super().__new__(cls)
def __init__(self, name):
print("2. __init__")
self.name = name
Product("book")
输出:
1. __new__
2. __init__
如果 __new__() 返回的不是该类实例,后续 __init__() 通常不会以普通方式执行。这是单例、不可变类型构造和对象缓存等机制的基础,但也增加了生命周期复杂度。
3. 调用失败不一定发生在 __call__()
如果调用对象不是可调用对象:
value = 42
value()
会产生 TypeError,因为 int 实例没有可用的调用协议。
如果对象实现了 __call__(),但参数不匹配,错误发生在进入方法后或参数绑定阶段:
class Greeter:
def __call__(self, name):
return f"Hello, {name}"
greeter = Greeter()
greeter() # TypeError: 缺少 name 参数
因此,调试“对象不可调用”时,应分别检查:
print(type(value))
print(callable(value))
print(getattr(type(value), "__call__", None))
七、上下文协议:with 如何保证清理
上下文管理器用于表达一段代码执行前后的资源状态转换。最常见的语法是:
with manager as value:
body
对象需要提供:
__enter__()
__exit__(exc_type, exc_value, traceback)
Python 语言参考规定,执行单个 with 项时,会先求值上下文表达式,然后获取 __enter__() 和 __exit__(),调用 __enter__(),执行代码块,最后调用 __exit__()。如果代码块发生异常,异常信息会作为三个参数传给 __exit__();如果 __exit__() 返回真值,异常会被抑制,否则异常会继续传播。(docs.python.org)
概念上:
with EXPRESSION as TARGET:
SUITE
接近:
manager = EXPRESSION
enter = manager.__enter__
exit = manager.__exit__
value = enter()
try:
TARGET = value
SUITE
except:
if not exit(*sys.exc_info()):
raise
else:
exit(None, None, None)
这段伪代码用于理解控制流,不应当被视为精确的源代码替换;实际 with 语句使用隐式特殊方法查找。
1. 一个完整的事务上下文
class Transaction:
def __init__(self):
self.active = False
self.committed = False
def __enter__(self):
print("BEGIN")
self.active = True
return self
def commit(self):
if not self.active:
raise RuntimeError("事务未启动")
print("COMMIT")
self.committed = True
def __exit__(self, exc_type, exc_value, traceback):
if exc_type is None:
if not self.committed:
print("ROLLBACK: 未显式提交")
else:
print(f"ROLLBACK: {exc_type.__name__}")
self.active = False
return False
with Transaction() as tx:
print("work")
tx.commit()
输出:
BEGIN
work
COMMIT
如果代码块发生异常:
try:
with Transaction() as tx:
print("work")
raise ValueError("bad input")
except ValueError:
print("exception propagated")
输出:
BEGIN
work
ROLLBACK: ValueError
exception propagated
因为 __exit__() 返回 False,异常不会被吞掉。
2. 何时抑制异常
class IgnoreValueError:
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
return exc_type is ValueError
with IgnoreValueError():
raise ValueError("ignored")
print("continues")
输出:
continues
但如果异常不是 ValueError:
with IgnoreValueError():
raise TypeError("not ignored")
TypeError 会继续传播。
抑制异常意味着控制流发生了真实改变。只有当上下文管理器明确知道该异常代表“可接受的结束状态”时,才应该返回真值。否则,异常会被静默丢失,诊断信息也会消失。
3. __enter__() 失败时的边界
如果 __enter__() 自身抛出异常,__exit__() 不会被调用,因为上下文尚未成功进入:
class BrokenEnter:
def __enter__(self):
print("entering")
raise RuntimeError("cannot enter")
def __exit__(self, exc_type, exc_value, traceback):
print("exiting")
try:
with BrokenEnter():
print("body")
except RuntimeError:
print("failed before body")
输出:
entering
failed before body
但只要 __enter__() 成功返回,即使把返回值绑定到目标变量时失败,__exit__() 也应被调用。这保证了已经成功获取的资源能够清理。(docs.python.org)
4. 多个上下文管理器的嵌套顺序
with A() as a, B() as b:
body()
语义上类似于:
with A() as a:
with B() as b:
body()
因此执行顺序为:
A.__enter__()
B.__enter__()
body()
B.__exit__()
A.__exit__()
如果 B.__enter__() 失败,B.__exit__() 不会执行,但 A.__exit__() 会执行,因为 A 已经成功进入。
5. 异步上下文协议
异步代码使用:
async with manager:
await operation()
对应:
__aenter__()
__aexit__(exc_type, exc_value, traceback)
这两个方法通常返回可等待对象:
class AsyncResource:
async def __aenter__(self):
print("open")
return self
async def __aexit__(self, exc_type, exc_value, traceback):
print("close")
async def fetch(self):
return "data"
使用:
import asyncio
async def main():
async with AsyncResource() as resource:
print(await resource.fetch())
asyncio.run(main())
__aenter__() 和 __aexit__() 适用于需要异步初始化或异步清理的资源,例如异步连接池、异步锁和异步文件接口。它们不能用普通 with 替代,因为普通上下文协议不会等待异步结果。
八、上下文管理器的状态、可重入性与并发风险
上下文管理器往往包含状态:
class Connection:
def __init__(self):
self.opened = False
def __enter__(self):
if self.opened:
raise RuntimeError("connection already open")
self.opened = True
return self
def __exit__(self, exc_type, exc_value, traceback):
self.opened = False
这个实现是“不可重入”的:同一个实例不能嵌套进入。
connection = Connection()
with connection:
with connection:
pass
内层进入会失败。
可重入上下文管理器需要定义嵌套深度或状态栈:
class ReentrantContext:
def __init__(self):
self.depth = 0
def __enter__(self):
self.depth += 1
print("enter", self.depth)
return self
def __exit__(self, exc_type, exc_value, traceback):
print("exit", self.depth)
self.depth -= 1
context = ReentrantContext()
with context:
with context:
pass
输出:
enter 1
enter 2
exit 2
exit 1
但“可重入”与“线程安全”是两个概念。即使同一个线程可以嵌套使用,多个线程同时进入仍可能破坏共享状态。若上下文管理器持有共享资源,必须明确:
- 状态是否属于实例;
- 是否允许同一实例重复进入;
- 是否允许跨线程使用;
__exit__()是否一定能恢复状态;- 清理过程失败时如何处理原始异常。
标准库 contextlib 也区分一次性、可复用和可重入上下文管理器;例如某些基于生成器的上下文通常只能使用一次。(docs.python.org)
九、属性访问与特殊方法的交叉点
特殊方法定义在类型上,但特殊方法内部经常访问实例属性:
class Account:
def __init__(self, balance):
self.balance = balance
def __repr__(self):
return f"Account(balance={self.balance})"
如果类还重写了 __getattribute__(),就必须注意递归:
class SafeAccount:
def __init__(self, balance):
self.balance = balance
def __getattribute__(self, name):
print("reading", name)
return object.__getattribute__(self, name)
def __repr__(self):
balance = object.__getattribute__(self, "balance")
return f"SafeAccount(balance={balance})"
错误写法:
class Broken:
def __getattribute__(self, name):
return self.__dict__[name]
访问 self.__dict__ 本身会再次触发 __getattribute__(),最终导致递归错误。应通过:
object.__getattribute__(self, name)
访问底层属性。
这与特殊方法的隐式查找有一个重要区别:
obj.custom_attribute # 可能进入 obj.__getattribute__()
len(obj) # 特殊方法隐式查找通常绕过实例属性访问
因此,在调试代理对象、ORM 实体、懒加载对象时,不能只在 __getattribute__() 中打印日志,然后假定所有语言操作都会经过它。
十、一个组合示例:可切片、可调用、可比较、可格式化的对象
下面把多个协议组合到一个小型数据对象中:
from functools import total_ordering
@total_ordering
class ScoreList:
def __init__(self, scores):
self._scores = tuple(float(score) for score in scores)
# 容器协议
def __len__(self):
return len(self._scores)
def __getitem__(self, key):
if isinstance(key, slice):
return type(self)(self._scores[key])
return self._scores[key]
def __iter__(self):
return iter(self._scores)
def __contains__(self, value):
return float(value) in self._scores
# 真值协议
def __bool__(self):
return bool(self._scores)
# 运算符协议
def __add__(self, other):
if not isinstance(other, ScoreList):
return NotImplemented
if len(self) != len(other):
raise ValueError("长度必须相同")
return type(self)(a + b for a, b in zip(self, other))
# 比较协议
def __eq__(self, other):
if not isinstance(other, ScoreList):
return NotImplemented
return self._scores == other._scores
def __lt__(self, other):
if not isinstance(other, ScoreList):
return NotImplemented
return self._scores < other._scores
# 调用协议:缩放所有分数
def __call__(self, factor):
return type(self)(score * factor for score in self)
# 表示协议
def __repr__(self):
return f"ScoreList({self._scores!r})"
def __str__(self):
return ", ".join(f"{score:g}" for score in self)
def __format__(self, spec):
if spec == "csv":
return ",".join(f"{score:g}" for score in self)
return str(self)
first = ScoreList([80, 90, 70])
second = ScoreList([5, 3, 10])
print(len(first)) # 3
print(first[1]) # 90.0
print(first[:2]) # 80, 90
print(90 in first) # True
print(first + second) # 85, 93, 80
print(first(0.5)) # 40, 45, 35
print(first == ScoreList([80, 90, 70])) # True
print(f"{first:csv}") # 80,90,70
这个例子中,每个协议都有明确的语义:
__len__()让对象具有大小;__getitem__()支持索引与切片;__iter__()提供顺序遍历;__contains__()定义成员判断;__bool__()让空对象为假;__add__()定义逐项相加;__eq__()和__lt__()定义值比较;__call__()定义缩放操作;__repr__()、__str__()和__format__()分别服务于调试、显示和格式化。
@total_ordering 可以根据部分比较方法补齐其他排序方法,但它会增加方法分派层次;对性能敏感或比较逻辑复杂的类型,直接实现需要的比较方法通常更清晰。
十一、常见失败模式与诊断方法
1. 把实例属性当成特殊方法实现
obj.__iter__ = lambda: iter([1, 2, 3])
iter(obj) # 可能失败
修复方式:
class Iterable:
def __iter__(self):
return iter([1, 2, 3])
2. 运算不支持时返回 None
def __add__(self, other):
if not isinstance(other, ExpectedType):
return None
这会让表达式看似成功,却把错误推迟到后续代码:
result = left + incompatible
result.value # AttributeError
更合适的是:
return NotImplemented
让 Python 尝试反向操作,或尽早产生明确的 TypeError。
3. __len__() 返回布尔以外的错误类型
class Wrong:
def __len__(self):
return "3"
调用 len(Wrong()) 会失败。长度协议要求非负整数,不能返回字符串或浮点数。
4. __iter__() 返回列表而不是迭代器
class Wrong:
def __iter__(self):
return [1, 2, 3]
iter(Wrong()) 要求返回迭代器对象,而列表本身是可迭代对象但不是迭代器。应写成:
def __iter__(self):
return iter([1, 2, 3])
或者使用生成器:
def __iter__(self):
yield from [1, 2, 3]
5. __exit__() 忘记返回值的含义
不返回值等价于返回 None,属于假值,异常会继续传播:
def __exit__(self, exc_type, exc_value, traceback):
cleanup()
# 隐式返回 None,异常不会被抑制
如果清理失败,应谨慎处理异常链。不要为了“保证退出”而无条件吞掉原始异常。
6. __repr__() 产生副作用
不建议这样写:
def __repr__(self):
self.refresh_from_database()
return ...
因为调试器、日志记录、容器显示甚至异常处理都可能隐式调用 repr()。表示方法应尽量是无副作用、有限时、不会再次触发复杂业务流程的纯观察操作。
7. 误以为 x += y 一定修改 x
对于不可变类型,x += y 可能创建新对象并重新绑定名称;对于可变类型,可能修改原对象。判断行为时要同时检查:
before = x
x += y
print(before is x)
但身份结果只能说明这次实现的结果,不能替代对类型协议的理解。
十二、如何选择需要实现的特殊方法
实现特殊方法之前,先从调用方的语法需求反推协议:
需要 len(x) -> __len__
需要 x[key] -> __getitem__
需要 x[key] = value -> __setitem__
需要 del x[key] -> __delitem__
需要 item in x -> __contains__
需要 for item in x -> __iter__
需要 reversed(x) -> __reversed__
需要 if x -> __bool__ 或 __len__
需要 x + y -> __add__,必要时 __radd__
需要 x += y -> __iadd__
需要 x == y -> __eq__
需要 hash(x) -> __hash__
需要 x(...) -> __call__
需要 repr(x) -> __repr__
需要 f"{x:spec}" -> __format__
需要 bytes(x) -> __bytes__
需要 with x -> __enter__ 和 __exit__
需要 async with x -> __aenter__ 和 __aexit__
协议之间还存在语义约束:
__eq__()与__hash__()必须保持一致;__iter__()返回值必须是迭代器;__len__()必须返回非负整数;__bool__()必须返回布尔值;- 运算不支持时通常返回
NotImplemented; __exit__()返回真值会抑制异常;__enter__()成功后,必须确保__exit__()能完成必要清理;- 作为字典键的对象,其哈希相关状态不能在进入容器后改变。
特殊方法的价值不在于“把类写得像内置类型”,而在于让对象和 Python 语言的既有控制流、表达式和资源管理机制建立一致契约。容器协议决定对象如何被访问和遍历,运算符协议决定对象如何参与表达式,调用协议决定对象能否作为行为单元传递,表示协议决定对象如何被观察,上下文协议则决定资源状态如何进入、退出和恢复。理解这些协议之间的查找规则、返回值约束和失败路径,才是真正理解 Python 对象模型的入口。
系列导航与关联阅读
- 系列入口:Python 完整学习路线:从语言模型、并发到 Web、数据、AI 与生产交付
- 上一篇:Python dataclass 与 Enum:数据模型、不可变性、比较和序列化
- 下一篇:Python 元类:类创建流程、prepare、注册和使用边界
- 延伸:Python 对象模型:type、object、属性查找、身份与生命周期
- 延伸:Python 描述符与 property:属性访问、绑定方法和 ORM 基础
官方资料
本文依据 Python 官方文档、相关 PEP 与生态项目官方文档重新梳理;正文、示例与工程清单由 WR BLOG 编写。

评论
0 条讨论