嵌套函数如何评估外部作用域的变量?

时间:2018-07-27 03:09:38

标签: python parameters

代码优先:

def another_func(func):
    func()


def outer_func(pa=1, pb=2):
    def inner_func():
        print(pa)
    print(type(inner_func))
    another_func(inner_func)

if __name__ == '__main__':
    outer_func()
    #print "1"

我不确定“ inner_func”调用的参数是“ outer_func”,但是它在“ outer_func”的主体中。当被another_func调用时,如何“知道”有一个“ pa”?

我的意思是,当另一个在“ outer_func”中调用时,实际上传递给another_func的是什么?似乎除了函数对象的引用以外,还有其他东西。

2 个答案:

答案 0 :(得分:5)

Python中的函数对象不仅仅是函数,它们是closures 1 ,它们带有对执行def语句的本地环境的引用。

特别是,可以从outer_func内部访问inner_func内部的局部变量。 (即使您return inner_func,这些值也会保持活动状态,因此只要inner_func处于活动状态,闭包仍将起作用。)

如果在nonlocal内添加inner_func语句,它甚至可以从outer_func的主体中重新分配局部变量。


这是如何工作的?

好吧,def语句 2 就像其他语句一样。它所做的是这样的:

inner_func = _make_closure(<code from compiling inner_func body>, locals())

<code from compiling inner_func body>实际上是一个常量值-编译器会在code时将模块中每个函数的主体编译为常量import对象。

但是从那个_make_closure返回的函数对象是一个动态创建的新事物,它引用了烘焙到其中的局部变量。每次运行outer_func时,它都会从同一inner_func创建一个新的<code>闭包,每个闭包都捕获当前的本地环境。


细节有些复杂-在某种程度上,实现之间会有所不同,因此这将是CPython特定的。

编译器的一部分工作是弄清楚函数中每个名称是什么类型的变量。您可能已经阅读了关于全局变量与局部变量的规则(当且仅当您在函数体中某处对名称进行了赋值并且没有global语句时,变量才是局部变量)。但是闭包使事情变得更加复杂。

  • 如果变量本来应该是,但是嵌套函数引用该变量但未分配该变量,或者具有nonlocal语句,则该变量为外部函数,内部函数中的自由变量。 3

  • 当解释器调用函数时,它将创建一个frame对象,该对象保存本地命名空间-对该函数所有本地变量的引用。

  • 但是单元格变量是特殊的:解释器为每个单元格创建一个特殊的cell对象,并且对该单元格的引用进入了名称空间,因此每次在值前面都有一个额外的取消引用您访问或更改它。

  • 上面的_make_closure伪代码所做的是将单元格从外部函数的框架复制到嵌套函数__closure__上的特殊属性。

  • 然后,当您调用内部函数时,解释器将这些单元格从__closure__复制到该函数的框架中。

  • 因此,外部函数的框架和内部函数的框架都引用相同的单元格,这就是它们可以共享变量的方式。

有关更多信息,请参见inspect模块的文档,该文档向您展示如何在交互式解释器中找到__closure__co_freevars之类的内容,以及dis模块可以让您查看函数被编译为的实际字节码。


1。这是具有一系列相关但不同含义的单词之一。 “关闭”可以表示在函数中捕获本地名称空间的技术,或者可以表示已捕获的名称空间,或者可以表示已附加捕获的名称空间的函数,或者可以表示捕获的名称空间中的变量之一。 。通常,从上下文中明显看出您的意思。如果不是,则必须说“关闭捕获”或“关闭函数”或“关闭变量”之类的内容。

2。如果您想知道,lambda表达式的工作方式与def语句完全相同。而且class的定义不完全相同,而是相似的。

3。如果您具有多层嵌套,实际上实际上仍然更加复杂,但是我们可以忽略它。

答案 1 :(得分:2)

如果函数与函数对象在一起,您似乎会混淆代码。读取源文件时,代码对象仅被评估一次。但是,每次调用inner_func时都会创建一个名为outer_func的新功能对象。发生这种情况是因为def语句是一种赋值类型:它将一个函数对象与指定的名称相关联。

当然,函数对象包含对其代码的引用,以及对其将需要操作的所有名称空间的引用,包括其父级的非本地名称空间和全局模块名称空间。

因此,painner_func的值将与调用时outer_func中的值相同。引用指向名称空间,而不是名称本身。如果outer_func返回(认为是装饰器),则名称空间将是固定的,并且只能通过inner_func对其的特殊引用进行访问。