如何实现闭包?

Mar*_*ark 21 python closures

"学习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
Run Code Online (Sandbox Code Playgroud)

makeActions()[n]每个人都是一样的,n因为变量i在调用时以某种方式查找.Python如何查找此变量?难道它根本不存在因为makeActions已经退出了吗?为什么Python不执行代码直观建议,并通过在循环运行时用for循环中的当前值替换i来定义每个函数?

从评论到THC4k:
我认为我错误地认为Python在内存中构建函数的方式.我在想,当遇到a makeActions()[n]或者a时n,Python会生成与该函数相关的所有必要的机器指令,并将其保存在内存中的某处.现在我认为它更像是将函数保存为文本字符串(并将其与闭包所需的引用捆绑在一起),并在每次调用函数时重新解析它.

Joc*_*zel 9

我认为这是很明显的,当你认为会发生什么i名字不是某种价值.你的lambda函数确实像"拍X:查找我的价值,我计算**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
Run Code Online (Sandbox Code Playgroud)

它可能看起来令人困惑,因为你经常被教导变量和它的值是相同的 - 这是真的,但仅限于实际使用变量的语言.Python没有变量,而是名称.

关于你的评论,实际上我可以更好地说明这一点:

i = 5 
myList = [i, i, i] 
i = 6
print(myList) # myList is still [5, 5, 5].
Run Code Online (Sandbox Code Playgroud)

你说你把我改为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没有内存概念 - 值浮动在空间的某个地方,名称是连接到它们的小标签(通过一个小的红色字符串).名称和值存在于不同的世界中!

编译函数时,这会产生很大的不同.如果您有引用,则您知道所引用对象的内存位置.然后你可以简单地用这个位置替换then引用.另一方面,名称没有位置,因此您必须执行的操作(在运行时)遵循那个小红色字符串并使用另一端的任何内容.这就是Python编译函数的方式:代码中有一个名称,它会添加一条指令来确定该名称的含义.

所以基本上Python完全编译函数,但是名称在嵌套命名空间中被编译为查找,而不是某种对内存的引用.

当您使用名称时,Python编译器将尝试确定它所属的命名空间的位置.这会导致从它找到的命名空间加载该名称的指令.

这让你回到原来的问题:在lambda x:x**i,在i编译器中编译为查找makeActions(因为i在那里使用).Python不知道,也不关心它背后的价值(它甚至不必是一个有效的名称).代码运行的代码i在其原始命名空间中查找并提供或多或少的预期值.


Owe*_* S. 5

创建闭包时会发生什么:

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

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

要获得所需的效果,您需要为i要捕获的每个值创建一个新帧,并且需要使用对该新帧的引用来创建每个lambda.你不会从for块本身那里得到它,但是你可以通过调用辅助函数来获得它,这将建立新的框架.请参阅THC4k的答案,了解这些方面的一种可能的解决方案.