【问题标题】:How to minimize space cost when using itertools.tee to check the next element?使用 itertools.tee 检查下一个元素时如何最小化空间成本?
【发布时间】:2018-11-09 00:34:03
【问题描述】:

我正在尝试使用 itertools.tee 来知道迭代器是否为空而不完全消耗它:

from itertools import tee
def get_iterator(i):
    i1, i2 = tee(i, 2)
    if next(i1, None) is None:
       # iterator is empty - raises some error
       pass
    return i2 # return not empty iterator to caller

正如 tee 的 docs 所说:

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

所以很明显,当 i 不为空时,i2 在 i1 之前使用了大部分数据。 一个简单的 del 可以解决这个问题吗?:

from itertools import tee
def get_iterator(i):
    i1, i2 = tee(i, 2)
    if next(i1, None) is None:
       # iterator is empty - raises some error
       pass
    del i1  # Does this overcome storage issue?
    return i2  # return not empty iterator to caller

有没有更好的方法来实现这个目标?

提前致谢!

【问题讨论】:

  • @Chris_Rands tee 基本上确实耗尽了整个迭代器来创建新的迭代器——这完全不是真的。
  • @Chris_Rands 文档说“以下 Python 代码有助于解释 tee 的作用(尽管实际实现更复杂,并且仅使用单个底层 FIFO 队列)。”如果您查看 CPython 代码中的 teedataobject_getitem,您会发现它仅在前导迭代器到达该点时才获取新数据 PyIter_Next。然后它会存储该值,直到所有 tees 都使用了该值。
  • 查看 Alex Martelli 关于标记值 here 的评论。
  • @Chris_Rands 仅当您运行新迭代器之一时。例如,如果您使用a, b, c = tee(itr, 3),那么如果您使用i = next(a); del i,您将在内存中存储i,直到 next(b)next(c) 都被执行。最坏的情况是,如果您执行la = list(a),那么您将在内存中拥有len(la) 元素,直到bc 都向前迭代。

标签: python itertools tee


【解决方案1】:

这有点微妙 - 它取决于 tee 函数的未记录属性以及 garbage collector 的故意模糊属性。示例 Python 代码将存储从创建迭代器直到它们被每个迭代器使用的所有项目,但人们可能很容易想象迭代器将具有清理效果,从而放弃对队列中数据的声明。但即便如此,del 还是删除了你的名字;它不保证对象的销毁。因此,这样的清理工作会起作用,但不一定在您期望的时间。要知道这是否发生需要阅读the source code for tee。它确实有对单个迭代器的weak reference 支持,这表明可以完成这种优化的一种方式。

tee_next 的 CPython 代码相当简单;它包含对teedataobject 的引用,这是一个最多包含 57 个项目的批次,也形成了一个单链表。因此,正常的引用计数语义适用于该批次级别。所以基本上,对于 CPython,多达 56 个项目即使在 所有 迭代器消耗后仍保留在内存中,但仅此而已,因为引用计数处理是即时的。只要tee 迭代器存在,它们之间的任意数量的项都可以被保存,但它们不会从源迭代器中提前读取;至少一个 tee 迭代器必须通过teedataobject_getitem 获取项目。

所以基本判断是:是的,del 将在 CPython 中工作,但使用 tee 意味着您暂时存储 57 个项目的批次,而不是 1。重复此方法可能会导致任意数量的此类窗口 -除了tee 迭代器是可复制的,并且将共享它们的底层列表。

这是对 CPython 的一个版本 (4243df51fe43) 的具体解释。实现会有所不同,例如PyPy、IronPython、Jython 或其他版本的 CPython。

例如,PyPy's tee(版本 cadf868)使用类似的链表,每个链接一个项目,因此不会像这个 CPython 版本那样进行批处理。

有一个显着的捷径可以防止这种成本增加:我研究过的tee 实现都产生了可复制的迭代器,也复制了可复制的迭代器。所以重复应用tee 不会创建新的迭代器层,这是chain 方法的一个潜在问题。

【讨论】:

  • 在 (...) 或不同版本的 CPython 中实现会有所不同。 :)
  • 可复制迭代器是什么意思?
  • 可复制迭代器有一个__copy__ 方法来生成自身的副本。迭代器的副本产生与第一个迭代器相同的输出。 tee 通过根据需要存储值来生成迭代器的副本,即使它们没有 __copy__ 方法也是如此。
【解决方案2】:

我的意思是,在你的具体情况下,这有什么问题

from itertools import chain
def get_iterator(i):
    try:
        first = next(i):
    except StopIteration:
       # iterator is empty - raises some error
       pass
    return chain([first], i)

它做同样的事情,但除了第一个值之外不存储任何东西。

【讨论】:

  • 它确实解决了我的特定需求,但我也很好奇 del 对 tee 对象的影响。非常感谢!
  • 这个答案值得赞成。它适用于第二个问题“有没有更好的方法”,而我的适用于第一个问题,“del 能否克服 [tee 的存储成本]”。
  • 在 cmets 中给出已经指向问题的答案是一种糟糕的品味……
  • @PiotrDobrogost 老实说,我什至没有关注您的链接。我独立想出了这个。很抱歉没有先检查。
  • 我实际上找到了反对这种形式的论据(以及tee 形式)。如果可以,PyPy 和 CPython tee 实现都会复制迭代器,这不会复制底层存储 - 并且 tee 迭代器是可复制的。
最近更新 更多