【问题标题】:Length of generator output [duplicate]发电机输出长度[重复]
【发布时间】:2010-09-28 10:16:18
【问题描述】:

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.,似乎我在尝试时犯了一个错误,它工作正常。给您添麻烦了。

【问题讨论】:

  • 建议将标题更改为 仅生成器输出的长度 -- 可以丢弃迭代的项目。否则这个问题会与another 混淆。
  • reimplementing a bicycle - 几乎就像重新发明轮子一样,只有程序员这么说。

标签: python generator iterable


【解决方案1】:

因此,对于那些想知道该讨论摘要的人。使用以下方法计算 5000 万长的生成器表达式的最终最高分:

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

('列表,秒',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)

('减少,秒',13.436614598002052) ```

所以,len(list(gen)) 是最频繁且更少的内存消耗

【讨论】:

【解决方案2】:

这是一个 hack,但如果你真的想让 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,并且(据我所知)应该涵盖所有可能的迭代类型。

【讨论】:

  • 覆盖内置函数将掩盖原始行为,从而导致难以(或不可能)调试代码。你真的应该为-function-that-must-not-be-named-len 使用不同的名称...
【解决方案3】:

尝试more_itertools 包以获得简单的解决方案。示例:

>>> import more_itertools

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

>>> more_itertools.ilen(it)
5

另一个应用示例请参见this post

【讨论】:

    【解决方案4】:

    没有,因为在一般情况下您无法做到 - 如果您有一个惰性无限生成器怎么办?例如:

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

    这永远不会终止,但会生成斐波那契数。拨打next(),您可以获得任意数量的斐波那契数。

    如果您确实需要知道项目的数量,那么无论如何您都无法线性地遍历它们一次,因此只需使用不同的数据结构,例如常规列表。

    【讨论】:

    • 我不确定我是否相信/接受这个解释。 sum 接受一个可迭代对象,即使该可迭代对象可能是无限的,因此“在一般情况下你不能这样做”,就像在一般情况下你不能做 len 一样。也许更可能的理由是人们“期望”len 为 O(1),这不是一般可迭代的?
    • 常规列表会消耗更多内存,这是 OP 想要避免的。
    • @Steve Jessop:如果你有很多对象,通常计算它们显然是 O(n)。如果您在收集对象时跟踪对象的数量,则为 O(1)。对于许多特殊情况,您可能可以使用对象性质来组成更好的算法(即通过称重来计算米粒)。如果对象在内存中排列,则内存消耗可用于对对象进行计数。但是对于生成器,一般没有这样的方法。
    • 我有一个过滤列表,我希望它的元素数量约为 2000000000 个。我不能只使用常规列表;我需要使用生成器。现在,由于这些元素的来源,我实际上可以非常高效地运行它们——我只是无法存储它们,因为我没有 40 演出的内存。这个答案对我来说完全没用。
    【解决方案5】:

    可以使用 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)
    

    【讨论】:

      【解决方案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 谢谢,已更正。
      【解决方案7】:

      根据定义,只有一部分生成器会在一定数量的参数之后返回(具有预定义的长度),即使这样,这些有限生成器中也只有一部分具有可预测的结束(访问生成器可以有边- 可以更早停止生成器的效果)。

      如果你想为你的生成器实现长度方法,你必须首先定义你认为的“长度”(它是元素的总数吗?剩余元素的数量?),然后将你的生成器包装在一个类中.这是一个例子:

      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 查询中获取计数。
      【解决方案8】:

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

      【讨论】:

      • 尽管我很喜欢这个解决方案,但这里的主要缺点是通过阅读您想要实现的代码并不明显。如果我在别人的代码中看到这一行,我会停下来思考“他为什么要在这里取款?” - 除非我以前见过这个“黑客”。
      • @CharlesSalvia 这就是 imho 的 cmets。我想说的是,获得生成器的长度是值得评论的。
      • 另一个主要缺点是它耗尽了生成器只是为了获得长度,这通常首先破坏了生成器的全部目的。
      • 请注意,这可能会减少内存消耗,但它似乎比简单地将其转换为列表要慢。
      • 可以说,len(list(gen)) 更清晰,根据下面的答案,效率更高
      【解决方案9】:
      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)
      

      【讨论】:

        猜你喜欢
        • 2011-11-19
        • 2016-03-11
        • 2019-07-18
        • 2014-06-21
        • 1970-01-01
        • 2016-08-13
        • 2018-09-17
        • 2011-07-20
        • 1970-01-01
        相关资源
        最近更新 更多