在Python中获取迭代器中元素的数量

213

在Python中,一般情况下有没有一种高效的方法来知道迭代器中有多少元素,而不需要遍历每个元素并计数?


20个回答

315

以下代码应该可以正常工作:

>>> iter = (i for i in range(50))
>>> sum(1 for _ in iter)
50

虽然它要遍历每个项并计数,但这是最快的方法。

当迭代器没有任何项时,它也可以工作:

>>> sum(1 for _ in range(0))
0

当然,如果输入是无限的,它将永远运行,所以请记住迭代器可以是无限的:

>>> sum(1 for _ in itertools.count())
[nothing happens, forever]

此外,请注意 这样做会耗尽迭代器,后续尝试使用它将看到没有元素的情况。这是 Python 迭代器设计的不可避免的后果。如果您想保留这些元素,则需要将它们存储在列表或其他数据结构中。


24
在我看来,这似乎正是 OP 不想做的事情:遍历迭代器并计数。 - Adam Crossland
47
这是一种在可迭代对象中高效计数元素的方式。 - Captain Lepton
13
虽然这不是OP想要的,考虑到他的问题没有答案,这个答案避免了列表实例化,并且在常数上经验性地比上面列出的reduce方法更快。 - Phillip Nordwall
6
无法帮助:下划线“_”是指Perl中的“$_”吗? :) - Alois Mahdal
22
在Python中,惯例上使用名称“_”表示一个虚拟变量,其值并不重要。请注意,这只是翻译,没有任何解释或其他内容返回。 - Taymon
显示剩余5条评论

136
不可能。
例子:
import random

def gen(n):
    for i in xrange(n):
        if random.randint(0, 1) == 0:
            yield i

iterator = gen(10)

iterator的长度在通过迭代器迭代之前是未知的。


18
另外,def gen(): yield random.randint(0, 1) 是无限的,因此您将永远无法通过迭代来找到其长度。 - tgray
2
那么,为了验证显而易见的事实:获取迭代器的“大小”的最佳方法就是计算您已经进行迭代的次数,对吗?在这种情况下,它将是numIters = 0; while iterator: numIters +=1 - Mike Williamson
有趣,那么这就是停机问题。 - Akababa
1
@tgray 这不是无限的,它只有一个元素。 - Kelly Bundy

95
不,任何方法都需要您解决每个结果。您可以这样做:
iter_length = len(list(iterable))

但在无限迭代器上运行它显然永远不会返回。它还将消耗该迭代器,并且如果您想使用其内容,则需要重置它。

告诉我们你试图解决的实际问题可能有助于我们找到更好的方法来完成你的实际目标。

编辑:使用list()将一次性将整个可迭代对象读入内存,这可能是不希望的。另一种方法是执行

sum(1 for _ in iterable)

正如另一个人发布的那样。这将避免将其保留在内存中。


问题在于我正在使用“pysam”读取一个有数百万条目的文件。Pysam返回一个迭代器。为了计算某个数量,我需要知道文件中有多少读取,但我不需要读取每一个...这就是问题所在。 - user248237
9
我不是 pysam 的用户,但它可能是“惰性”读取文件。这是有道理的,因为您不希望在内存中保存大文件。因此,如果您必须在迭代之前知道记录数,则唯一的方法是创建两个迭代器,并使用第一个计算元素数量,第二个读取文件。顺便说一句,不要使用 len(list(iterable)),它会将所有数据加载到内存中。 您可以使用:reduce(lambda x, _: x+1, iterable, 0)。编辑:Zonda333 的代码也很好,使用 sum 函数。 - Tomasz Wysocki
1
@user248237:为什么你说你需要知道有多少条目可用来计算某个数量?你可以只读取固定数量的条目,并处理当少于该固定数量时的情况(使用iterslice非常简单)。你必须读取所有条目的另一个原因是什么? - kriss
1
@Tomasz 注意,reduce函数已被弃用,并且在Python 3及以上版本中将不再存在。 - Wilduck
8
它并没有消失,只是移动到了functools.reduce中。 - Daenyth
显示剩余2条评论

45
你不能(除非特定迭代器类型实现了某些特定的方法),否则无法确定迭代器的类型。通常情况下,只有通过消费迭代器才能计算其项数。其中可能最有效的一种方法:
import itertools
from collections import deque

def count_iter_items(iterable):
    """
    Consume an iterable not reading it into memory; return the number of items.
    """
    counter = itertools.count()
    deque(itertools.izip(iterable, counter), maxlen=0)  # (consume at C speed)
    return next(counter)

(对于Python 3.x,请使用zip替换itertools.izip)。


4
与“sum(1 for _ in iterator)”相比,这个方法的速度快了将近一倍。 - augustomen
1
更准确地说,它通过将每个项读入内存并立即丢弃来消耗可迭代对象。 - Rockallite
需要注意的是(我之前忽略了):zip 函数的参数顺序很重要,如果你传递 zip(counter, iterable),实际上会得到比可迭代对象数量多 1 的结果! - Kye W Shi
非常好的答案。会为它提供赏金。 - Reut Sharabani
我认为这应该被视为最佳答案。谢谢! - Alessandro Suglia

20

有点像。你可以检查__length_hint__方法,但要注意(至少到Python 3.4为止,正如gsnedders所指出的),它是一个未记录的实现细节线程中的以下消息),它可能会消失,或者召唤鼻涕鬼。

否则没有。迭代器只是一种仅公开next()方法的对象。您可以根据需要调用它多次,它们可能会最终引发StopIteration异常。幸运的是,这种行为大部分时间对编码人员来说是透明的。 :)


