我该如何计算任意可迭代对象(例如生成器)中的项目数量?

101
假设我有一个任意的可迭代对象 - 例如,一个生成器,它遍历文件的行并yield与正则表达式匹配的行。
那么,在不关心元素本身的情况下,我该如何计算可迭代对象中的项数?

8
请不要使用 _ 作为变量名,因为 (1) 它往往会使人们产生混淆,误以为这是一种特殊的语法;(2) 与交互式解释器中的 _ 重名;(3) 与常见的 gettext 别名冲突。请使用其他合适的变量名代替。 - Sven Marnach
9
@Sven: 我经常在编程时使用“_”表示未使用的变量,这是我从Prolog和Haskell编程中养成的习惯。这也是我提出这个问题的原因之一。(1)就是如此。感谢您指出了(2)和(3),我之前没有考虑到。 - Fred Foo
2
请返回翻译后的文本:重复:https://dev59.com/z3RC5IYBdhLWcg3wKtv2 - tokland
python 3.x, if there exits repeated items and you also want to check the count for each item, use Counter(generator/iterator), eg., c = Counter(iter('goodbadugly')), then count the total: sum(c.values()) - Kuo
1
@SvenMarnach:在函数内部,特别是在生成器表达式中使用“_”不会与交互式解释器发生冲突(在Py2中,在全局范围内使用它在列表推导中会干扰交互式解释器对“_”的使用,但在Py3中已经修复了这个问题,其中列表推导运行在单独的作用域中)。如果您的函数还使用gettext别名,那么是的,这是一个问题,但是在非交互式解释器代码中,“_”是一种被接受的方式,表示“我不关心这里的值”,以至于检查分配未读名称的linter将专门接受它。 - ShadowRanger
@ShadowRanger 我反对它的主要论点是第一个——人们仍然认为下划线有特殊含义,抛弃结果而不是保留它,但实际上它只是一个普通的变量名。如果我可以选择写每个人都能立即理解的代码和一些人有误解的代码,其他条件相等,我会选择前者。然而,我已经放弃了这场特定的斗争——它已经变得太普遍了。 - Sven Marnach
8个回答

195

在Python 2中调用itertools.imap()或在Python 3中调用map()的函数可以被等价的生成器表达式所替代:

sum(1 for dummy in it)

这也使用了惰性生成器,因此它避免在内存中材料化所有迭代器元素的完整列表。


4
你可以使用len(list(it))——如果元素是唯一的,那么使用len(set(it))可以节省一个字符。 - F1Rumors
43
在大多数情况下,使用len(list(it))是可以的。但是当你有一个产生大量元素的惰性迭代器时,你不想在同一时间将它们全部存储在内存中进行计数,这种情况下使用此答案中的代码可以避免这个问题。 - Sven Marnach
同意:作为答案,它的前提是“最短代码”比“最低内存”更重要。 - F1Rumors
7
正如在这个帖子中所建议的,sum(1 for _ in generator)避免填充内存。 - Sylvain

48

在可迭代对象很长的情况下比 sum(1 for i 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 代码中执行循环(dequecountzip 都是用 C 实现的)。避免每次循环的字节码执行通常是 CPython 性能的关键。

想要比较性能,找到公平的测试用例非常困难(list 使用 __length_hint__ 欺骗,而这在任意输入可迭代对象中都不太可能存在;不提供 __length_hint__itertools 函数经常具有特殊的操作模式,当每个循环返回的值在请求下一个值之前得到释放/释放时会更快,可以使用带有 maxlen=0deque 实现)。我使用的测试用例是创建一个生成器函数,该函数将采用输入并返回缺少特殊 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 ns,相当于简单的sum方法的10%),但是当你将输入长度增加到2时,成本就变得相当,而在长度为30左右时,初始开销与真正的工作相比微不足道;使用sum方法需要的时间大约多出50%。
基本上,如果内存使用很重要或输入没有有界大小并且您更关心速度而不是简洁性,请使用此解决方案。如果输入受到限制且较小,则len(list(it))可能最佳,如果它们是无界的,则使用sum(1 for _ in it)可以使代码更简洁。

