如何实现闭包?

时间:2010-06-30 01:25:06

标签: 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

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

5 个答案:

答案 0 :(得分:8)

我认为当您将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].

你说你将我改为6 ,这不是实际发生的事情:i=6表示“我有一个值6,我想给它命名{{ 1}}”。您已经使用i作为名称的事实对Python没有任何意义,它只会重新分配名称,而不是更改它的值(仅适用于变量) )。

你可以说在i中,无论myList = [i, i, i]当前指向的值(数字5)都有三个新名称:i。这与调用函数时发生的情况相同:参数被赋予新名称。但这可能违背了对列表的任何直觉......

这可以解释示例中的行为:您指定mylist[0], mylist[1], mylist[2]mylist[0]=5mylist[1]=5 - 难怪他们在您重新分配mylist[2]=5时不会更改。如果i是可静音的,例如列表,那么更改i也会反映i中的所有条目,因为您只有相同值的不同名称

您可以在myList左侧使用mylist[0]的简单事实证明它确实是一个名称。我喜欢调用= assign name operator :它在左边有一个名字,在右边有一个表达式,然后计算表达式(调用函数,查找名字后面的值) )直到它有一个值,最后给出值的名称。它确实没有改变任何东西

对于Marks关于编译函数的评论:

好吧,引用(和指针)只有在我们有某种可寻址内存时才有意义。这些值存储在内存中的某个位置,引用会引导您到达该位置。使用引用意味着在内存中访问该位置并使用它执行某些操作。问题是Python使用了这些概念的 none

Python VM没有内存概念 - 值浮在空间某处,名称是连接到它们的小标签(通过一点点红色字符串)。名称和值存在于不同的世界中!

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

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

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

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

答案 1 :(得分:5)

创建闭包时会发生什么:

  • 闭包是使用指向 frame (或粗略地, block )的指针构建的,它在以下情况下创建:在这种情况下,for
  • 闭包实际上假定该帧的共享所有权,通过递增帧的引用计数并将指针存储到闭包中的该帧。反过来,该框架保留了对其所包含的框架的引用,用于在堆栈中进一步捕获的变量。
  • 只要for循环正在运行,该框架中i的值就会不断变化 - i的每个分配都会更新该框架中i的绑定。
  • 一旦for循环退出,框架就会从堆栈中弹出,但它不会像通常那样被抛弃!相反,它保持不变,因为闭包对框架的引用仍然是活动的。但是,此时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的最终值。

专家?