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。它们不是“语法糖形式的普通方法”这么简单:解释器会针对不同操作,按照特定规则从对象的类型、基类和相关协议中查找并调用它们。因此,理解特殊方法,必须同时理解三个层次:

  1. 语法层:某种表达式或语句表示什么操作;
  2. 协议层:该操作要求对象提供哪些方法;
  3. 类型查找层:解释器从哪里查找这些方法,以及失败时如何处理。

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__()

对于自定义类,隐式特殊方法调用通常要求特殊方法定义在对象的类型上,而不是定义在实例字典中。解释器会绕过实例属性查找,直接从类型及其继承体系中寻找对应的特殊方法。这样既保证了行为一致,也避免了对 intstr 等类型对象进行隐式调用时出现绑定歧义。(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] = valuedel 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)

对象的真值判断主要遵循:

  1. 如果类型定义了 __bool__(),调用它;
  2. 否则如果定义了 __len__(),长度为零表示假,非零表示真;
  3. 否则对象为真。
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

字典内部仍然保存着这个对象,但它现在按照新的哈希值查找,无法定位原来的槽位。因此,作为字典键的对象必须保证哈希相关状态在生命周期内稳定。


五、表示协议:reprstr、格式化与字节转换

对象的“表示”至少涉及四个常用协议:

  • __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 官方文档、相关 PEP 与生态项目官方文档重新梳理;正文、示例与工程清单由 WR BLOG 编写。