如何实现闭包?

2024-05-15 01:55:55 发布

您现在位置:Python中文网/ 问答频道 /正文

“学习Python,第4版”提到:

the enclosing scope variable is looked up when the nested functions are later called..

但是,我认为当一个函数退出时,它所有的局部引用都会消失。在

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都是相同的,因为变量{}是在调用时查找的。Python如何查找这个变量?它不应该因为makeActions已经退出而根本不存在吗?为什么Python不按照代码直观的建议去做,并在循环运行时用for循环中的当前值替换i来定义每个函数?在


Tags: the函数forisfunctionsvariablenestedscope
3条回答

我认为当你把i看作一个名称而不是某种值时,会发生什么。你的lambda函数执行类似“取x:查找i的值,计算i**x”。。。所以,当你实际运行这个函数时,它会查找i,然后,所以i就是{}。在

也可以使用当前编号,但必须使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没有变量,只有名称。在

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

^{pr2}$

你说你把i改成了6,实际上并不是这样:i=6意思是“我有一个值,6,我想把它命名为i”。事实上,您已经使用i作为名称,这对Python来说无关紧要,它只是重新分配名称,而不是更改它的值(这只适用于变量)。在

您可以说在myList = [i, i, i]中,i当前指向的任何值(数字5)都会得到三个新名称:mylist[0], mylist[1], mylist[2]。这与调用函数时发生的情况相同:参数被赋予新的名称。但这可能违背了任何关于列表的直觉。。。在

这可以解释示例中的行为:指定mylist[0]=5mylist[1]=5mylist[2]=5-难怪当你重新分配i时,它们不会改变。如果i是可静音的,例如一个列表,那么更改i也会影响到{}中的所有条目,因为对于同一个值,只有不同的名称!在

您可以在=的左边使用mylist[0]这个简单的事实证明它确实是一个名称。我喜欢调用=assign name操作符:它在左边取一个名称,在右边取一个表达式,然后对表达式求值(调用函数,查找名称后面的值),直到它有一个值,最后将名称赋给该值。它不会改变任何东西。在

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

好吧,引用(和指针)只有在我们有某种可寻址内存时才有意义。存储在你记忆中的某个地方。使用引用意味着去内存中的那个地方并用它做一些事情。问题是Python没有使用这些概念!在

pythonvm没有内存的概念,值在空间的某处浮动,名称是连接到它们的小标记(通过一个红色的小字符串)。名字和价值存在于不同的世界!在

这在编译函数时会产生很大的不同。如果有引用,则知道所引用对象的内存位置。然后您可以简单地用这个位置替换Then reference。 另一方面,名称没有位置,因此(在运行时)您要做的是跟随红色小字符串并使用另一端的任何内容。这就是Python编译函数的方式:在哪里 只要代码中有一个名字,它就会添加一条指令来找出这个名字代表什么。在

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

当您使用一个名称时,Python编译器将尝试找出它属于哪个名称空间。这将导致从找到的命名空间加载该名称的指令。在

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

本地引用之所以持久,是因为它们包含在本地作用域中,而本地作用域是闭包保持引用的地方。在

结束时会发生什么:

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

简而言之:帧就像其他Python对象一样被垃圾回收,在本例中,一个额外的引用被保留在与for块相对应的帧周围,这样当for循环超出范围时,它就不会被破坏。在

要获得所需的效果,需要为要捕获的i的每个值创建一个新帧,并且每个lambda都需要创建一个对该新帧的引用。您不会从for块本身获得该值,但可以通过调用helper函数来获得,该函数将建立新的框架。请参阅THC4k的答案,了解沿着这些思路的一个可能的解决方案。在

相关问题 更多 >

    热门问题