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

一般来说,有没有一种有效的方法可以知道Python中的迭代器中有多少个元素,而不用遍历每个元素并计数?

201032 次浏览

不,任何方法都需要解析每个结果。你可以这样做

iter_length = len(list(iterable))

但是在无限迭代器上运行它当然不会返回。它还将消耗迭代器,如果你想使用内容,它将需要重置。

告诉我们你想要解决的真正问题可能会帮助我们找到更好的方法来实现你的实际目标。

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

sum(1 for _ in iterable)

正如另一个人发布的那样。这样可以避免把它保存在记忆中。

这段代码应该工作:

>>> 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迭代器设计的一个不可避免的结果。如果你想保留元素,你就必须把它们存储在一个列表或其他东西中。

不。这是不可能的。

例子:

import random


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


iterator = gen(10)

iterator的长度是未知的,直到你遍历它。

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

在计算机上有两种方法来获取“某物”的长度。

第一种方法是存储一个计数——这需要任何接触文件/数据的东西来修改它(或者一个只公开接口的类——但归根结底是一样的)。

另一种方法是遍历它并计算它有多大。

有点。你可以检查__length_hint__方法,但要注意(至少在Python 3.4之前,正如gsnedders帮助指出的那样)它是一个未记录的实现细节 (线程中的消息),它很可能消失或召唤鼻子恶魔。

否则,没有。迭代器只是一个只公开next()方法的对象。你可以根据需要多次调用它,它们最终可能会引发StopIteration,也可能不会。幸运的是,大多数时候这种行为对编码器来说是透明的。:)

通常的做法是将这类信息放在文件头中,并让pysam允许您访问这些信息。我不知道格式,但是你检查过API了吗?

正如其他人所说,你不能从迭代器中知道长度。

关于你最初的问题,答案仍然是,在Python中通常没有办法知道迭代器的长度。

考虑到你的问题是由pysam库的应用程序驱动的,我可以给出一个更具体的答案:我是pysam的贡献者,明确的答案是SAM/BAM文件不提供精确的对齐读取计数。从BAM索引文件中也不容易获得这些信息。最好的方法是在读取一些对齐并根据文件的总大小进行外推之后,通过使用文件指针的位置来估计对齐的大致数量。这足以实现一个进度条,但不能实现在常数时间内计算对齐的方法。

不能(除非特定迭代器的类型实现了一些特定的方法,使之成为可能)。

通常,只能通过使用迭代器来计数迭代器项。最有效的方法之一:

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)。

这违背了迭代器的定义,迭代器是一个指向对象的指针,加上如何到达下一个对象的信息。

迭代器不知道在终止之前它还能迭代多少次。这个可以是无穷,所以无穷可能是你的答案。

def count_iter(iter):
sum = 0
for _ in iter: sum += 1
return sum

我喜欢基数包,它是非常轻量级的,并尝试使用最快的实现,这取决于可迭代对象。

用法:

>>> 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

一个简单的基准:

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)

虽然一般情况下不可能做到要求的内容,但计算在上迭代了多少项通常仍然是有用的。为此,你可以使用jaraco.itertools.Counter或类似的方法。下面是一个使用python3和rwt加载包的例子。

$ rwt -q jaraco.itertools -- -q
>>> import jaraco.itertools
>>> items = jaraco.itertools.Counter(range(100))
>>> _ = list(counted)
>>> items.count
100
>>> import random
>>> def gen(n):
...     for i in range(n):
...         if random.randint(0, 1) == 0:
...             yield i
...
>>> items = jaraco.itertools.Counter(gen(100))
>>> _ = list(counted)
>>> items.count
48

所以,对于那些想知道讨论总结的人。使用以下方法计算5000万长度生成器表达式的最终最高分:

  • len(list(gen)),
  • len([_ for _ in gen]),
  • sum(1 for _ in gen),
  • ilen(gen) (from more_itertool),
  • 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))

('list, sec', 1.9684218849870376)

2: test_list_compr.py: 8:0.867 KiB

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

('list_compr, sec', 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()

('sum, sec', 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.436614598002052) ' ' ' < / p >

因此,len(list(gen))是使用频率最高且占用内存较少的

假设,您希望在不遍历的情况下计算项的数量,这样迭代器就不会耗尽,稍后可以再次使用它。这可以通过copydeepcopy实现

import copy


def get_iter_len(iterator):
return sum(1 for _ in copy.copy(iterator))


###############################################


iterator = range(0, 10)
print(get_iter_len(iterator))


if len(tuple(iterator)) > 1:
print("Finding the length did not exhaust the iterator!")
else:
print("oh no! it's all gone")

输出为"Finding the length did not exhaust the iterator!"

可选地(并且不明智地),你可以像下面这样隐藏内置的len函数:

import copy


def len(obj, *, len=len):
try:
if hasattr(obj, "__len__"):
r = len(obj)
elif hasattr(obj, "__next__"):
r = sum(1 for _ in copy.copy(obj))
else:
r = len(obj)
finally:
pass
return r

一个简单的方法是使用set()内置函数:

iter = zip([1,2,3],['a','b','c'])
print(len(set(iter)) # set(iter) = {(1, 'a'), (2, 'b'), (3, 'c')}
Out[45]: 3

iter = range(1,10)
print(len(set(iter)) # set(iter) = {1, 2, 3, 4, 5, 6, 7, 8, 9}
Out[47]: 9

我认为有必要建立一个微观基准来比较这里提到的不同方法的运行时间。

免责声明:我使用simple_benchmark(我编写的库)进行基准测试,还包括iteration_utilities.count_items(我编写的第三方库中的函数)。

为了提供更有区别的结果,我做了两个基准测试,一个只包括不构建中间容器的方法,另一个包括以下方法:

from simple_benchmark import BenchmarkBuilder
import more_itertools as mi
import iteration_utilities as iu


b1 = BenchmarkBuilder()
b2 = BenchmarkBuilder()


@b1.add_function()
@b2.add_function()
def summation(it):
return sum(1 for _ in it)


@b1.add_function()
def len_list(it):
return len(list(it))


@b1.add_function()
def len_listcomp(it):
return len([_ for _ in it])


@b1.add_function()
@b2.add_function()
def more_itertools_ilen(it):
return mi.ilen(it)


@b1.add_function()
@b2.add_function()
def iteration_utilities_count_items(it):
return iu.count_items(it)


@b1.add_arguments('length')
@b2.add_arguments('length')
def argument_provider():
for exp in range(2, 18):
size = 2**exp
yield size, [0]*size


r1 = b1.run()
r2 = b2.run()


import matplotlib.pyplot as plt


f, (ax1, ax2) = plt.subplots(2, 1, sharex=True, figsize=[15, 18])
r1.plot(ax=ax2)
r2.plot(ax=ax1)
plt.savefig('result.png')

结果如下:

enter image description here

它使用log-log-轴,以便可以检查所有范围(小值,大值)。由于这些图是用于定性比较的,因此实际值并不太有趣。一般来说,y轴(垂直)表示时间,x轴(水平)表示输入“可迭代”中的元素数量。纵轴上越低表示越快。

上图显示了不使用中间列表的方法。这表明iteration_utilities方法是最快的,其次是more_itertools,最慢的是使用sum(1 for _ in iterator)

下面的图表还包括在中间列表上使用len()的方法,一次使用list,一次使用列表理解。使用len(list)的方法在这里是最快的,但与iteration_utilities方法的区别几乎可以忽略不计。使用理解的方法比直接使用list的方法要慢得多。

总结

这里提到的任何方法都依赖于输入的长度,并且迭代遍历可迭代对象中的每个元素。没有迭代就无法获得长度(即使迭代是隐藏的)。

如果你不想要第三方扩展,那么使用len(list(iterable))绝对是测试过的方法中最快的方法,但是它会生成一个中间列表,其中可以使用更多的内存。

如果你不介意额外的包,那么iteration_utilities.count_items几乎和len(list(...))函数一样快,但不需要额外的内存。

但是需要注意的是,微基准测试使用列表作为输入。基准测试的结果可能不同,这取决于您想要获取的迭代对象的长度。我还用range和一个简单的生成器表达式进行了测试,趋势非常相似,但我不能排除时间不会因输入类型而改变。

这是从理论上讲不可能的:这实际上是停止的问题

证明

相反,假设可以使用函数len(g)来确定任何生成器g的长度(或无限长度)。

对于任意程序P,现在让我们将P转换为生成器g(P): 对于P中的每个返回点或退出点,产生一个值而不是返回它

如果len(g(P)) == infinity, P不停止。

这解决了暂停问题,这是不可能的,见维基百科。矛盾。


因此,如果不对泛型生成器进行迭代(==实际运行整个程序),就不可能对其元素进行计数。

更具体地说,考虑

def g():
while True:
yield "more?"

长度是无限的。这样的发生器有无穷多个。