问题描述
当在 Python 3 中动态嵌套生成器表达式时,当生成器表达式引用动态引用的函数时,我看到了一些非常奇怪的行为,我不知道如何解释。
这是一个重现问题的非常简化的案例:
double = lambda x: x * 2
triple = lambda x: x * 3
processors = [double,triple]
data = range(3)
for proc in processors:
data = (proc(i) for i in data)
result = list(data)
print(result)
assert result == [0,6,12]
在这种情况下,我希望每个数字都乘以 6 (triple(double(x))
),但实际上 triple(triple(x))
被调用。我或多或少很清楚,当生成器表达式运行时,proc
指向 triple
,而不管它在创建生成器表达式时指向什么。
那么,(1) 这是预期的,有人可以指出 Python 文档或其他地方的一些相关信息来解释这一点吗?
和 (2) 你能推荐另一种嵌套生成器表达式的方法,其中每个级别都调用一个动态提供的可调用对象吗?
编辑:我在 Python 3.8.x 上看到它,还没有用其他版本测试
解决方法
这是两件事的结果:
- 生成器是惰性求值的,所以函数只在生成器被消耗时调用,
- 名称在评估时解析,而不是在生成器创建时解析。
所以当你用list(data)
消费生成器时,名称proc
指的是函数triple
,两个生成器都调用了名称proc
绑定的函数,所以你得到 triple
两次。
map
起作用的原因是因为它是一个函数,所以当您将 proc
作为参数传递时,它会在调用 proc
时收到 map
的值,它在循环中,而 proc
仍然可以引用 double
函数。
是的,这是意料之中的,而且你的理由是对的。
由于生成器是惰性的,proc(i)
仅在被请求时才会被评估。这涉及评估 proc
和 i
然后。当您最终请求时,proc
已经是 triple
,所以这就是使用的内容。
在这种特殊情况下,data = map(proc,data)
完成了这项工作。之所以有效,是因为 map
捕获并记住了 proc
,就像您调用 map
时一样。
你可以用一个生成器函数做同样的事情。我尝试使用生成器 expression 像
data = (p(i) for p in [proc] for i in data)
但它失败了 ValueError: generator already executing
。不过这行得通:
data = (lambda proc: (proc(i) for i in data))(proc)
,
这不起作用,因为 proc
在循环的第二次迭代中被重新赋值给 triple
,这会改变第一次迭代的 proc
引用。通过展开循环并删除最后一个生成器,这更加明确:
double = lambda x: x * 2
triple = lambda y: y * 3
data = range(3)
proc = double
data = (proc(i) for i in data)
# Let's change `proc` to be triple
proc = triple
result = list(data)
print(result)
# [0,3,6]
数据增加了三倍,这意味着通过将 proc 重新分配为 triple
,您可以更改第一个生成器中引用后面的值。
我发现使用 map
确实有效,就我而言,这是对 #2 的一个很好的答案:
double = lambda x: print(x) or x * 2
triple = lambda x: print(x) or x * 3
processors = [double,triple]
data = range(3)
for proc in processors:
data = map(proc,data)
result = list(data)
print(result)
assert result == [0,6,12]
不过,我很高兴知道 #1 - 这种行为的原因是什么,这是否记录在某处?