在Python中比较两个生成器

31

我想问一下在比较两个生成器时使用==的情况。

例如:

x = ['1','2','3','4','5']

gen_1 = (int(ele) for ele in x)
gen_2 = (int(ele) for ele in x)

gen_1gen_2在实际上是相同的,但是当我进行比较时:

>>> gen_1 == gen_2
False

我的猜测是这里的==被处理得像通常的 is 一样,而且由于gen_1gen_2存储在不同的内存位置:

>>> gen_1
<generator object <genexpr> at 0x01E8BAA8>
>>> gen_2
<generator object <genexpr> at 0x01EEE4B8>

他们的比较结果为False。我的猜测是正确的吗?欢迎分享任何其他见解。

顺便说一句,我知道如何比较两个生成器:

>>> all(a == b for a,b in zip(gen_1, gen_2))
True

或者甚至更多

>>> list(gen_1) == list(gen_2)
True

但是如果有更好的方法,我很乐意知道。


5
把生成器表达式看作像函数一样,而不是像列表一样。 - agf
1
一旦你比较生成器,你就会耗尽它们,然后它们就变为空了。 - Luka Rahne
@LukaRahne 不完全正确。生成器可以是无限的。== 只会比较对象标识。如果您在生成器上进行迭代,通常情况下可能会导致无限循环。 - Gulzar
@Gulzar的评论是关于all(a == b for a,b in zip(gen_1, gen_2))的。 - Luka Rahne
4个回答

25

你的猜想是正确的 - 对于没有定义 == 的类型进行比较的后备方法是基于对象标识的比较。

更好的比较它们生成的值的方法是

from itertools import zip_longest, tee
sentinel = object()
all(a == b for a, b in zip_longest(gen_1, gen_2, fillvalue=sentinel))

(对于Python 2.x,请使用izip_longest而不是zip_longest)

这个方法实际上可以在不必查看所有值的情况下提前结束。正如评论中larsmans所指出的,我们不能使用zip()。因为如果生成器产生不同数量的元素,则可能会得到错误的结果--zip()将停止在最短的迭代器上。我们使用一个新创建的object实例作为zip_longest()的填充值,因为object实例与任何可能出现在这些生成器中的理性值(包括其他object实例)都不相等。

请注意,没有比较生成器的方法而不改变它们的状态。如果您之后需要它们,您可以存储已经消耗掉的项:

gen_1, gen_1_teed = tee(gen_1)
gen_2, gen_2_teed = tee(gen_2)
all(a == b for a, b in zip_longest(gen_1, gen_2, fillvalue=sentinel))

这将使得gen_1gen_2的状态本质上不变。被all()消耗的所有值都存储在tee对象中。

此时,你可能会问自己是否值得为手头的应用程序使用惰性生成器——将它们转换为列表并与列表一起使用可能更好。


关于使用 tee() 的注意事项:两个创建的迭代器应该是相等的。没有理由偏爱其中一个。保留分配给原始名称的迭代器以供以后处理不是更合理吗?gen_1, gen_1_to_compare = tee(gen_1),或者您使用新名称 gen_1_teed 来清楚地表明迭代器已更改(可能会增加开销,内存中的值...)? - pabouk - Ukraine stay strong
@pabouk 在这里我没有任何强烈的命名意见 - 在真实世界的应用中,我会根据实际情况调整名称。同时请注意,我在十年前写了这个答案,所以我不太记得当时我是怎么想的。 :) - Sven Marnach
@amka66,我所说的“some_object”是指一个“object”的实例。 - Sven Marnach
@amka66 更具体地说,我指的是 sentinel.__eq__(something_else) 总是返回 NotImplemented - Sven Marnach
@amka66 我认为我们在这里存在一些交叉的线路。是的,自定义类可以被实现成与 sentinel 相等,这会有点破坏这个答案中的代码。无论哪个迭代器更短都没有关系。如果 sentinel.__eq__(x) 返回 NotImplemented,解释器将调用 x.__eq__(sentinel),所以两种方式都是可能的。然而,这将是自定义类 __eq__ 实现中的一个 bug,我们不应该使我们的代码更加复杂和难以阅读,以适应其他地方非常不可能出现的 bug。 - Sven Marnach
显示剩余6条评论

12
因为生成器在需要时才会生成它们的值,没有任何方法可以在不实际“消耗”它们的情况下进行“比较”。如果您的生成器生成无限序列的值,则您提出的这种相等性测试将是无用的。

7

==在两个生成器上确实与is相同,因为这是唯一的检查,可以在不改变它们的状态并且不失去元素的情况下进行。

list(gen_1) == list(gen_2)

这是一种可靠且通用的比较两个有限生成器的方法(但显然会消耗两者);您基于zip的解决方案在它们生成不同数量的元素时会失败:

>>> list(zip([1,2,3,4], [1,2,3]))
[(1, 1), (2, 2), (3, 3)]
>>> all(a == b for a, b in zip([1,2,3,4], [1,2,3]))
True

当任意一个生成器生成无限数量的元素时,基于列表的解决方案仍将失败。你可以想出一个解决方法,但是当两个生成器都是无限的时,你只能为非相等设计一个半算法


5
为了像列表和其他容器一样对两个生成器进行逐项比较,Python必须完全消耗它们(至少是较短的那个)。我认为明确要求你必须这样做是好的,特别是因为其中一个可能是无限的。

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