如何计算任意可迭代对象(如生成器)中的项数?
假设我有一个可以遍历的东西,比如一个生成器,它可以逐行读取文件,并且只返回那些符合某个规则的行。
那么,我该怎么计算这个可遍历的东西里面有多少个元素呢?假设我并不关心这些元素的具体内容。
8 个回答
一种简单的方法是:
def ilen(it):
return len(list(it))
需要注意的是,如果你要生成很多元素(比如说,几万甚至更多),那么把它们放在一个列表里可能会影响性能。不过,这里给出的只是一个简单的想法,对于大多数情况来说,性能问题并不是很重要。
这个方法在处理很长的可迭代对象时,比 sum(1 for _ in it)
快得多,而在处理短的可迭代对象时速度差不多,且它的内存使用是固定的(不像 len(list(it))
),这样可以避免在处理大数据时出现频繁的内存交换和重新分配的问题:
# On Python 2 only, get zip that lazily generates results instead of returning list
from future_builtins import zip
from collections import deque
from itertools import count
# Avoid constructing a deque each time, reduces fixed overhead enough
# that this beats the sum solution for all but length 0-1 inputs
consumeall = deque(maxlen=0).extend
def ilen(it):
# Make a stateful counting iterator
cnt = count()
# zip it with the input iterator, then drain until input exhausted at C level
consumeall(zip(it, cnt)) # cnt must be second zip arg to avoid advancing too far
# Since count 0 based, the next value is the count
return next(cnt)
和 len(list(it))
一样,它在 CPython 中用 C 代码执行循环(deque
、count
和 zip
都是用 C 实现的);在 CPython 中,避免每次循环都执行字节码通常是提高性能的关键。
想出公平的测试案例来比较性能其实挺难的(list
使用了 __length_hint__
,而这在任意输入的可迭代对象中不太可能可用,itertools
的一些函数如果不提供 __length_hint__
,通常会有一些特殊的操作模式,这些模式在每次循环返回的值在请求下一个值之前被释放时会更快,而 deque
在 maxlen=0
的情况下就会这样做)。我使用的测试案例是创建一个生成器函数,它接受一个输入并返回一个没有特殊 itertools
返回容器优化或 __length_hint__
的 C 级生成器,使用 Python 3.3+ 的 yield from
:
def no_opt_iter(it):
yield from it
然后使用 ipython
的 %timeit
魔法(用不同的常数替换 100):
>>> %%timeit fakeinput = (0,) * 100
... ilen(no_opt_iter(fakeinput))
当输入不大到 len(list(it))
会导致内存问题时,在一台运行 Python 3.9 x64 的 Linux 机器上,我的解决方案比 def ilen(it): return len(list(it))
慢大约 50%,无论输入长度如何。
对于最小的输入,加载/调用 consumeall
/zip
/count
/next
的设置成本意味着这种方式比 def ilen(it): sum(1 for _ in it)
要慢一点(在我机器上,长度为 0 的输入大约慢 40 纳秒,这比简单的 sum
方法多了 10%),但当输入长度达到 2 时,成本就差不多了,而在长度达到 30 时,初始的开销与实际工作相比几乎可以忽略不计;而 sum
方法大约慢 50%。
总的来说,如果内存使用很重要,或者输入的大小不固定,并且你更关心速度而不是简洁性,那就用这个方法。如果输入的大小是有限的且比较小,len(list(it))
可能是最好的选择;如果输入不受限制,但简洁性更重要,那就用 sum(1 for _ in it)
。
在Python 2中,可以用itertools.imap()
,而在Python 3中可以用map()
,这些都可以用类似的生成器表达式来替代:
sum(1 for dummy in it)
这个方法也使用了懒加载的生成器,所以它不会一次性把所有迭代器的元素都放到内存里。