理解迭代器中的len函数

5 投票
1 回答
1579 浏览
提问于 2025-04-20 11:58

在阅读文档时,我发现内置的 len 函数并不是支持所有可迭代对象,而只是支持序列、映射(还有集合)。在看到这一点之前,我一直以为 len 函数是通过一种叫做 迭代协议 的方式来计算对象的长度,所以看到这个信息时我感到很惊讶。

我也看过一些已经发布的问题(这里这里),但我还是感到困惑,依然不明白为什么 len 不允许在所有可迭代对象上使用。

这是一个更概念性或逻辑上的原因,而不是实现上的原因吗?我的意思是,当我询问一个对象的长度时,我其实是在问它的一个属性(它有多少个元素),而生成器这样的对象并没有这个属性,因为它们内部没有元素,而是 生成 元素。

此外,生成器对象可以产生无限的元素,这样就导致了长度是未定义的,而其他对象,比如列表、元组、字典等,是不会出现这种情况的……

所以我理解得对吗?还是说还有其他的见解或者我没有考虑到的东西?

1 个回答

10

最大的原因是 它降低了类型安全性

你写过多少程序,真的 需要 消耗一个可迭代对象,只是为了知道它有多少个元素,而把其他的都丢掉?

在我多年的Python编程经历中,我 从来 不需要这样做。这在正常程序中是没有意义的。一个迭代器可能没有长度(比如无限迭代器或通过 send() 接收输入的生成器),所以询问它的长度并没有太大意义。len(an_iterator) 产生错误的事实意味着 你可以发现代码中的错误。你可以看到在程序的某个部分,你在错误的对象上调用了 len,或者你的函数实际上需要一个序列,而不是你预期的迭代器。

消除这样的错误会产生一种新的错误类型,人们在调用 len 时错误地消耗了一个迭代器,或者把迭代器当作序列使用而没有意识到。

如果你真的需要知道一个迭代器的长度,使用 len(list(iterator)) 有什么问题呢?多出的6个字符?写一个适用于迭代器的版本是很简单的,但正如我所说,99%的情况下,这只是意味着 你的代码有问题,因为这样的操作并没有太大意义。

第二个原因是,做出这样的改变会违反当前对所有(已知)容器的 len 的两个良好特性:

  • 它在Python中实现的所有容器上都是已知的便宜操作(所有内置类型、标准库、numpyscipy 以及 所有 其他大型第三方库在动态和静态大小的容器上都是这样)。所以当你看到 len(something) 时,你知道这个 len 调用是便宜的。如果让它支持迭代器,那么突然间所有程序可能因为计算长度而变得低效。

    另外,请注意,你可以轻松地在每个容器上实现 O(1) 的 __len__。预计算长度的成本通常是微不足道的,通常是值得的。唯一的例外是如果你实现了 不可变 容器,并且它们的内部表示与其他实例共享(以节省内存)。不过,我不知道有任何实现是这样做的,而且大多数情况下你可以实现比 O(n) 更好的时间复杂度。

    总之:目前 每个人 都在 O(1) 的时间复杂度下实现 __len__,而且 继续这样做很简单。所以 人们期望len 的调用是 O(1) 的。即使这不是标准的一部分,Python 开发者 故意 避免在文档中使用 C/C++ 的法律术语,并信任用户。在这种情况下,如果你的 __len__ 不是 O(1),你应该在文档中说明。

  • 它被认为是 不具破坏性 的。任何 合理的实现__len__ 不会改变它的参数。所以你可以确保 len(x) == len(x),或者 n = len(x);len(list(x)) == n

    即使这个特性在文档中没有定义,但大家都期望如此,目前没有人违反这一点。

这样的特性是好的,因为 你可以基于它们推理和做出假设。它们可以帮助你确保一段代码的正确性,或者理解它的渐进复杂度。你提议的改变会让你更难以查看某些代码并理解它是否正确或它的复杂度,因为你必须考虑特殊情况。

总之,你提议的改变有一个非常小的好处:在非常特定的情况下节省几个字符,但它有几个大的缺点,会影响大量现有代码。


还有一个小原因。如果 len 消耗迭代器,我 相信 会有人开始滥用它的副作用(替代已经很丑陋的 map 或列表推导式)。突然间,人们可以写出这样的代码:

len(print(something) for ... in ...)

来打印文本,这真的很丑陋。它读起来不顺畅。有状态的代码应该被限制在语句中,因为它们提供了副作用的视觉提示。

撰写回答