【问题标题】:Closure semantics in Python generator comprehensionsPython 生成器理解中的闭包语义
【发布时间】:2022-01-17 09:02:23
【问题描述】:

在下面的代码中,l1 得到 ([1], [1]),l2, l3, l4 得到 ([0], [1])。这让我感到惊讶,尤其是在 in 子句 (l3, l4) 中使用 t 之间的区别,无论是生成器理解还是列表理解,与 if 子句 (@987654329) 没有区别@),它在哪里。

l1 = [(x for x in (0, 1) if x == t) for t in (0, 1)]
l2 = [[x for x in (0, 1) if x == t] for t in (0, 1)]
l3 = [(x for x in [t]) for t in (0, 1)]
l4 = [[x for x in [t]] for t in (0, 1)]
print([(*map(list, l),) for l in (l1, l2, l3, l4)])

能否请您详细解释一下有关此类表达的规则?相关文档的链接?理由?

【问题讨论】:

  • 我强烈建议不要编写依赖于这种语义的代码。
  • @KarlKnechtel,我无意中注意到它,解决了 2021 年代码出现的第 13 天。要听从您的建议,或对一组可取的做法发表我自己的明智意见,我需要清楚地了解发生了什么在。否则我什至不知道我什么时候要写一些你建议避免的东西。
  • 来自已删除的答案(但可能有助于更好地理解):尝试任何其他列表:>>>​​ l1 = [(x for x in (0, 1, 2) if x == t ) for t in (0, 1, 2)] >>> (*map(next, l1),) (2, 2, 2) 我的评论:当然是 (2, 2, 2),我的用例确实有更多的价值,我为这个问题提取了它的本质。你的回答也是我的第一个猜测,但它仍然不能解释 l1 和 l3 之间的区别。而 t 在每种情况下的确切范围仍然难以捉摸。
  • “否则我什至不知道我什么时候要写一些你建议避免的东西。”如果不需要,就不要关闭可变值?
  • @KarlKnechtel,它显然不是可变的,看不到赋值(简单或复合)运算符。如果您的意思是任何具有延迟评估的代码(包括生成器)都不应该引用可变变量(包括 for 循环的控制变量,无论是在 for 语句中还是任何类型的理解中),那么它就很牵强限制。并不是我强烈反对你的建议,只是想权衡利弊;也许之后我会分享你的结论。但无论如何,对于这个问题,这是一个切题的讨论。

标签: python closures list-comprehension generator semantics


【解决方案1】:

当你执行时

l1 = [(x for x in (0, 1) if x == t) for t in (0, 1)]

l1 是一个生成器列表,每个生成器都包含对相同“捕获”变量t 的引用。创建l1[0] 时,t 的值为0,但尚未评估生成器。当l1[1]创建时,t的值为1,之后不再修改。您可以使用以下方法进行检查:

c1 = l1[0]
c2 = l1[1]
print(c1.gi_frame.f_locals)
print(c2.gi_frame.f_locals)
print(c1.gi_frame.f_locals['t'] is c2.gi_frame.f_locals['t'])

至于为什么l3 不会发生这种情况,我理解是为了构建 生成器,必须评估它们的限制,所以必须评估[t] 以及在创建生成器时创建的列表。一个修改过的脚本,有助于理解接下来会发生什么(为了便于比较,将原始代码注释掉):

def testequal(x, t):
    print(f"Called with x={x}, t={t}")
    return x == t

def generate_limits():
    print("Creating limits")
    return (0, 1)

def generate_list(t):
    print(f"creating list with t={t}")
    rv = [t,]
    return rv

print("Creating l1...")
# l1 = [(x for x in (0, 1) if x == t) for t in (0, 1)]
l1 = [(x for x in generate_limits() if testequal(x,t)) for t in (0, 1)]
print("Creating l2...")
l2 = [[x for x in (0, 1) if x == t] for t in (0, 1)]
print("Creating l3...")
# l3 = [(x for x in [t]) for t in (0, 1)]
l3 = [(x for x in generate_list(t)) for t in (0, 1)]
print("Creating l3...")
l4 = [[x for x in [t]] for t in (0, 1)]
print("Evaluating..." )
print([(*map(list, l),) for l in (l1, l2, l3, l4)])

【讨论】:

  • 是的,就是这样!我使用此代码(跟踪评估顺序,与您的类似)得出了相同的解释(包括基本原理)来验证:github.com/ByteEater-pl/WriteCodeAlmostEveryDay/blob/master/…。但是,就像我通常做的那样(我显然已经在回答自己的问题方面享有很高的声誉以增加它),我等了一段时间,看看是否有人给出答案,甚至可能比我做得更好。所以谢谢和荣誉!
  • 此外,PEP 289 实际上确实有详细信息。它将生成器表达式的语义指定为等效于通过句法转换获得的生成器函数。它使用for 循环,其中的引用警告(包括在闭包中)在许多语言中广为人知。
猜你喜欢
  • 2018-12-28
  • 2017-12-27
  • 2011-12-19
  • 1970-01-01
  • 1970-01-01
  • 2016-10-04
  • 1970-01-01
  • 1970-01-01
  • 2014-06-08
相关资源
最近更新 更多