“学习Python,第四版。”提到:
在嵌套函数中查找封闭范围变量 后来被称为..
但是,我认为当一个函数退出时,它的所有本地引用都会消失。
def makeActions():
acts = []
for i in range(5): # Tries to remember each i
acts.append(lambda x: i ** x) # All remember same last i!
return acts
makeActions()[n]
对于每个n
都是相同的,因为变量i
在调用时以某种方式查找。 Python如何查找此变量?难道它根本不存在因为makeActions已经退出了吗?为什么Python不执行代码直观建议,并在循环运行时用for循环中的当前值替换i来定义每个函数?
答案 0 :(得分:8)
我认为当您将i
视为名称而不是某种值时会发生什么。你的lambda函数做的事情就像“取x:查找i的值,计算i ** x”......所以当你实际运行该函数时,它会查找i
然后所以i
是4
。
您也可以使用当前号码,但必须让Python将其绑定到另一个名称:
def makeActions():
def make_lambda( j ):
return lambda x: j * x # the j here is still a name, but now it wont change anymore
acts = []
for i in range(5):
# now you're pushing the current i as a value to another scope and
# bind it there, under a new name
acts.append(make_lambda(i))
return acts
这可能看起来令人困惑,因为你经常被告知变量和它的值是相同的 - 这是真的,但仅限于实际使用变量的语言。 Python没有变量,而是名称。
关于你的评论,实际上我可以更好地说明这一点:
i = 5
myList = [i, i, i]
i = 6
print(myList) # myList is still [5, 5, 5].
你说你将我改为6 ,这不是实际发生的事情:i=6
表示“我有一个值6
,我想给它命名{{ 1}}”。您已经使用i
作为名称的事实对Python没有任何意义,它只会重新分配名称,而不是更改它的值(仅适用于变量) )。
你可以说在i
中,无论myList = [i, i, i]
当前指向的值(数字5)都有三个新名称:i
。这与调用函数时发生的情况相同:参数被赋予新名称。但这可能违背了对列表的任何直觉......
这可以解释示例中的行为:您指定mylist[0], mylist[1], mylist[2]
,mylist[0]=5
,mylist[1]=5
- 难怪他们在您重新分配mylist[2]=5
时不会更改。如果i
是可静音的,例如列表,那么更改i
也会反映i
中的所有条目,因为您只有相同值的不同名称!
您可以在myList
左侧使用mylist[0]
的简单事实证明它确实是一个名称。我喜欢调用=
assign name operator :它在左边有一个名字,在右边有一个表达式,然后计算表达式(调用函数,查找名字后面的值) )直到它有一个值,最后给出值的名称。它确实没有改变任何东西。
好吧,引用(和指针)只有在我们有某种可寻址内存时才有意义。这些值存储在内存中的某个位置,引用会引导您到达该位置。使用引用意味着在内存中访问该位置并使用它执行某些操作。问题是Python使用了这些概念的 none !
Python VM没有内存概念 - 值浮在空间某处,名称是连接到它们的小标签(通过一点点红色字符串)。名称和值存在于不同的世界中!
编译函数时,这会产生很大的不同。如果您有引用,则您知道所引用对象的内存位置。然后你可以简单地用这个位置替换then引用。 另一方面,名称没有位置,因此您必须执行的操作(在运行时)遵循那个小红色字符串并使用另一端的任何内容。这就是Python编译函数的方式:Where 如果代码中有一个名称,它会添加一条指令来确定该名称代表什么。
所以基本上Python完全编译函数,但是名称在嵌套命名空间中被编译为查找,而不是作为对内存的某种引用。
当您使用名称时,Python编译器将尝试确定它所属的命名空间的位置。这会导致从它找到的命名空间加载该名称的指令。
这会让您回到原来的问题:在=
中,lambda x:x**i
被编译为i
命名空间中的查找(因为那里使用了makeActions
)。 Python不知道,也不关心它背后的价值(它甚至不必是一个有效的名称)。代码运行i
的代码会在其原始命名空间中查找并提供或多或少的预期值。
答案 1 :(得分:5)
创建闭包时会发生什么:
for
块i
的值就会不断变化 - i
的每个分配都会更新该框架中i
的绑定。i
的值不再更新。i
的任何值。因为在for循环中你创建闭包,但实际上不调用它们,调用时i
的值将是它毕竟的最后一个值循环完成了。makeActions
的未来调用将创建不同的帧。在这种情况下,您不会重复使用for循环的前一帧,也不会更新前一帧的i
值。简而言之:框架像其他Python对象一样被垃圾收集,在这种情况下,额外的引用保持在与for
块对应的框架周围,因此当for for时它不会被破坏循环超出范围。
要获得所需的效果,您需要为要捕获的i
的每个值创建一个新帧,并且需要使用对该新帧的引用来创建每个lambda。你不会从for
块本身获得它,但是你可以通过调用一个将建立新帧的辅助函数来获得它。请参阅THC4k的答案,了解这些方面的一种可能的解决方案。
答案 2 :(得分:1)
本地引用仍然存在,因为它们包含在本地作用域中,闭包保留了引用。
答案 3 :(得分:1)
我认为当一个函数退出时,它的所有本地引用都会消失。
除了在关闭时关闭的当地人。那些不会消失,即使它们本地的功能已经返回。
答案 4 :(得分:0)
直观地,人们可能会认为i
会在当前状态下被捕获,但事实并非如此。将每个图层视为名称值对的字典。
Level 1: acts i Level 2: x
每次为内部lambda创建闭包时,都会将引用捕获到第一级。我只能假设运行时将执行变量i
的查找,从级别2 开始并进入级别1 。由于您没有立即执行这些功能,因此它们都将使用i
的最终值。
专家?