Python中在函数内部使用生成器表达式时,会有哪些意想不到的行为?

make_index 这个函数就是简单的做一个倒排索引,将出现了某种语言的文章与该语言关联起来,返回的结果是一个列表,列表中每个元素是一个 tuple 。

from collections import namedtuple

WikipediaArticle = namedtuple(“WikipediaArticle”, [“title”, “text”])

def make_index(langs, articles): result = [] for lang in langs: # 创建包含该 lang 的文章的生成器 article_gen = (article for article in articles if article.text.find(lang) >= 0) result.append((lang, article_gen)) return result

if name == ‘main’: articles = [ WikipediaArticle(‘1’, “Groovy is pretty interesting, and so is Erlang”), WikipediaArticle(‘2’, “Scala and Java run on the JVM”), WikipediaArticle(‘3’, “Scala is not purely functional”), WikipediaArticle(‘4’, “The cool kids like Haskell more than Java”), WikipediaArticle(‘5’, “Java is for enterprise developers”) ] langs = [“Scala”, “Java”, “Groovy”, “Haskell”, “Erlang”] for item in make_index(langs, articles): print(item[0], list(item[1])

然后跑出来的结果是:

    Scala [WikipediaArticle(title='1', text='Groovy is pretty interesting, and so is Erlang')]
    Java [WikipediaArticle(title='1', text='Groovy is pretty interesting, and so is Erlang')]
    Groovy [WikipediaArticle(title='1', text='Groovy is pretty interesting, and so is Erlang')]
    Haskell [WikipediaArticle(title='1', text='Groovy is pretty interesting, and so is Erlang')]
    Erlang [WikipediaArticle(title='1', text='Groovy is pretty interesting, and so is Erlang')]

很明显这结果有问题,取得都是第一条数据, 但是如果在函数内部做一点修改,会得到以下结果:

# result.append((lang, article_gen)) 这一句改成 result.append((lang, list(article_gen)))
('Scala', [WikipediaArticle(title='2', text='Scala and Java run on the JVM'), WikipediaArticle(title='3', text='Scala is not purely functional')])
('Java', [WikipediaArticle(title='2', text='Scala and Java run on the JVM'), WikipediaArticle(title='4', text='The cool kids like Haskell more than Java'), WikipediaArticle(title='5', text='Java is for enterprise developers')])
('Groovy', [WikipediaArticle(title='1', text='Groovy is pretty interesting, and so is Erlang')])
('Haskell', [WikipediaArticle(title='4', text='The cool kids like Haskell more than Java')])
('Erlang', [WikipediaArticle(title='1', text='Groovy is pretty interesting, and so is Erlang')])

感觉好奇怪,为什么会这样,求解, 在函数里面解开生成器和在函数外面解开有什么区别吗?


Python中在函数内部使用生成器表达式时,会有哪些意想不到的行为?

29 回复

在函数内部使用生成器表达式时,最需要注意的就是它的延迟求值特性。这会导致一些看起来理所当然的代码,实际运行时却产生意料之外的结果,尤其是在与循环变量或可变状态交互时。

一个典型的“坑”是闭包捕获循环变量的问题。看看下面这个例子:

def create_generators():
    gens = []
    for i in range(3):
        # 生成器表达式记住了变量i,而不是创建时的值
        gen = (i * j for j in range(3))
        gens.append(gen)
    return gens

for gen in create_generators():
    print(list(gen))
# 你以为会输出:[0, 1, 2], [0, 2, 4], [0, 3, 6]
# 实际输出:[6, 7, 8], [6, 7, 8], [6, 7, 8]  (如果i最终为3)

为什么会这样? 生成器表达式 (i * j for j in range(3)) 在定义时,并没有立即计算 i 的值。它只是创建了一个“配方”,其中包含了一个对变量 i引用。当你在外部真正遍历这个生成器(例如调用 list(gen))时,它才会去查找 i 当前的值。此时循环早已结束,i 的最终值是 2,所以所有生成器都使用了 i=2

如何避免? 关键是把循环变量的值在定义时“固定”下来,使其成为一个局部变量。最常用的方法是使用默认参数

def create_generators_fixed():
    gens = []
    for i in range(3):
        # 使用默认参数 i=i 将当前i的值绑定到lambda的局部变量i
        gen = (lambda i=i: (i * j for j in range(3)))()
        # 或者更直接地,在表达式内部通过函数参数绑定
        # gen = (i * j for j in range(3))  # 这样不行
        # 正确做法:使用一个函数来捕获当前值
        def make_gen(num):
            return (num * j for j in range(3))
        gen = make_gen(i)
        gens.append(gen)
    return gens

for gen in create_generators_fixed():
    print(list(gen))
# 现在正确输出:[0, 1, 2], [0, 2, 4], [0, 3, 6]

另一个常见问题是,生成器表达式本身是一次性的。遍历一次后,它就“耗尽”了。如果你在函数里多次使用同一个生成器表达式对象,第二次遍历会得到空结果。

总结: 小心生成器的延迟求值,尤其是在捕获外部变量时。

大哥,你有认真看我的描述吗,我看你发的链接是教程,完全没解答问题呢。希望能给建设性的指教。

兄弟
<br>[article for article in articles if article.text.find(lang) &gt;= 0]<br>

居然不是震惊…

不过输出成这样有点奇怪
按理说不该是<generator object…>吗
觉得该是 print 的锅

为什么我试了一下没问题呢
<br>WikipediaArticle(title='2', text='Scala and Java run on the JVM')<br>WikipediaArticle(title='2', text='Scala and Java run on the JVM')<br>WikipediaArticle(title='1', text='Groovy is pretty interesting, and so is Erlang')<br>WikipediaArticle(title='4', text='The cool kids like Haskell more than Java')<br>WikipediaArticle(title='1', text='Groovy is pretty interesting, and so is Erlang')<br>

怎么输出成这样的…

还是有问题, 我测试错了
python<br> article_gen = (article for article in articles if article.text.find(lang) &gt;= 0)<br> print(lang, next(article_gen))<br> result.append((lang, article_gen))<br>
我实在里面试了一下, 最后输出的还是跟楼主发的一样。

这么短一个程序有混用 tab 和空格,少括弧,先生成 tuple 又莫明其妙转换成 list 这么多错误,所以叫你学好基础知识。

楼主明天就来 UC 上班,待遇从优

25 87 SETUP_LOOP 50 (to 140)
90 LOAD_GLOBAL 1 (make_index)
93 LOAD_FAST 1 (langs)
96 LOAD_FAST 0 (articles)
99 CALL_FUNCTION 2 (2 positional, 0 keyword pair)
102 GET_ITER
>> 103 FOR_ITER 33 (to 139)
106 STORE_FAST 2 (item)

26 109 LOAD_GLOBAL 2 (print)
112 LOAD_FAST 2 (item)
115 LOAD_CONST 16 (0)
118 BINARY_SUBSCR
119 LOAD_GLOBAL 3 (list)
122 LOAD_FAST 2 (item)
125 LOAD_CONST 17 (1)
128 BINARY_SUBSCR
129 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
132 CALL_FUNCTION 2 (2 positional, 0 keyword pair)
135 POP_TOP
136 JUMP_ABSOLUTE 103
>> 139 POP_BLOCK
>> 140 LOAD_CONST 0 (None)
143 RETURN_VALUE

寄存器的锅?求大神解释…

生成器只能读取一次,自己想想逻辑哪里有问题吧
提示: list(<generator object>) in for

局部变量 lang 被所有生成器表达式捕获并共享,表达式被遍历输出的时候才会延迟获取 lang 的值,此时循环已经结束, lang 获取的是最语言列表的最后一个值

原因: generator 在取 next 时才去执行的代码,执行代码时 lang 的值是最后一次的值,可以改成这样看看效果:
article_gen = ((lang, article) for article in articles if article.text.find(lang) >= 0) 这里返回的 lang 就是最后一个值。

暂时没想到更好的在循环里生成 generator 的办法,我会避免使用。参照之前经典的 js 面试题改了下代码:
article_gen = (lambda l: (article for article in articles if article.text.find(l) >= 0))(lang)

我用的不是列表解析啊,用的是生成器表达式,如果只是 print 生成器,就是 generator objec , 但是我 print 的时候把他转成列表了。
所有我还是不明白为什么,转成列表之后,就只剩一个元素了。

不好意思,可能是我 markdown 没写好,但是我代码都是 pep8 检测了的,不可能会有什么语法上的错误或者多括号少括号的问题,至于你说的生成 tuple 和 list ,我觉得你可能没看懂代码。转成 list 的原因是要把生成器的元素释放出来。

原因是变量的作用域问题

gen_generator = lambda lang, articles: (article for article in articles if article.text.find(lang) >= 0)

in loop:

article_gen = gen_generator(lang, articles)

可是我的结果是五个 ( String , generator ) tuple :
(‘Scala’, <generator object make_index.<locals>.<genexpr> at 0x0061BDB0>)
(‘Java’, <generator object make_index.<locals>.<genexpr> at 0x006291E0>)
(‘Groovy’, <generator object make_index.<locals>.<genexpr> at 0x0061BDB0>)
(‘Haskell’, <generator object make_index.<locals>.<genexpr> at 0x006291E0>)
(‘Erlang’, <generator object make_index.<locals>.<genexpr> at 0x0061BDB0>)

然后对每个 tuple 的生成器利用 list 函数,为啥返回的结果是一毛一样的。我就是这点存在疑惑。

你 article 还是当年的 article , lang 已经不是当年的 lang 了。

看错了,是无意义的把 list 转换成 list

list(item[1])

…item[1]是 generator
嗯,我图形化代码的时候,发现所有的 generator 都指向了最后那个 lang 为 Erlang 的结果。 看了确实是 article 没变, lang 却变了。谢谢你们,我大概知道出错的原因了。但是还是不太理解里面的过程,请问哪里可以多了解这方面的知识?~

你真傻还是装傻啊

result.append((lang, list(article_gen)))

你说 item[1]是不是 generator


Finding closure with closures :

<iframe src="https://www.youtube.com/embed/E9wS6LdXM8Y" class="embedded_video" allowfullscreen="" type="text/html" id="ytplayer" frameborder="0"></iframe>

你是这里找到的吧 # result.append((lang, article_gen)) 这一句改成 result.append((lang, list(article_gen)))
这里是为了说明问题所做的比较啊大哥…关注点也是清奇。

所以请好好审题。明确别人问的是什么。然后丢合适的链接。要么就不答,要么就认真答。大家都是奔着解决问题去的。你这种回答态度很让人不舒服。 还好其他 v 友读懂了题目,给我上了一课。

确实能找到 不好意思,自己打脸了,我这下真心诚意接收你的建议,补基础去。收回前面的话~ sorry!!!

这是闭包的一个容易误解的地方嘛,等你把 generator 转成 list 的时候, generator 的代码开始执行,这时 lang 的值是 Erlang

回到顶部