发电机输出的长度

179

Python提供了一个很好的方法来获取迫切可迭代对象的长度,即使用len(x)。但是对于由生成器推导式和函数表示的惰性可迭代对象,我找不到类似的东西。当然,编写类似下面的代码也不难:

def iterlen(x):
  n = 0
  try:
    while True:
      next(x)
      n += 1
  except StopIteration: pass
  return n

但我感觉我在重新发明轮子。

(当我在打这个函数时,有一个想法突然袭来:也许真的没有这样的函数,因为它“破坏”了它的参数。不过这对我的情况不是问题)。

P.S.:关于第一个答案 - 是的,类似 len(list(x)) 这样的东西也可以工作,但会大大增加内存使用量。

P.P.S.:已重新检查... 请忽略 P.S.,看起来我在尝试那个时犯了个错误,它能正常工作。对造成的麻烦表示抱歉。


建议将标题更改为“仅生成器输出的长度 - 迭代项可以被丢弃”。否则,此问题会与另一个问题混淆。 - Bob Stein
6
重新实现自行车 - 这几乎就像重新发明轮子,只不过是程序员说的。 - Cullub
9个回答

308
最简单的方法可能只是使用sum(1 for _ in gen),其中gen是您的生成器。

13
虽然我很喜欢这个解决方案,但主要的缺点在于通过阅读代码并不明显你试图实现什么。如果我在别人的代码中看到这行,我会停下来思考“他为什么要在这里求和?”- 除非我以前见过这个“技巧”。 - Charles Salvia
26
“@CharlesSalvia,我个人认为这就是评论的作用。我想说得是,获得生成器的长度值得一条评论。” - Niels Bom
48
另一个主要的缺点是为了得到长度,它会耗尽生成器,这通常会打败使用生成器的初衷。 - ely
6
注意,这种方法可能会占用较少的内存,但似乎比将其转换为列表更慢。 - lumbric
7
或许,len(list(gen)) 更加清晰明了,并且根据以下答案所述,更为高效。 - Anish Gupta
1
这清除了生成器,我无法使用它,因为它已经被清空。 - alper

103

以下是对那次讨论总结的概述。对于使用50百万长度生成器表达式进行计数,最终的最高得分来自于以下方法:

  • 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))
('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)

因此,len(list(gen)) 是最常见且占用内存较少的方法。


6
我个人发现 len 列表方法比 sum 方法慢两倍。所以结果可能会有所不同。 - steveayre
1
FYI,more_itertools 基于我改进的使用 maxlen=0deque 触发超优化输入消耗的代码版本改善了他们的实现;当 list 不会增长到导致交换抖动时,它仍然比 len(list(gen)) 慢,但只需要大约 50% 的时间,对于有意义大小的输入,它需要的时间大约是 sum(1 for _ in gen) 的一半。 - ShadowRanger

43

没有这样的方法,因为在一般情况下你无法做到 - 如果你有一个懒惰的无限生成器会怎样呢?例如:

def fib():
    a, b = 0, 1
    while True:
        a, b = b, a + b
        yield a

这段代码永远不会停止,但它将生成斐波那契数列。通过调用next()可以获得任意数量的斐波那契数。

如果你真的需要知道有多少个元素,那么无论如何你都不能线性迭代它们一次,因此只需使用其他数据结构,例如常规列表。


104
我不确定我是否相信/接受这个解释。即使那个可迭代对象是无限的,sum函数也可以接受它,因此“你不能在一般情况下做到这一点”和你不能在一般情况下使用len函数一样都是不正确的。也许更可能的原因是人们“期望”len函数是O(1)的,然而对于一般的可迭代对象,它并不是O(1)。 - Steve Jessop
14
常规列表占用更多的内存,而这正是原帖作者想要避免的。 - akaihola
@Steve Jessop:如果你有很多对象,通常情况下计数显然是O(n)的。如果在收集它们的同时跟踪对象数量,则为O(1)。对于许多特殊情况,您可能能够利用对象的性质来构建更好的算法(例如通过称重来计算大米粒数)。如果对象按顺序排列在内存中,则可以使用内存消耗来计数对象。但是对于生成器而言,通常没有这样的方法。 - lumbric
1
我有一个过滤列表,预计会有大约20亿个元素。我不能只使用常规列表;我“需要”使用生成器。现在,由于这些元素的来源方式,我实际上可以相当高效地运行它们--我只是不能存储它们,因为我没有40GB的内存。这个答案对我来说完全没有用。 - anon

19
def count(iter):
    return sum(1 for _ in iter)

或者更好的做法:

def count(iter):
    try:
        return len(iter)
    except TypeError:
        return sum(1 for _ in iter)
如果它不可迭代,它会抛出一个TypeError异常。
或者,如果你想在生成器中计数特定的内容:
def count(iter, key=None):
    if key:
        if callable(key):
            return sum(bool(key(x)) for x in iter)
        return sum(x == key for x in iter)
    try:
        return len(iter)
    except TypeError:
        return sum(1 for _ in iter)

8
你可以使用enumerate()循环遍历生成的数据流,然后返回最后一个数字--即项目数量。
我尝试使用itertools.count()和itertools.izip(),但没有成功。这是我想到的最好/最短的答案:
#!/usr/bin/python

import itertools

def func():
    for i in 'yummy beer':
        yield i

def icount(ifunc):
    size = -1 # for the case of an empty iterator
    for size, _ in enumerate(ifunc()):
        pass
    return size + 1

print list(func())
print 'icount', icount(func)

# ['y', 'u', 'm', 'm', 'y', ' ', 'b', 'e', 'e', 'r']
# icount 10

Kamil Kisiel的解决方案要好得多:

def count_iterable(i):
    return sum(1 for e in i)

8

根据定义,只有一部分生成器会在特定数量的参数(具有预定义的长度)后返回,即使如此,仅有这些有限的生成器子集具有可预测的结束(访问生成器可能会产生副作用,这可能会提前停止生成器)。

如果您希望为您的生成器实现长度方法,则必须首先定义您认为的“长度”(它是元素的总数还是剩余元素的数量?),然后将您的生成器包装在一个类中。以下是一个示例:

class MyFib(object):
    """
    A class iterator that iterates through values of the
    Fibonacci sequence, until, optionally, a maximum length is reached.
    """

    def __init__(self, length):
        self._length = length
        self._i = 0

     def __iter__(self):
        a, b = 0, 1
        while not self._length or self._i < self._length:
            a, b = b, a + b
            self._i += 1
            yield a

    def __len__(self):
        "This method returns the total number of elements"
        if self._length:
            return self._length
        else:
            raise NotImplementedError("Infinite sequence has no length")
            # or simply return None / 0 depending
            # on implementation

以下是如何使用它的方法:

In [151]: mf = MyFib(20)

In [152]: len(mf)
Out[152]: 20

In [153]: l = [n for n in mf]

In [154]: len(l)
Out[154]: 20

In [155]: l
Out[155]: 
[1,
 1,
 2,
...
6765]


In [156]: mf0 = MyFib(0)

In [157]: len(mf0)
---------------------------------------------------------------------------
NotImplementedError                       Traceback (most recent call last)
<ipython-input-157-2e89b32ad3e4> in <module>()
----> 1 len(mf0)

/tmp/ipython_edit_TWcV1I.py in __len__(self)
     22             return self._length
     23         else:
---> 24             raise NotImplementedError
     25             # or simply return None / 0 depending
     26             # on implementation

NotImplementedError: 

In [158]: g = iter(mf0)

In [159]: l0 = [g.next(), g.next(), g.next()]

In [160]: l0
Out[160]: [1, 1, 2]

这是一种实现迭代器/生成器的解决方案,它可以为len()函数提供长度。您可以通过实现自己的__iter__方法来从这个类派生出您的生成器,并且如果需要,您还可以实现自己的__init____len__方法。例如,在某些ORM类型对象中,此模式可能非常有用,您可以执行SQL查询,然后使用游标(通过迭代器)逐行获取结果,并且__len__方法从实际的SQL查询中获取计数。 - sleblanc

6

如果需要一个内存高效的纯函数解决方案,可以使用reduce(function, iterable[, initializer])函数:

>>> iter = "This string has 30 characters."
>>> reduce(lambda acc, e: acc + 1, iter, 0)
30

你的时间不准确是因为迭代器被消耗了。只有第一次尝试len(list(iter))时实际上会遍历任何值,其他所有的尝试都在计算一个长度为零的序列。在我的测试中,reducelen(list())enumeratesum要慢。 - Blckknght

5

尝试使用more_itertools包进行简单的解决方案。例如:

>>> import more_itertools

>>> it = iter("abcde")                                         # sample generator
>>> it
<str_iterator at 0x4ab3630>

>>> more_itertools.ilen(it)
5

请参见此篇文章,了解另一个应用实例。


1
这是一个小技巧,但如果你真的想让 len 在一般可迭代对象上工作(以消耗它的方式),你可以创建自己的 len 版本。

len 函数本质上等价于以下内容(尽管实现通常提供一些优化,以避免额外的查找):

def len(iterable):
    return iterable.__len__()

因此,我们可以定义我们的new_len来尝试这样做,如果__len__不存在,则通过消耗可迭代对象来计算元素数量:
def new_len(iterable):
    try:
      return iterable.__len__()
    except AttributeError:
      return sum(1 for _ in iterable)

上述代码适用于Python 2/3,并且(据我所知)应该涵盖了各种可迭代类型。

3
覆盖内置函数会掩盖原始行为,这会导致难以(或不可能)调试的代码。你应该为那个不可命名的函数使用不同的名称-len... - wouter bolsterlee

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