为什么代码根据集合内的变量或生成器工作不同?

Why does code work different based on vars or generators inside a set?

目标是找出a**b的个数,其中2<=a,b<=100。任务很简单,我找到了答案,但我不明白为什么这很好用:

def count():
   return len(set(a**b for a in range(2,101) for b in range(2,101)))

但这是错误的:

def count():
    a = (i for i in range(2,101))
    b = (i for i in range(2,101))
    return len(set(i**j for i in a for j in b))

即使这样也能正常工作(基于第二个函数):return len(set(i**j for i in a for j in range(2, 101)))
但是把第一个var改成generator,对下一个做相反的操作,就出错了: return len(set(i**j for i in range(2, 101) for j in a))
真的很想自己解决这个问题,但我就是不知道哪里出了问题

@ddejohn 给了你一个非常简短的答案,所以我会展开。

Python 的生成器的行为起初有点奇怪。这是一个简单的例子:

my_generator = (i for i in range(3))  # 0 to 2 included
print(next(my_generator))  # 0
print(next(my_generator))  # 1
print(next(my_generator))  # 2
print(next(my_generator))  # StopIteration raised

生成器将提供其值,然后引发 StopIteration 异常。

当生成器耗尽时对 next 的后续调用将继续引发 StopIteration 异常。
这意味着我们不能在生成器上循环两次,这是一个例子:

my_generator = (i for i in range(3))  # 0 to 2 included
for letter in ('a', 'b'):
    print(letter)
    for num in my_generator:
        print(num)
    print("end")
a
0
1
2
end
b
end

对于第一个字母,我们循环生成器值,直到它引发一个 StopIteration,这表明 for 循环停止。对于第二个字母,生成器立即引发 StopIteration 因此循环根本没有进行迭代。

这是设计使然:生成器只能使用一次,它们会生成值直到耗尽。

回到您的代码,我将您的生成器 i**j for i in a for j in b 转换为两个循环以使其更易于打印:

def count():
    a = (i for i in range(2,101))
    b = (i for i in range(2,101))
    for i in a:
        print(f"outer loop {i=}")
        for j in b:
            print(f"  inner loop {j=}")
count()
outer loop i=2
  inner loop j=2
  inner loop j=3
  inner loop j=4
  [...]
  inner loop j=99
  inner loop j=100
outer loop i=3
outer loop i=4
outer loop i=5
outer loop i=6
[...]
outer loop i=98
outer loop i=99
outer loop i=100

您可以看到 i 的第一次迭代是预期的,但其他的不是,因为 j 生成器已经耗尽。

这里有两种方法可以解决这个问题:

  • 不使用生成器(因为这里你只有 ~100 个值,所以不需要生成器)
    def count():
        a = (i for i in range(2,101))
        b = tuple(i for i in range(2,101))
        #   ^^^^^
        for i in a:
            print(f"outer loop {i=}")
            for j in b:
                print(f"  inner loop {j=}")
    count()
    
  • 或者为每个 outer-iteration 构建一个新的生成器:
    def count():
        a = (i for i in range(2,101))
        # not here
        for i in a:
            print(f"outer loop {i=}")
            # but here :
            b = (i for i in range(2,101))
            for j in b:
                print(f"  inner loop {j=}")
    count()
    

第二种解决方案是在您的代码版本中隐式完成的工作 (a**b for a in range(2,101) for b in range(2,101)):在 a 的每次迭代中创建一个新的 range 对象。

我希望现在更清楚了。


吹毛求疵:(i for i in range(2,101)) 几乎等同于简单的 range(2,101) 因为 range 对象已经是惰性对象,所以将它包装在显式生成器中不会增加任何内容。