5
这已不再是一个问题,自 PEP 424 和 Python 3.4之后。 __length_hint__ 现已有文件记录,但它仅是提示,并不能保证准确无误。 - gsnedders

14

一个快速基准测试:

import collections
import itertools

def count_iter_items(iterable):
    counter = itertools.count()
    collections.deque(itertools.izip(iterable, counter), maxlen=0)
    return next(counter)

def count_lencheck(iterable):
    if hasattr(iterable, '__len__'):
        return len(iterable)

    d = collections.deque(enumerate(iterable, 1), maxlen=1)
    return d[0][0] if d else 0

def count_sum(iterable):           
    return sum(1 for _ in iterable)

iter = lambda y: (x for x in xrange(y))

%timeit count_iter_items(iter(1000))
%timeit count_lencheck(iter(1000))
%timeit count_sum(iter(1000))

结果:

10000 loops, best of 3: 37.2 µs per loop
10000 loops, best of 3: 47.6 µs per loop
10000 loops, best of 3: 61 µs per loop

也就是说,使用简单的count_iter_items方法是正确的选择。

调整后适用于Python3:

61.9 µs ± 275 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
74.4 µs ± 190 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
82.6 µs ± 164 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

注意:此测试基于Python2。 - normanius

12

因此,对于那些想要了解讨论摘要的人。使用以下方法计算一个长度为5000万的生成器表达式的最终得分:

  • len(list(gen))
  • len([_ for _ in gen])
  • sum(1 for _ in gen)
  • ilen(gen) (来自more_itertools),
  • reduce(lambda c, i: c + 1, gen, 0)

按执行性能(包括内存消耗)排序,将让您惊讶:

```

1:test_list.py:8:0.492 KiB

gen = (i for i in data*1000); t0 = monotonic(); len(list(gen))

('列表,第二', 1.9684218849870376)

2:test_list_compr.py:8:0.867千字节

gen = (i for i in data*1000); t0 = monotonic(); len([i for i in gen])

('列表推导式,第二部分', 2.5885991149989422)

3:test_sum.py:8:0.859 KiB

gen = (i for i in data*1000); t0 = monotonic(); sum(1 for i in gen); t1 = monotonic()

('合计,秒', 3.441088170016883)

4:more_itertools/more.py:413:1.266 KiB

d = deque(enumerate(iterable, 1), maxlen=1)

test_ilen.py:10: 0.875 KiB
gen = (i for i in data*1000); t0 = monotonic(); ilen(gen)

('ilen,sec',9.812256851990242)

5:test_reduce.py:8:0.859 KiB

gen = (i for i in data*1000); t0 = monotonic(); reduce(lambda counter, i: counter + 1, gen, 0)

('reduce, sec', 13.44)

所以,len(list(gen)) 是最常用且内存消耗最少的。


1
你是如何测量内存消耗的? - normanius
3
你能解释一下为什么 len(list(gen)) 消耗的内存应该比基于 reduce 的方法更少吗?前者创建了一个涉及内存分配的新 list,而后者不应该这样做。因此,我期望后者更加内存高效。此外,内存消耗将取决于元素类型。 - normanius
FYI:我可以在 MacBookPro 上重现 Python 3.6.8 的情况,方法1在运行时间方面优于其他方法(我跳过了方法4)。 - normanius
2
len(tuple(iterable)) 可能会更加高效:Nelson Minar的文章 - VMAtm
1
@normanius 这里也没有办法从一个"长度为5000万的生成器表达式"中创建一个只占用几百字节的列表。显然是错误的。回答不好。 - Kelly Bundy
显示剩余2条评论

12

我喜欢使用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

实际的count()实现如下:

def count(iterable):
    if hasattr(iterable, '__len__'):
        return len(iterable)

    d = collections.deque(enumerate(iterable, 1), maxlen=1)
    return d[0][0] if d else 0

如果你使用那个函数,我认为你仍然可以迭代这个迭代器,对吗? - jcollum
@jcollum 看一下这个答案末尾给出的 count 代码,如果可迭代对象没有 .__len__ 属性,它将被消耗。如果它是一个“一次性”对象,比如生成器,在调用 count 后它将为空。 - Stef

9

迭代器只是一个对象,它有一个指针指向缓冲区或流中要读取的下一个对象,就像LinkedList一样,你不知道有多少东西,直到你通过迭代它们。迭代器旨在高效,因为它们只是通过引用告诉你下一个是什么,而不是使用索引(但正如你所看到的,你失去了查看下一个条目数量的能力)。


2
迭代器与链表完全不同。从迭代器返回的对象并不指向下一个对象,这些对象也不一定存储在内存中。相反,它可以基于任何内部逻辑(可能是但不一定是基于存储列表)依次产生对象。 - Tom
1
@Tom,我使用LinkedList作为示例,主要是因为你不知道有多少元素,因为你只知道下一个元素(如果有的话)。如果我的措辞有些不当或者让你误解了它们是一样的,请原谅。 - Jesus Ramos

8
关于你的原始问题,答案仍然是在Python中通常没有办法知道迭代器的长度。
考虑到你的问题是与pysam库的应用相关的,我可以给出一个更具体的答案:我是PySAM的贡献者,确定的答案是SAM/BAM文件不提供对齐读取的精确计数。从BAM索引文件中也不容易获取这些信息。最好的方法是通过使用读取一定数量对齐位置后的文件指针位置,并根据文件的总大小进行外推来估算大约的对齐数。这足以实现进度条,但不能以恒定时间计数对齐。

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