【问题标题】:How to prevent iterator getting exhausted?如何防止迭代器耗尽?
【发布时间】:2022-01-03 02:27:10
【问题描述】:

如果我创建两个列表并压缩它们

a=[1,2,3]
b=[7,8,9]
z=zip(a,b)

然后我将 z 类型转换为两个列表

l1=list(z)
l2=list(z)

那么 l1 的内容原来是 [(1,7),(2,8),(3,9)],但是 l2 的内容只是 []。

我猜这是 python 在可迭代对象方面的一般行为。但是作为一个从 C 家族迁移出来的新手程序员,这对我来说没有意义。为什么它会以这种方式表现?有没有办法解决这个问题?

我的意思是,是的,在这个特定的示例中,我可以将 l1 复制到 l2,但总的来说,在我迭代一次之后,有没有办法“重置”Python 用来迭代“z”的任何内容?

【问题讨论】:

  • 这是 generators 的行为,而不是所有的可迭代对象。例如,列表是可迭代的,您可以调用list(a) 并获得a 的副本,只要您想要。

标签: python python-3.x


【解决方案1】:

没有办法“重置”生成器。但是,您可以使用itertools.tee 来“复制”一个迭代器。

>>> z = zip(a, b)
>>> zip1, zip2 = itertools.tee(z)
>>> list(zip1)
[(1, 7), (2, 8), (3, 9)]
>>> list(zip2)
[(1, 7), (2, 8), (3, 9)]

这涉及缓存值,因此只有以大致相同的速率迭代两个可迭代对象时才有意义。 (换句话说,不要像我这里那样使用它!)

另一种方法是传递生成器函数,并在您想要迭代它时调用它。

def gen(x):
    for i in range(x):
        yield i ** 2

def make_two_lists(gen):
    return list(gen()), list(gen())

但是现在您必须在传递时将参数绑定到生成器函数。您可以为此使用lambda,但很多人觉得lambda 丑陋。 (但不是我!YMMV。)

>>> make_two_lists(lambda: gen(10))
([0, 1, 4, 9, 16, 25, 36, 49, 64, 81], [0, 1, 4, 9, 16, 25, 36, 49, 64, 81])

我希望不用说,在大多数情况下,最好只是列出并复制它。

此外,作为解释此行为的更一般的方式,请考虑这一点。生成器的目的是产生一系列值,同时在迭代之间保持某种状态。现在,有时,您可能想要执行以下操作,而不是简单地迭代生成器:

z = zip(a, b)
while some_condition():
    fst = next(z, None)
    snd = next(z, None)
    do_some_things(fst, snd)
    if fst is None and snd is None:
        do_some_other_things()

假设这个循环可能可能不会耗尽z。现在我们有一个处于不确定状态的生成器!因此,在这一点上,以明确定义的方式限制生成器的行为是很重要的。虽然我们不知道生成器在其输出中的位置,但我们知道 a) 所有后续访问都将在系列中产生 later 值,并且 b) 一旦它为“空”,我们就得到了该系列中的所有项目恰好一次。我们操纵z 状态的能力越强,就越难以推理它,所以我们最好避免违反这两个承诺的情况。

当然,正如 Joel Cornett 在下面指出的那样,可以编写一个通过 send 方法接受消息的生成器;并且可以编写一个可以使用send 重置的生成器。但请注意,在这种情况下,我们所能做的就是发送消息。我们不能直接操纵生成器的状态,因此对生成器状态的所有更改都是明确定义的(由生成器本身 - 假设它是正确编写的!)。 send 确实是为了实现coroutines,所以我不会将它用于此目的。日常生成器几乎从不使用发送给它们的值做任何事情——我认为这正是我上面给出的原因。

【讨论】:

  • 还有send() 功能。
  • 这行得通,但是很复杂而且过分,IMO。 “不要像我一样使用它”也暗示了这一点。 :-)
  • @LennartRegebro,好吧,我认为tee 的存在是有充分理由的,它是我能想到的标准库中最接近 OP 所要求的功能的东西。我假设 OP 已经知道可以复制列表!
  • @user1265125,考虑一下我最近的编辑,它更详细地回答了你的问题。
【解决方案2】:

如果你需要两份列表,如果你需要修改它们,那么我建议你做一次列表,然后复制它:

a=[1,2,3]
b=[7,8,9]
l1 = list(zip(a,b))
l2 = l1[:]

【讨论】:

  • 是的,正如我提到的,我可以简单地复制第一个列表。我问这个问题只是因为我想清楚我的 Python 概念。还是谢谢!
【解决方案3】:

只需使用一次list() 从您的迭代器中创建一个列表,然后再使用它。

恰巧zip 返回了一个generator,它是一个iterator,你只能迭代一次。

您可以根据需要多次迭代列表。

【讨论】:

  • 这并不是真正的“铸造”,但这通常是最好的方法。此外,您已经正确识别出“迭代器”是一个包含生成器和序列的超类别,因此我投票选出最佳答案。
【解决方案4】:

不,没有办法“重置它们”。

生成器一次按需生成输出,然后在输出耗尽时完成。

把它们想象成读取一个文件,一旦你读完了,如果你想再次读取数据,就必须重新启动。

如果您需要保留生成器的输出,请考虑将其存储在列表中,例如,然后根据需要重复使用它。 (有点类似于指导使用 xrange() 的决策,这是一个生成器 vs range() 在 v2 中创建了内存中的整个项目列表)

更新:更正的术语,暂时的大脑中断...

【讨论】:

  • 您所描述的基本上是由itertools.tee() 完成的,如senderle 的回答中所述。不过,我要 +1,因为您的讨论是相关的。
【解决方案5】:

又一个解释。作为程序员,您可能了解类与实例(即对象)之间的区别。 zip() 据说是一个内置函数(在官方文档中)。实际上,它是一个内置的generator函数。这意味着它更像是类。您甚至可以在交互模式下尝试:

>>> zip
<class 'zip'>

类是类型。因此,以下内容也应该清楚:

>>> type(zip)
<class 'type'>

您的z 是类的实例,您可以将调用zip() 视为调用类构造函数:

>>> a = [1, 2, 3]
>>> b = [7, 8, 9]
>>> z = zip(a, b)
>>> z
<zip object at 0x0000000002342AC8>
>>> type(z)
<class 'zip'>

z 是一个迭代器(对象),它保留在 ab 的迭代器内。由于其通用实现,z(或zip 类)无法通过ab 或任何序列重置迭代器。因此,无法重置z。解决您的具体问题的最简洁方法是复制列表(正如您在问题和Lennart Regebro suggests 中提到的那样)。另一种可以理解的方法是使用zip(a, b) 两次,从而构造两个类似z 的迭代器,它们从一开始就以相同的方式运行:

>>> lst1 = list(zip(a, b))
>>> lst2 = list(zip(a, b))

但是,这通常不能使用相同的结果。想想ab 是基于某些当前条件(比如从几个温度计读取的温度)生成的唯一序列。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-11-11
    • 1970-01-01
    • 2017-05-02
    • 1970-01-01
    相关资源
    最近更新 更多