10-为什么-len-不是方法
约 3285 字大约 11 分钟
2026-09-28
许多面向对象语言会把对象相关操作写成方法调用:
collection.length()
collection.size()Python 却使用:
len(collection)但自定义类型为了支持这个操作,又要实现:
def __len__(self):
...这看起来像多绕了一层。既然长度属于对象,为什么不直接统一成 collection.len()?
答案不只是“因为内置函数更快”。len() 同时体现了 Python 的三项设计取舍:
- 为常见基础操作提供统一、醒目的语言入口;
- 允许内置类型使用紧贴底层数据结构的高效实现;
- 仍然通过
__len__让自定义类型接入同一个协议。
1. len() 是公开入口,__len__ 是实现入口
先看一个自定义容器:
class TaskQueue:
def __init__(self, tasks):
self._tasks = list(tasks)
def __len__(self):
return len(self._tasks)
queue = TaskQueue(["build", "test", "deploy"])
print(len(queue))输出为:
3这里存在两个不同角色:
调用者使用:len(queue)
类型作者实现:TaskQueue.__len__可以把它理解为一个语言级协议:
len(obj)
-> 询问 obj 的类型是否支持长度协议
-> 如果支持,读取对应的长度实现
-> 检查结果是否是合法的非负整数
-> 返回长度因此,len() 不是绕开对象的外部工具。对象仍然通过自己的类型决定长度,只是调用者使用统一入口发起操作。
2. 统一入口消除了方法命名差异
如果 Python 没有规定统一入口,不同类型可能各自选择不同名称:
list.size()
text.length()
mapping.count()
queue.item_count()调用者不仅要知道对象具有长度,还要知道这个类型选择了哪个名字。
len(obj) 把问题改成:
这个对象是否支持 Python 的长度协议?
只要支持,调用方式就一致:
print(len([10, 20, 30]))
print(len("Python"))
print(len({"host": "localhost", "port": 8000}))
print(len(TaskQueue(["build", "test"])))输出为:
3
6
2
2列表、字符串、字典和自定义队列的数据结构完全不同,但调用者不需要更换 API。
这种统一性也适用于泛型代码:
def is_empty(value):
return len(value) == 0函数不必检查参数是列表、元组还是自定义集合,只依赖长度协议。
3. len() 让基础操作在代码中更醒目
Python 把长度视为非常常用的基础操作。写成内置函数后,它在代码中具有稳定形式:
len(value)阅读者不需要先了解类型的业务 API,就能识别这是在获取项数。
同类设计还包括:
abs(number)
iter(collection)
next(iterator)
repr(obj)
hash(obj)
bool(obj)它们都采用“公开内置函数 + 对象特殊方法”的形式:
abs(obj) -> __abs__
iter(obj) -> __iter__
next(obj) -> __next__
repr(obj) -> __repr__
hash(obj) -> __hash__
bool(obj) -> __bool__,必要时回退到 __len__这些操作是 Python 对象模型中的通用协议,不是某个具体业务类型独占的动作。
4. 内置类型可以直接使用底层长度信息
在 CPython 中,list、tuple、str 等内置对象的底层结构会保存长度或能够快速读取长度。
因此,解释器处理:
len(a_list)时,可以通过类型中的内部操作槽读取长度,而不必像普通 Python 方法调用那样:
- 从实例上查找属性;
- 创建绑定方法对象;
- 再执行一段 Python 函数体。
这使 len() 能够为核心内置类型提供非常低成本的路径。
需要注意两个边界。
第一,这是 CPython 的实现细节。其他 Python 实现可以采用不同内部结构,只要外部行为符合语言协议。
第二,len() 的存在并不自动保证任意自定义对象的长度计算都是 O(1)。下面的实现每次都会遍历数据:
class SlowCollection:
def __init__(self, source):
self.source = source
def __len__(self):
count = 0
for _ in self.source:
count += 1
return count协议只规定结果语义和返回值要求,没有强制复杂度。但使用者通常预期 len(obj) 是便宜且无副作用的操作,因此自定义类型应尽量满足这一习惯。
5. 为什么 len(obj) 不等于普通的 obj.__len__()
对于简单纯 Python 类,两者可能返回相同数字:
queue = TaskQueue(["build", "test"])
print(len(queue))
print(queue.__len__())2
2但语义上仍然不同。
隐式调用从类型上查找
给单个实例添加同名属性,不会让它接入长度协议:
class Report:
pass
report = Report()
report.__len__ = lambda: 5
print(report.__len__())
print(len(report))第一行输出 5,第二行抛出:
TypeError: object of type 'Report' has no len()len(report) 从 Report 类型及其继承关系中寻找协议实现,不采用实例字典里临时添加的函数。
len() 检查返回值
class InvalidLength:
def __len__(self):
return 2.5
value = InvalidLength()
print(value.__len__())
print(len(value))显式调用会返回 2.5,但 len(value) 会抛出 TypeError。长度协议要求结果是非负整数。
因此,调用者应使用:
len(obj)而不是绕过公开协议直接调用:
obj.__len__()6. __len__ 让统一入口保持可扩展
如果 len() 只认识解释器自带的几种类型,它会是一个封闭的特殊功能。__len__ 让它成为开放协议:用户定义的类型也可以参与。
class Page:
def __init__(self, rows):
self._rows = tuple(rows)
def __len__(self):
return len(self._rows)
page = Page(["r1", "r2", "r3"])
print(len(page))3解释器不需要事先知道 Page 的存在。它只需要确认对象类型提供了符合要求的长度实现。
这种模式兼顾了两类对象:
- 内置类型可以使用底层优化;
- 自定义类型可以通过特殊方法扩展协议。
7. 长度协议还参与真假判断
__len__ 不只服务 len()。当对象没有定义 __bool__ 时,Python 会使用长度判断真假:
class TaskQueue:
def __init__(self, tasks):
self._tasks = list(tasks)
def __len__(self):
return len(self._tasks)
empty_queue = TaskQueue([])
ready_queue = TaskQueue(["deploy"])
print(bool(empty_queue))
print(bool(ready_queue))False
True一个长度实现因此接入了两项语言行为:
len(obj) -> 返回项数
bool(obj) -> 没有 __bool__ 时,根据长度是否为 0 判断这也是对象协议的组合价值:类型作者提供一个基础事实,解释器可以在定义清楚的范围内复用它。
8. 为什么不是 .length 属性
另一种可能的设计是:
collection.length这种形式看起来适合“长度是对象属性”的理解,但它会遇到同样的统一性问题:
- 所有类型是否都必须保留
length这个名称? - 动态计算长度时,属性访问是否会隐藏昂贵成本?
- 内置类型怎样在不暴露实现结构的情况下提供统一行为?
- 真假回退和类型级特殊查找怎样与普通属性区分?
Python 已经把答案固定在 len() 和 __len__ 协议中。自定义类型应遵守现有惯例,而不是同时再提供一套 .length,除非领域中确实存在另一个不同概念。
例如,一个分页 API 可能同时拥有:
len(current_page) # 当前页实际加载的记录数
result.total_count # 服务端报告的全部匹配记录数这两个值含义不同,使用不同接口是合理的。不要让 len(current_page) 假装返回尚未加载的远程总数。
9. 为什么很多业务动作仍然是方法
len() 的设计不意味着所有对象操作都应该变成全局函数。
业务动作通常与具体对象职责紧密相关,适合普通方法:
order.cancel()
connection.commit()
document.publish()
cache.invalidate(key)这些操作没有跨越大量无关类型的统一语言语义,也不需要解释器参与特殊查找或运算符回退。
可以采用以下判断方式:
- Python 已经定义了对应协议时,实现特殊方法并让调用者使用标准入口;
- 操作是类型特有的业务行为时,使用普通方法;
- 不要自行创建新的双下划线方法来模拟语言协议;
- 不要为了“面向对象纯度”重复包装已有内置操作。
例如,这种封装没有增加信息:
class TaskQueue:
def size(self):
return len(self._tasks)如果对象的语义就是容器长度,实现 __len__ 后统一使用 len(queue) 更符合 Python 习惯。
10. len() 与多态
在其他语言中,多态常通过某个命名接口实现:
interface Sized {
int size();
}Python 的长度多态由协议完成:不同类型提供各自的 __len__,调用者统一使用 len()。
from collections.abc import Sized
class TaskQueue:
def __init__(self, tasks):
self._tasks = list(tasks)
def __len__(self):
return len(self._tasks)
queue = TaskQueue(["test"])
print(isinstance(queue, Sized))
print(len(queue))True
1Sized ABC 为运行时分类提供了一个命名接口,但 len() 本身不要求调用者先检查 isinstance(obj, Sized)。通常直接尝试所需操作更简单:
length = len(obj)只有程序确实需要根据能力走不同分支时,才考虑类型检查。
11. 不要用 hasattr(obj, "__len__") 判断长度能力
下面的检查并不可靠:
if hasattr(obj, "__len__"):
length = len(obj)原因包括:
- 隐式特殊方法查找通常从类型上进行;
- 实例属性里可能存在不会被
len()使用的__len__; - 方法存在不代表返回值符合协议;
- 动态代理可能定制普通属性访问。
如果代码确实需要长度,最直接的方法是调用 len() 并在合适的边界处理 TypeError:
try:
length = len(obj)
except TypeError:
length = None如果能力分类本身就是业务逻辑,也可以使用 collections.abc.Sized。但 ABC 检查同样不能验证实现是否返回合法值,真正使用时仍需依赖协议行为。
12. len() 不应该触发破坏性消费
一次性迭代器或流通常不知道剩余长度,除非把所有数据读完。为了实现 __len__ 而消费数据会产生意外副作用:
class BadStream:
def __init__(self, source):
self.source = iter(source)
def __len__(self):
return sum(1 for _ in self.source)调用 len(stream) 后,流已经被耗尽。随后遍历不会得到任何数据。
这种类型不应该实现 __len__。缺少长度能力比提供一个破坏对象状态的长度实现更诚实。
如果数据源能够给出估算值,可以使用带明确名称的方法或属性,例如:
stream.estimated_remaining()不要让 len() 的稳定语义变成“执行一次有副作用的统计任务”。
13. 一个完整示例:当前页长度与总记录数
下面的分页结果同时包含当前页数据和远程查询报告的总数。它区分了两个不同概念:
class Page:
def __init__(self, records, total_count):
self._records = tuple(records)
self.total_count = total_count
def __len__(self):
return len(self._records)
def __iter__(self):
return iter(self._records)
def __repr__(self):
return (
f"Page(records={list(self._records)!r}, "
f"total_count={self.total_count!r})"
)
if __name__ == "__main__":
page = Page(
records=["order-101", "order-102", "order-103"],
total_count=127,
)
print(page)
print(len(page))
print(page.total_count)
print(bool(page))
print(list(page))输出为:
Page(records=['order-101', 'order-102', 'order-103'], total_count=127)
3
127
True
['order-101', 'order-102', 'order-103']这个设计保持了接口语义:
len(page)返回当前对象实际包含的 3 条记录;page.total_count明确表示查询匹配的 127 条总记录;bool(page)通过__len__回退判断当前页是否为空;__iter__只遍历当前页已经加载的内容。
如果让 len(page) 返回 127,调用者可能误以为这个对象能够直接迭代 127 项,长度和容器实际内容就会产生冲突。
14. 本节小结
len(obj) 不是对面向对象设计的破坏,而是 Python 数据模型中的统一协议入口:
- 调用者使用稳定的
len(obj),不必记忆每种类型的长度方法名; - 内置类型可以通过解释器内部槽位高效提供长度;
- 自定义类型通过
__len__接入同一协议; len()会执行类型级特殊方法查找和返回值检查;__len__还可以为没有__bool__的对象提供真假语义;- 长度应表示对象实际包含的项数,并尽量快速、稳定、无副作用;
- 不知道长度的一次性流不应为了兼容
len()而破坏性消费数据; - Python 已定义的通用操作使用标准协议,类型特有的业务动作仍然使用普通方法。
这一设计在实用性、统一性、内置类型性能和用户扩展能力之间取得了平衡。
拓展阅读与查阅
以下资料用于进一步研究或日后查阅。理解本节内容不依赖这些链接。
