【问题标题】:Python generator conflicting with list comprehensionPython生成器与列表理解冲突
【发布时间】:2026-02-12 00:15:01
【问题描述】:

我一直在 Python 中使用生成器函数。我想写一个函数,它接受一个生成器,其值为元组,并返回一个生成器列表,其中每个生成器的值对应于原始元组中的一个索引。

目前,我有一个函数可以为元组中的硬编码数量的元素完成此操作。这是我的代码:

import itertools

def tee_pieces(generator):
    copies = itertools.tee(generator)
    dropped_copies = [(x[0] for x in copies[0]), (x[1] for x in copies[1])]
    # dropped_copies = [(x[i] for x in copies[i]) for i in range(2)]
    return dropped_copies

def gen_words():
    for i in "Hello, my name is Fred!".split():
        yield i

def split_words(words):
    for word in words:
        yield (word[:len(word)//2], word[len(word)//2:])

def print_words(words):
    for word in words:
        print(word)

init_words = gen_words()
right_left_words = split_words(init_words)
left_words, right_words = tee_pieces(right_left_words)
print("Left halves:")
print_words(left_words)
print("Right halves:")
print_words(right_words)

这会正确拆分生成器,导致 left_words 包含左半部分,而 right_words 包含右半部分。

当我尝试使用上面注释掉的行参数化要创建的生成器的数量时,问题就出现了。据我所知,它应该是等效的,但是当我改用该行时,left_words 和 right_words 最终都包含单词的右半部分,输出如下:

Left halves:
lo,
y
me
s
ed!
Right halves:
lo,
y
me
s
ed!

为什么会这样?我怎样才能实现想要的结果,即参数化生成器的分割数?

【问题讨论】:

    标签: python scope functional-programming generator


    【解决方案1】:

    这与python's lexical scoping 规则有关。展示它的经典“令人惊讶”的例子:

    funcs = [ lambda: i for i in range(3) ]
    print(funcs[0]())
    => 2  #??
    print(funcs[1]())
    => 2  #??
    print(funcs[2]())
    => 2
    

    您的示例是相同规则的另一个结果。

    要修复,您可以使用附加功能“打破”范围:

    def make_gen(i):
        return (x[i] for x in copies[i])
    dropped_copies = [make_gen(i) for i in range(2)]
    

    这会将i 的值绑定到传递给make_gen 的特定调用的特定值,从而实现所需的行为。没有它,它会绑定“名为 i 的变量的当前值”,对于您创建的所有生成器,该值最终都是相同的值(因为只有一个名为 i 的变量)。

    【讨论】:

      【解决方案2】:

      添加到 shx2 的答案中,您也可以用 lambda 替换附加功能:

      dropped_copies = [(lambda j: (x[j] for x in copies[j]))(i) for i in range(2)]
      

      当 lambda 被调用时,这也会创建一个新的作用域,不同的变量名可以清楚地看出这一点。但是,它也可以使用相同的名称,因为 lambda 中的参数会影响生成器中的参数:

      dropped_copies = [(lambda i: (x[i] for x in copies[i]))(i) for i in range(2)]
      

      这种作用域看起来很混乱,但如果将生成器重写为 for 循环,就会变得更直观:

      dropped_copies = []
      for i in range(2):
          dropped_copies.append((x[i] for x in copies[i]))
      

      请注意,这与原始列表理解版本的破坏方式相同。

      【讨论】:

        【解决方案3】:

        这是因为dropped_copies 是一对迭代器,当对迭代器求值时,i 已经递增到 1。

        尝试使用列表推导,你可以看到区别:

        dropped_copies = [[x[i] for x in copies[i]] for i in range(2)]
        

        【讨论】: