关闭是如何实施的?
“学习 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 不按照代码直观的建议进行操作,并通过在循环运行时将 i 替换为 for 循环中的当前值来定义每个函数?
"Learning Python, 4th Ed." mentions that:
the enclosing scope variable is looked up when the nested functions
are later called..
However, I thought that when a function exits, all of its local references disappear.
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]
is the same for every n
because the variable i
is somehow looked up at call time. How does Python look up this variable? Shouldn't it not exist at all because makeActions
has already exited? Why doesn't Python do what the code intuitively suggests, and define each function by replacing i with its current value within the for loop as the loop is running?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(5)
我认为,当您将
i
视为名称而不是某种值时,会发生什么是非常明显的。您的 lambda 函数会执行类似“取 x:查找 i 的值,计算 i**x”之类的操作...因此,当您实际运行该函数时,它会查找i
所以i
是4
。您也可以使用当前数字,但必须让 Python 将其绑定到另一个名称:
这可能看起来令人困惑,因为您经常被告知变量和它的值是同一件事 - 这是正确的,但仅限于语言实际上使用变量。 Python 没有变量,只有名称。
关于你的评论,实际上我可以更好地说明这一点:
你说你将 i 更改为 6,这并不是实际发生的情况:
i=6
意味着“我有一个值6
,我想将其命名为i
”。您已经使用i
作为名称这一事实对 Python 来说无关紧要,它只会重新分配名称,而不更改它的值(这只适用于变量)。您可以说,在
myList = [i, i, i]
中,无论i
当前指向的值(数字 5)都会获得三个新名称:mylist[ 0]、mylist[1]、mylist[2]
。这与调用函数时发生的情况相同:参数被赋予新名称。但这可能违背了关于列表的任何直觉......这可以解释示例中的行为:您分配
mylist[0]=5
,mylist[1]=5
,mylist[2]=5
- 难怪当您重新分配i
时它们不会改变。如果i
是可变的,例如列表,那么更改i
也会反映在myList
中的所有条目,因为您只有 相同值的不同名称!您可以在
=
的左侧使用mylist[0]
这一简单事实证明它确实是一个名称。我喜欢调用=
分配名称运算符:它在左侧接受一个名称,在右侧接受一个表达式,然后计算该表达式(调用函数,查找名称后面的值)直到它有一个值,最后将名称赋予值。它不会改变任何东西。对于 Marks 关于编译函数的评论:
嗯,只有当我们拥有某种可寻址内存时,引用(和指针)才有意义。这些值存储在内存中的某个位置,并且引用会引导您到达该位置。使用引用意味着进入内存中的那个位置并用它做一些事情。问题是 Python 没有使用这些概念!
Python VM 没有内存的概念 - 值漂浮在空间中的某处,名称是连接到它们的小标签(通过一个红色的小字符串)。名称和值存在于不同的世界中!
当您编译函数时,这会产生很大的差异。如果您有引用,您就知道所引用的对象的内存位置。然后你可以简单地用这个位置替换然后引用。
另一方面,名称没有位置,因此您必须做的(在运行时)是遵循那个小红色字符串并使用另一端的任何内容。这就是 Python 编译函数的方式:
只要代码中有一个名称,它就会添加一条指令来弄清楚该名称代表什么。
所以基本上 Python 确实完全编译函数,但名称被编译为嵌套命名空间中的查找,而不是作为对内存的某种引用。
当您使用名称时,Python 编译器将尝试找出它属于哪个名称空间。这会产生一条从它找到的命名空间加载该名称的指令。
这让您回到原来的问题:在 lambda x:x**i 中,
i
被编译为makeActions
命名空间中的查找(因为那里使用了i
)。 Python 不知道,也不关心其背后的值(它甚至不必是有效的名称)。运行i
的代码会在其原始命名空间中查找,并给出或多或少的预期值。I think it's pretty obvious what happens when you think of
i
as a name not some sort of value. Your lambda function does something like "take x: look up the value of i, calculate i**x" ... so when you actually run the function, it looks upi
just then soi
is4
.You can also use the current number, but you have to make Python bind it to another name:
It might seem confusing, because you often get taught that a variable and it's value are the same thing -- which is true, but only in languages that actually use variables. Python has no variables, but names instead.
About your comment, actually i can illustrate the point a bit better:
You said you changed i to 6, that is not what actually happend:
i=6
means "i have a value,6
and i want to name iti
". The fact that you already usedi
as a name matters nothing to Python, it will just reassign the name, not change it's value (that only works with variables).You could say that in
myList = [i, i, i]
, whatever valuei
currently points to (the number 5) gets three new names:mylist[0], mylist[1], mylist[2]
. That's the same thing that happens when you call a function: The arguments are given new names. But that is probably going against any intuition about lists ...This can explain the behavior in the example: You assign
mylist[0]=5
,mylist[1]=5
,mylist[2]=5
- no wonder they don't change when you reassign thei
. Ifi
was something muteable, for example a list, then changingi
would reflect on all entries inmyList
too, because you just have different names for the same value!The simple fact that you can use
mylist[0]
on the left hand of a=
proves that it is indeed a name. I like to call=
the assign name operator: It takes a name on the left, and a expression on the right, then evaluates the expression (call function, look up the values behind names) until it has a value and finally gives the name to the value. It does not change anything.For Marks comment about compiling functions:
Well, references (and pointers) only make sense when we have some sort of addressable memory. The values are stored somewhere in memory and references lead you that place. Using a reference means going to that place in memory and doing something with it. The problem is that none of these concepts are used by Python!
The Python VM has no concept of memory - values float somewhere in space and names are little tags connected to them (by a little red string). Names and values exist in separate worlds!
This makes a big difference when you compile a function. If you have references, you know the memory location of the object you refer to. Then you can simply replace then reference with this location.
Names on the other hand have no location, so what you have to do (during runtime) is follow that little red string and use whatever is on the other end. That is the way Python compiles functions: Where
ever there is a name in the code, it adds a instruction that will figure out what that name stands for.
So basically Python does fully compile functions, but names are compiled as lookups in the nesting namespaces, not as some sort of reference to memory.
When you use a name, the Python compiler will try to figure out where to which namespace it belongs to. This results in a instruction to load that name from the namespace it found.
Which brings you back to your original problem: In
lambda x:x**i
, thei
is compiled as a lookup in themakeActions
namespace (becausei
was used there). Python has no idea, nor does it care about the value behind it (it does not even have to be a valid name). One that code runs thei
gets looked up in it's original namespace and gives the more or less expected value.创建闭包时会发生什么:
i
的值就会不断变化 - 每次对i
的赋值都会更新该帧中i
的绑定框架。i
的值不再更新。i
的任何值。由于在 for 循环中您创建闭包,但实际上调用它们,因此调用时i
的值将是它的最后一个值所有循环完成后。i
值。简而言之:帧像其他 Python 对象一样被垃圾收集,在这种情况下,与
for
块相对应的帧周围保留了一个额外的引用,因此当 for 时它不会被破坏。循环超出范围。为了获得您想要的效果,您需要为您想要捕获的每个
i
值创建一个新框架,并且需要使用对该新框架的引用来创建每个 lambda。您不会从for
块本身获得该信息,但可以通过调用将建立新框架的辅助函数来获得该信息。请参阅 THC4k 的答案,了解一种可能的解决方案。What happens when you create a closure:
for
block.i
in that frame keeps changing as long as the for loop is running – each assignment toi
updates the binding ofi
in that frame.i
is no longer updated.i
is in the parent frame at the time of invocation. Since in the for loop you create closures, but don't actually invoke them, the value ofi
upon invocation will be the last value it had after all the looping was done.makeActions
will create different frames. You won't reuse the for loop's previous frame, or update that previous frame'si
value, in that case.In short: frames are garbage-collected just like other Python objects, and in this case, an extra reference is kept around to the frame corresponding to the
for
block so it doesn't get destroyed when the for loop goes out of scope.To get the effect you want, you need to have a new frame created for each value of
i
you want to capture, and each lambda needs to be created with a reference to that new frame. You won't get that from thefor
block itself, but you could get that from a call to a helper function which will establish the new frame. See THC4k's answer for one possible solution along these lines.本地引用会持续存在,因为它们包含在闭包保留对本地范围的引用中。
The local references persist because they're contained in the local scope, which the closure keeps a reference to.
除了那些被关闭的当地人之外。即使它们所属的函数已经返回,它们也不会消失。
Except for those locals which are closed over in a closure. Those do not disappear, even when the function to which they are local has returned.
直觉上,人们可能会认为
i
会以其当前状态被捕获,但事实并非如此。将每一层视为名称值对的字典。每次为内部 lambda 创建闭包时,您都会捕获对一级的引用。我只能假设运行时将执行变量
i
的查找,从级别2开始并进入级别1。由于您没有立即执行这些函数,因此它们都将使用i
的最终值。专家?
Intuitively one might think
i
would be captured in its current state but that is not the case. Think of each layer as a dictionary of name value pairs.Every time you create a closure for the inner lambda you are capturing a reference to level one. I can only assume that the run-time will perform a look-up of the variable
i
, starting in level 2 and making its way to level 1. Since you are not executing these functions immediately they will all use the final value ofi
.Experts?