1
这正是more_itertools.ilen中的实现。 - rsalmei
5
看起来他们在八个月前采用了我的实现(https://github.com/erikrose/more-itertools/commit/5161c3455375492ce9dfb4ad32a2e5ee1506f966)。技术上说,它略微慢一些(因为它们通过关键字传递了“maxlen”,而不是按位置),但这是固定的开销,在大O运行时间上没有意义。无论如何,他们抄袭了我(我在3.5年前发布了这个内容),而不是相反的情况。 :-) - ShadowRanger
@user650654:部分困难在于,在测试用例中,您需要多次运行它,而不必支付重复创建迭代器的成本(这将隐藏性能差异)。在现实世界中,您不关心廉价制作虚假输入;您有输入,需要计算一次,然后完成(有很多东西会像我的测试用例输入一样行事,只是昂贵的重新创建)。话虽如此,我同意特定情况需要不同的方法;这就是我最后一段所说的。 - ShadowRanger
@ShadowRanger,我非常怀疑他们是在抄袭你,更有可能是在抄袭迭代工具消耗配方,该配方自2009年以来一直存在于Python官方库中。 - wim
@wim:consume食谱不是它的复制部分(实际上,我稍微调整了我实现的consume子集的方式,但它几乎没有什么原创性)。被复制的是使用zipitertools.count以一种避免存储zip结果的方式(从而启用重用tupel而不是分配新tupelzip优化)的consumemore-itertools#230中提出的实现几乎是从此代码在其开放时的近似复制。 - ShadowRanger
显示剩余2条评论

9
一个简短的方法是:
def ilen(it):
    return len(list(it))

请注意,如果您正在生成大量元素(例如数万个或更多),则将它们放入列表中可能会成为性能问题。但是,在大多数情况下,这只是一种简单的表达方式,性能并不重要。

1
我曾考虑过这个,但性能确实很重要,因为我经常处理大文本文件。 - Fred Foo
9
只要内存充足,这个解决方案在性能方面实际上相当不错,因为它将循环操作完全放在C代码中执行--无论如何都需要生成所有对象。即使对于大型迭代器,只要一切都适合内存,这比使用sum(1 for i in it)更快。 - Sven Marnach
1
实际上很奇怪,len(it) 不起作用。 sum(it)max(it)min(it) 等都按预期工作,只有 len(it) 不行。 - Kai Petzke
5
it是一个迭代器时,没有保证它知道自己的长度而不运行它。最明显的例子是文件对象;它们的长度基于文件中的行数,但是行的长度是可变的,知道有多少行的唯一方法就是读取整个文件并计算换行符的数量。len()旨在成为一种廉价的O(1)操作; 当你要求它们的长度时,你想让它静默地读取多GB的文件吗?summaxmin是必须读取它们的数据的聚合函数,而len则不是。 - ShadowRanger
@ShadowRanger:一个选择是添加一个O(n)的聚合count(it) - Kai Petzke

7

more_itertools是一个第三方库,它实现了一个名为ilen的工具。使用pip install more_itertools进行安装。

import more_itertools as mit


mit.ilen(x for x in range(10))
# 10

5
len(list(it))

不过,如果它是一个无限生成器,它会挂起。


3
无论如何都无法数清一个无限生成器中的项。 - LarsH

2
我喜欢使用 cardinality 包来实现这个功能,它非常轻量级,并尝试根据可迭代对象选择最快的实现方法。
用法:
>>> import cardinality
>>> cardinality.count([1, 2, 3])
3
>>> cardinality.count(i for i in range(500))
500
>>> def gen():
...     yield 'hello'
...     yield 'world'
>>> cardinality.count(gen())
2

不错!还保持得好吗? - jtlz2
好!还保持得不错吗? - undefined
@jtlz2 或许不会,但考虑到其内容,变化可能不大。 - Erwin Mayer

2
这是我可以选择的两个方案之一:
最初的回答:这将是我的选择之一。
print(len([*gen]))
print(len(list(gen)))

1
第一种选择似乎没有什么意义,因为它只会在将生成器转换为list之前增加扩展整个生成器的开销。也就是说,除非您能解释第一种选择有什么优点,否则此答案不会增加任何价值。 - jpmc26
@jpmc26,楼主问的是计算生成器中元素数量的最短方式。 len([*gen])非常简短。例如,在“代码高尔夫”比赛中,这将非常有价值。但是,我同意您的看法,在大多数用例中,这种解决方案不是最优的。 - ruancomelli
实际上,标题中写的是“最短的方法”,但问题的主体却有所不同。len([*gen]) 对我来说感觉不太符合 Python 的风格。 - ruancomelli
@jpmc26 Python实际上没有"转换"的概念;通过将生成器传递给list函数来创建一个列表,与使用[*gen]的技巧是相同的。例如,在我的机器上,python -m timeit "len([*(_ for _ in range(100))])"显示出比python -m timeit "len(list(_ for _ in range(100)))"稍微更好的性能(可能是因为不需要在全局命名空间中查找list这个名称)。 - Karl Knechtel

0

如果您想在其他地方使用可迭代对象并知道消耗了多少元素,可以创建一个简单的包装类:

from collections.abc import Iterable, Iterator
from typing import Generic, TypeVar

_T = TypeVar("_T")


class IterCounter(Generic[_T]):
    """Iterator that keeps count of the consumed elements"""

    def __init__(self, iterable: Iterable[_T]) -> None:
        self._iterator = iter(iterable)
        self.count = 0

    def __iter__(self) -> Iterator[_T]:
        return self

    def __next__(self) -> _T:
        element = next(self._iterator)
        self.count += 1
        return element


counter = IterCounter(range(5))

print(counter.count)  # 0

print(list(counter))  # [0, 1, 2, 3, 4]

print(counter.count)  # 5

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接