04-特殊方法是如何使用的
约 3877 字大约 13 分钟
2026-09-28
前面的纸牌案例展示了一个直接效果:实现 __len__ 和 __getitem__ 后,自定义对象就能使用 len()、下标、切片和迭代等 Python 操作。
这一节进一步回答几个更底层的问题:
- 谁在调用特殊方法?
len(obj)是否等价于obj.__len__()?- Python 在哪里查找特殊方法?
- 为什么给单个对象临时添加
__len__往往没有作用? - 一个专用方法不存在时,Python 为什么有时会回退到其他协议?
- 业务代码什么时候可以直接调用特殊方法?
理解这些机制后,我们就不会把特殊方法误当成一组命名奇怪的普通业务 API。
1. 特殊方法主要由解释器调用
特殊方法是 Python 解释器与对象之间的协议入口。
开发者通常发起的是公开操作:
len(obj)
obj[index]
left + right
for item in obj:
...解释器再根据操作寻找相应的特殊方法:
len(obj) -> 长度协议
obj[index] -> 下标协议
left + right -> 加法协议
for item in obj -> 迭代协议以长度协议为例:
class Inventory:
def __init__(self, items):
self._items = list(items)
def __len__(self):
print("调用 Inventory.__len__")
return len(self._items)
inventory = Inventory(["keyboard", "mouse", "display"])
print(len(inventory))输出为:
调用 Inventory.__len__
3调用者只使用 len(inventory),但解释器通过 Inventory.__len__ 获得了长度。
这形成了一个重要分工:
- 类的作者实现特殊方法,用来接入协议;
- 类的使用者采用对应的语法、运算符或内置函数。
业务代码通常不应该写成:
inventory.__len__()应优先使用:
len(inventory)后者才是长度协议公开给调用者的统一入口。
2. len(obj) 不只是普通方法调用的缩写
对一个简单的纯 Python 类来说,下面两种写法可能得到相同数值:
len(inventory)
inventory.__len__()但不能因此认为 Python 只是把第一种写法机械翻译成第二种写法。
更接近实际机制的概念模型是:
type(inventory).__len__(inventory)这个模型仍然省略了继承、描述符和 CPython 内部槽位等实现细节,但它准确表达了关键点:隐式特殊方法调用通常从对象的类型上查找,而不是像普通属性那样先从实例自身查找。
内置函数还会执行协议检查
len() 不只是调用方法,还会检查结果是否符合长度协议。
class InvalidLength:
def __len__(self):
return 3.5
value = InvalidLength()
print(value.__len__())
print(len(value))直接调用 value.__len__() 会得到:
3.5但 len(value) 会抛出 TypeError,因为长度协议要求返回非负整数。直接调用方法绕开了 len() 提供的协议入口,也失去了这层统一检查。
内置类型可能走更快的路径
对 list、str、bytes 等内置类型,CPython 可以直接读取底层结构中记录的长度,而不必执行一次普通的 Python 方法调用。
这属于解释器实现优化。编写业务代码时无须依赖具体内部结构,只需要遵循统一接口:
len(value)同一个入口既适用于内置对象,也适用于实现了 __len__ 的自定义对象。
3. 特殊方法通常从类型上查找
普通实例属性可以只存在于某个对象上:
class Report:
pass
report = Report()
report.title = "September"
print(report.title)输出为:
September但隐式特殊方法调用通常不会以相同方式读取实例属性。下面给对象临时添加一个名为 __len__ 的可调用属性:
class Report:
pass
report = Report()
report.__len__ = lambda: 5
print(report.__len__())
print(len(report))第一条 print 会输出:
5第二条却会抛出:
TypeError: object of type 'Report' has no len()原因是 len(report) 寻找的是 type(report),也就是 Report 上的长度协议实现。它不会把实例字典中的 report.__len__ 当作隐式协议入口。
把方法定义在类上才会生效:
class Report:
def __len__(self):
return 5
report = Report()
print(len(report))5这种类型级查找有明确价值:
- 同一类型的对象遵守一致的语言行为;
- 解释器不必为每次基础运算检查实例字典;
- 特殊方法能够映射到类型层面的内部操作槽位;
+、len()、迭代等基础语义不容易被单个实例偶然覆盖。
如果不同实例确实需要不同结果,应让类上的特殊方法读取实例状态,而不是给每个实例替换特殊方法:
class Report:
def __init__(self, rows):
self.rows = list(rows)
def __len__(self):
return len(self.rows)
short_report = Report(["one"])
long_report = Report(["one", "two", "three"])
print(len(short_report))
print(len(long_report))1
3协议实现在类型上保持一致,具体结果仍然可以由每个实例的数据决定。
4. 隐式调用通常绕过实例的普通属性访问
如果一个类自定义 __getattribute__,普通的显式属性读取会经过它:
class TracedLength:
def __getattribute__(self, name):
print(f"读取属性:{name}")
return object.__getattribute__(self, name)
def __len__(self):
return 7
value = TracedLength()
print(value.__len__())输出包含一次属性读取:
读取属性:__len__
7因为 value.__len__ 是显式读取实例属性。
改为使用内置函数:
print(len(value))输出只有:
7len(value) 的隐式查找不会通过 value.__getattribute__("__len__") 这条普通实例属性路径,而是从类型层面解析长度操作。
这里不能推出“所有双下划线名称都会绕过 __getattribute__”。只有由 Python 语法或内置操作触发的隐式特殊方法查找具有这种规则。代码显式写出 value.__len__ 时,它仍然是一次普通属性访问。
5. 继承会参与特殊方法查找
特殊方法从类型上查找,并不意味着只查看当前类。Python 仍会按照方法解析顺序(Method Resolution Order,MRO)处理继承关系。
class BaseQueue:
def __init__(self, items):
self._items = list(items)
def __len__(self):
return len(self._items)
class PendingQueue(BaseQueue):
pass
queue = PendingQueue(["build", "test", "deploy"])
print(len(queue))输出为:
3PendingQueue 没有自行定义 __len__,但它从 BaseQueue 继承了长度协议。
子类也可以覆盖这个行为:
class VisibleQueue(BaseQueue):
def __len__(self):
return sum(item != "hidden" for item in self._items)
queue = VisibleQueue(["build", "hidden", "deploy"])
print(len(queue))2此时 len(queue) 会按照 VisibleQueue 的 MRO 找到子类实现。
需要谨慎的是,长度通常应表达一个稳定、直观且成本合理的概念。如果不同子类让 len() 表达完全不同的含义,调用者会难以建立一致预期。
6. 每种操作有自己的查找和回退规则
“特殊方法不存在时,Python 会尝试别的方法”并不是一条普遍、统一的规则。回退路径由每种语言操作分别定义。
常见例子包括:
bool(obj)
真假判断优先使用 __bool__。如果没有,再尝试 __len__;两者都没有时,对象默认为真。
bool(obj)
-> __bool__
-> 如果没有,尝试 __len__
-> 如果仍没有,结果为 Trueiter(obj)
迭代优先使用 __iter__。如果没有,Python 可以尝试从索引 0 开始调用 __getitem__,直到遇到 IndexError。
iter(obj)
-> __iter__
-> 如果没有,尝试基于 __getitem__ 创建序列迭代器item in obj
成员判断优先使用 __contains__。如果没有,尝试迭代;迭代本身又可能回退到 __getitem__。
item in obj
-> __contains__
-> 如果没有,尝试 __iter__
-> 如果仍没有,尝试 __getitem__reversed(obj)
反向遍历优先使用 __reversed__。如果没有,可以在对象同时支持长度和整数索引时,从最后一个位置倒序读取。
reversed(obj)
-> __reversed__
-> 如果没有,尝试 __len__ + __getitem__left + right
二元运算涉及两个操作数。加法通常先尝试左操作数类型的 __add__;如果该实现明确返回 NotImplemented,解释器可能再尝试右操作数类型的 __radd__。
left + right
-> type(left).__add__(left, right)
-> 返回 NotImplemented 时,考虑 type(right).__radd__(right, left)
-> 双方都不支持时抛出 TypeError其中还涉及两个类型的继承关系和方法优先级,后面的向量小节会结合代码详细展开。
这些流程说明:实现一个专用方法通常能提供最明确、最高效的行为;缺少专用方法时,某些操作才会按照各自协议规定尝试较基础的能力。
7. 回退路径不是免费的
协议回退提高了对象的可组合性,但不一定是性能最好的实现。
例如,一个容器没有实现 __contains__ 时,item in container 可以逐项迭代:
最坏情况:检查所有 n 个元素
时间复杂度:O(n)如果对象内部已经维护了哈希索引,就可以实现 __contains__,把成员判断降为平均 O(1)。
类似地:
__reversed__可以为链式结构提供比反复索引更合理的反向遍历;__iter__可以避免旧式__getitem__迭代产生的连续索引调用;__bool__可以在长度计算昂贵时直接表达对象是否有效。
因此,能通过回退工作不代表永远不需要专用方法。选择时应同时考虑:
- 对象语义是否明确;
- 回退结果是否正确;
- 时间和空间成本是否合适;
- 是否需要被抽象基类或静态工具识别;
- 专用实现会不会增加不必要的维护负担。
8. 哪些特殊方法可能在代码中显式出现
“不要直接调用特殊方法”是良好的默认原则,但不是绝对禁令。
在子类初始化中调用父类实现
子类经常通过 super() 调用父类的 __init__:
class BaseClient:
def __init__(self, endpoint):
self.endpoint = endpoint
class AuthenticatedClient(BaseClient):
def __init__(self, endpoint, token):
super().__init__(endpoint)
self.token = token这里显式出现 __init__ 是为了在继承链中复用初始化实现,不是在替代对象创建语法。创建对象时仍然写:
client = AuthenticatedClient("https://api.example", "secret")而不是手动创建未初始化对象后调用 client.__init__()。
在特殊方法中调用相关协议函数
实现特殊方法时,通常仍然优先使用公开入口处理其他对象。例如:
class Batch:
def __init__(self, records):
self._records = records
def __len__(self):
return len(self._records)这里使用 len(self._records),而不是 self._records.__len__()。
调试和教学验证
为了观察方法本身的原始返回值,调试或教学代码有时会显式调用特殊方法。前面的 InvalidLength 示例就是故意对比直接调用与协议入口。
这种做法应该是有明确目的的局部验证,不应成为正常业务调用风格。
9. 不要自行发明双下划线名称
Python 为语言协议保留了大量 __name__ 形式的名称。自定义下面这样的业务方法并不合适:
class Order:
def __approve__(self):
...问题在于:
- 阅读者可能误以为它是 Python 定义的协议;
- Python 未来可能为同名特殊方法赋予语言含义;
- 双下划线形式不能为普通业务方法带来任何特殊能力。
业务动作应使用普通名称:
class Order:
def approve(self):
...只有在实现 Python 已经定义的对象协议时,才使用对应的特殊方法名称,并遵守它的参数、返回值和异常约定。
10. 特殊方法连接了哪些语言能力
特殊方法覆盖的范围远大于运算符重载。常见类别包括:
- 对象创建与初始化,例如
__new__、__init__; - 字符串表示,例如
__repr__、__str__; - 容器行为,例如
__len__、__getitem__、__contains__; - 迭代行为,例如
__iter__、__next__、__reversed__; - 真假判断,例如
__bool__; - 数值运算,例如
__add__、__mul__、__abs__; - 比较和哈希,例如
__eq__、__lt__、__hash__; - 可调用对象,例如
__call__; - 上下文管理,例如
__enter__、__exit__; - 属性访问,例如
__getattr__、__getattribute__、__setattr__; - 异步操作,例如
__await__、__aiter__、__anext__。
这些名称不需要一次背完。更有效的学习方式是从希望对象支持的公开操作出发,再查找对应协议:
希望对象支持 len(obj)
-> 研究 __len__ 的约定
希望对象支持 for item in obj
-> 研究迭代器协议
希望对象支持 with obj
-> 研究上下文管理协议后面的各小节会继续沿用这种方式,而不是把特殊方法当作孤立的方法清单。
11. 一个用于观察调用路径的完整示例
下面的对象同时实现长度、索引和真假协议。每个方法都会打印自己的名称,便于观察不同公开操作选择了哪个入口。
class TracedCollection:
def __init__(self, items):
self._items = list(items)
def __len__(self):
print("进入 __len__")
return len(self._items)
def __getitem__(self, index):
print(f"进入 __getitem__,index={index}")
return self._items[index]
def __bool__(self):
print("进入 __bool__")
return bool(self._items)
if __name__ == "__main__":
collection = TracedCollection(["alpha", "beta"])
print("长度:", len(collection))
print("首项:", collection[0])
print("真假:", bool(collection))
print("遍历:")
for item in collection:
print(item)输出为:
进入 __len__
长度: 2
进入 __getitem__,index=0
首项: alpha
进入 __bool__
真假: True
遍历:
进入 __getitem__,index=0
alpha
进入 __getitem__,index=1
beta
进入 __getitem__,index=2从结果可以看到:
len(collection)选择__len__;- 下标语法选择
__getitem__; - 即使类也有
__len__,bool(collection)仍优先选择__bool__; - 类没有
__iter__,所以for循环从索引 0 开始回退调用__getitem__; - 索引 2 引发的
IndexError被序列迭代器当作结束信号,没有传播到循环外。
如果删除 __bool__,bool(collection) 才会回退到 __len__。如果再删除 __len__,对象的默认真值就是 True。这些真假规则将在“自定义类型的布尔值”一节详细分析。
12. 本节小结
特殊方法不是供业务代码随意调用的一组普通方法,而是对象接入 Python 语言协议的实现入口。
- 调用者应优先使用
len(obj)、obj[index]、left + right等公开操作; - 隐式特殊方法调用通常从对象的类型上查找,并按照 MRO 处理继承;
- 给单个实例临时添加
__len__,不会让len(instance)接受该对象; - 隐式查找通常绕过实例的普通属性访问路径;
- 内置函数还可能负责返回值检查,并为内置类型使用优化路径;
- 每种操作都有各自定义的查找和回退规则,不能假设存在一套通用回退机制;
- 回退协议能够增加组合能力,但专用实现可能具有更明确的语义和更好的性能;
super().__init__()等继承场景可以合理地显式引用特殊方法;- 不应为普通业务动作自行发明双下划线名称。
接下来的小节会通过二维向量继续研究数值协议,观察 abs(vector)、vector1 + vector2 和 vector * scalar 如何分别触发对象的特殊方法。
拓展阅读与查阅
以下资料用于进一步研究或日后查阅。理解本节内容不依赖这些链接。
