【问题标题】:Length of a finite generator有限生成器的长度
【发布时间】:2013-08-03 13:53:57
【问题描述】:

我有这两种实现来计算有限生成器的长度,同时保留数据以供进一步处理:

def count_generator1(generator):
    '''- build a list with the generator data
       - get the length of the data
       - return both the length and the original data (in a list)
       WARNING: the memory use is unbounded, and infinite generators will block this'''
    l = list(generator)
    return len(l), l

def count_generator2(generator):
    '''- get two generators from the original generator
       - get the length of the data from one of them
       - return both the length and the original data, as returned by tee
       WARNING: tee can use up an unbounded amount of memory, and infinite generators will block this'''
    for_length, saved  = itertools.tee(generator, 2)
    return sum(1 for _ in for_length), saved

两者都有缺点,都可以胜任。有人可以对它们发表评论,甚至提供更好的选择吗?

【问题讨论】:

  • 如果不消耗整个东西,就无法知道可迭代生成器的长度。
  • 我知道。这不是问题
  • 注意:如果您不需要精确的长度,那么您可以使用 operator.length_hint() (Python 3.4+) 来返回估计长度而不消耗迭代器。见PEP 424 - A method for exposing a length hint
  • @J.F.Sebastian 这是 3.4 的一个很好的补充
  • @gonvaled:length_hint 将调用 __length_hint__(),这在生成器上实现起来很棘手。

标签: python generator


【解决方案1】:

如果必须这样做,第一种方法要好得多 - 因为您消耗所有值,itertools.tee() 无论如何都必须存储所有值,这意味着列表会更有效。

引用the docs:

这个 itertool 可能需要大量的辅助存储(取决于 需要存储多少临时数据)。一般来说,如果一个 迭代器在另一个迭代器启动之前使用大部分或全部数据, 使用 list() 而不是 tee() 更快。

【讨论】:

  • 好吧,在这两种情况下,我都在使用生成器,并存储完整的数据。第一个通过创建list,第二个只是因为tee 必须做同样的事情(或类似的事情)。我认为获取列表的长度更快(已经是列表对象的一部分?),这就是为什么我更喜欢第一种方法。从内存消耗的角度来看,两者似乎是等效的,对吧?
  • @gonvaled 内存使用可能会相似,但正如我从文档中引用的那样,制作列表会更快。
【解决方案2】:

我使用我能想到的几种方法运行 Windows 64 位 Python 3.4.3 timeit

>>> from timeit import timeit
>>> from textwrap import dedent as d
>>> timeit(
...     d("""
...     count = -1
...     for _ in s:
...         count += 1
...     count += 1
...     """),
...     "s = range(1000)",
... )
50.70772041983173
>>> timeit(
...     d("""
...     count = -1
...     for count, _ in enumerate(s):
...         pass
...     count += 1
...     """),
...     "s = range(1000)",
... )
42.636973504498656
>>> timeit(
...     d("""
...     count, _ = reduce(f, enumerate(range(1000)), (-1, -1))
...     count += 1
...     """),
...     d("""
...     from functools import reduce
...     def f(_, count):
...         return count
...     s = range(1000)
...     """),
... )
121.15513102540672
>>> timeit("count = sum(1 for _ in s)", "s = range(1000)")
58.179126025925825
>>> timeit("count = len(tuple(s))", "s = range(1000)")
19.777029680237774
>>> timeit("count = len(list(s))", "s = range(1000)")
18.145157531932
>>> timeit("count = len(list(1 for _ in s))", "s = range(1000)")
57.41422175998332

令人震惊的是,最快的方法是使用list(甚至不是tuple)来耗尽迭代器并从那里获取长度:

>>> timeit("count = len(list(s))", "s = range(1000)")
18.145157531932

当然,这会带来内存问题的风险。最好的低内存替代方案是在 NOOP for-loop 上使用枚举:

>>> timeit(
...     d("""
...     count = -1
...     for count, _ in enumerate(s):
...         pass
...     count += 1
...     """),
...     "s = range(1000)",
... )
42.636973504498656

干杯!

【讨论】:

    【解决方案3】:

    如果您在处理数据之前不需要迭代器的长度,则可以使用带有 future 的辅助方法将计数添加到迭代器/流的处理中:

    import asyncio
    def ilen(iter):
        """
        Get future with length of iterator
        The future will hold the length once the iteartor is exhausted
        @returns: <iter, cnt-future>
        """
        def ilen_inner(iter, future):
            cnt = 0
            for row in iter:
                cnt += 1
                yield row
            future.set_result(cnt)
        cnt_future = asyncio.Future()
        return ilen_inner(iter, cnt_future), cnt_future
    

    用法如下:

    data = db_connection.execute(query)
    data, cnt = ilen(data)
    solve_world_hunger(data)
    print(f"Processed {cnt.result()} items")
    

    【讨论】:

      猜你喜欢
      • 2023-03-17
      • 2021-10-21
      • 2016-02-16
      • 2017-11-12
      • 2021-04-01
      • 2014-11-04
      • 2020-03-05
      • 2012-01-26
      • 1970-01-01
      相关资源
      最近更新 更多