关闭是如何实施的?

发布于 2024-09-07 18:25:36 字数 568 浏览 5 评论 0原文

“学习 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

§普罗旺斯的薰衣草 2024-09-14 18:25:36

我认为,当您将 i 视为名称而不是某种时,会发生什么是非常明显的。您的 lambda 函数会执行类似“取 x:查找 i 的值,计算 i**x”之类的操作...因此,当您实际运行该函数时,它会查找 i 所以i4

您也可以使用当前数字,但必须让 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].

你说你将 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 up i just then so i is 4.

You can also use the current number, but you have to make Python bind it to another name:

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

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:

i = 5 
myList = [i, i, i] 
i = 6
print(myList) # myList is still [5, 5, 5].

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 it i". The fact that you already used i 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 value i 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 the i. If i was something muteable, for example a list, then changing i would reflect on all entries in myList 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, the i is compiled as a lookup in the makeActions namespace (because i 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 the i gets looked up in it's original namespace and gives the more or less expected value.

凉薄对峙 2024-09-14 18:25:36

创建闭包时会发生什么:

  • 闭包是用指向它创建所在的框架(或者粗略地,)的指针构造的:在本例中,<代码>for块。
  • 闭包实际上通过增加帧的引用计数并将指向该帧的指针存储在闭包中来假定该帧的共享所有权。反过来,该帧保留对其所包含的帧的引用,以获取在堆栈中进一步捕获的变量。
  • 只要 for 循环运行,该帧中 i 的值就会不断变化 - 每次对 i 的赋值都会更新该帧中 i 的绑定框架。
  • 一旦 for 循环退出,帧就会从堆栈中弹出,但它不会像通常那样被丢弃!相反,它被保留下来,因为闭包对框架的引用仍然处于活动状态。但此时,i 的值不再更新。
  • 当调用闭包时,它会获取调用时父框架中的 i 的任何值。由于在 for 循环中您创建闭包,但实际上调用它们,因此调用时i的值将是它的最后一个值所有循环完成后。
  • 未来对 makeActions 的调用将创建不同的框架。在这种情况下,您不会重用 for 循环的前一帧,也不会更新该前一帧的 i 值。

简而言之:帧像其他 Python 对象一样被垃圾收集,在这种情况下,与 for 块相对应的帧周围保留了一个额外的引用,因此当 for 时它不会被破坏。循环超出范围。

为了获得您想要的效果,您需要为您想要捕获的每个 i 值创建一个新框架,并且需要使用对该新框架的引用来创建每个 lambda。您不会从 for 块本身获得该信息,但可以通过调用将建立新框架的辅助函数来获得该信息。请参阅 THC4k 的答案,了解一种可能的解决方案。

What happens when you create a closure:

  • The closure is constructed with a pointer to the frame (or roughly, block) that it was created in: in this case, the for block.
  • The closure actually assumes shared ownership of that frame, by incrementing the frame's ref count and stashing the pointer to that frame in the closure. That frame, in turn, keeps around references to the frames it was enclosed in, for variables that were captured further up the stack.
  • The value of i in that frame keeps changing as long as the for loop is running – each assignment to i updates the binding of i in that frame.
  • Once the for loop exits, the frame is popped off the stack, but it isn't thrown away as it might usually be! Instead, it's kept around because the closure's reference to the frame is still active. At this point, though, the value of i is no longer updated.
  • When the closure is invoked, it picks up whatever value of 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 of i upon invocation will be the last value it had after all the looping was done.
  • Future calls to makeActions will create different frames. You won't reuse the for loop's previous frame, or update that previous frame's i 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 the for 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.

暗地喜欢 2024-09-14 18:25:36

本地引用会持续存在,因为它们包含在闭包保留对本地范围的引用中。

The local references persist because they're contained in the local scope, which the closure keeps a reference to.

睡美人的小仙女 2024-09-14 18:25:36

我认为当一个函数退出时,它的所有本地引用都会消失。

除了那些被关闭的当地人之外。即使它们所属的函数已经返回,它们也不会消失。

I thought that when a function exits, all of its local references disappear.

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.

奶气 2024-09-14 18:25:36

直觉上,人们可能会认为 i 会以其当前状态被捕获,但事实并非如此。将每一层视为名称值对的字典。

    Level 1:
        acts
        i
    Level 2:
        x

每次为内部 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.

    Level 1:
        acts
        i
    Level 2:
        x

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 of i.

Experts?

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文