如何在嵌套函数中计算来自外部作用域的变量?

How do variables from an outer scope get evaluated in nested functions?

提问人:leonbear 提问时间:7/27/2018 最后编辑:Mad Physicistleonbear 更新时间:7/27/2018 访问量:602

问:

代码优先:

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什么?似乎除了函数对象的引用之外,还有更多的东西。

Python 参数

评论

0赞 Joran Beasley 7/27/2018
父作用域可以读取的任何变量,内部作用域也可以读取...但是,编辑值会因范围而异......没有办法替换父局部范围的变量......内部函数只能编辑其局部变量或位于全局空间中并全局显式调用的变量
2赞 user2357112 7/27/2018
@JoranBeasley:允许您在 Python 3 中分配给闭包变量。nonlocal
0赞 Joran Beasley 7/27/2018
哎呀,我忘了那个:P我说错了
0赞 Mad Physicist 7/27/2018
希望您不介意标题更改。
0赞 abarnert 7/27/2018
您在这里寻找什么级别的细节?我写了一个答案,从接近表面水平开始,大约到一半,但如果你对编译器如何知道何时发出以及 C API 用于处理单元对象之类的事情感兴趣,或者如果你只需要在语言不可知的层面上温和地介绍闭包的概念, 这可能并不理想。LOAD_DEREF

答:

6赞 abarnert 7/27/2018 #1

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

特别是,可以从内部访问内部的局部变量。(即使你 ,这些值保持活动状态,因此只要存在,闭包仍然有效。outer_funcinner_funcreturn inner_funcinner_func

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


这是如何工作的?

好吧,语句2 只是一个语句,就像任何其他语句一样。它的作用是这样的:def

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

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

但是从中返回的函数对象是一个动态创建的新事物,并且它具有对嵌入其中的局部变量的引用。每次运行时,它都会从相同的闭包中创建一个新的闭包,每个闭包捕获当前的本地环境。_make_closureouter_funcinner_func<code>


细节稍微复杂一些,而且在某种程度上,它们因实现而异,因此这将是特定于 CPython 的。

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

  • 如果一个变量本来局部的,但嵌套函数引用了该变量而没有赋值给它,或者有一个语句,那么它在外部函数中是一个单元格变量,在内部函数中是一个自由变量。3nonlocal

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

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

  • 上面的伪代码的作用是将单元格从外部函数的框架复制到嵌套函数上名为 的特殊属性。_make_closure__closure__

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

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

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


1. 这是具有一系列相关但不同含义的词之一。“闭包”可以表示在函数中捕获本地命名空间的技术,也可以表示捕获的命名空间,也可以表示附加了捕获的命名空间的函数,也可以表示捕获的命名空间中的某个变量。通常,从上下文中可以明显看出您指的是哪一个。如果没有,你必须说“闭包捕获”或“闭包函数”或“闭包变量”之类的话。

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

3. 如果你有多层嵌套,它实际上会更复杂,但让我们忽略这一点。

评论

0赞 Mad Physicist 7/27/2018
打败我:)
0赞 leonbear 7/27/2018
它非常详细,我想我可能需要更多的阅读。谢谢你的帮助!
2赞 Mad Physicist 7/27/2018 #2

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

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

因此,in 的值将是调用时的任何值。引用的是命名空间,而不是名称本身。如果返回(想想装饰器),命名空间将是固定的,并且只能通过对它的特殊引用来访问。painner_funcouter_funcouter_funcinner_func

评论

0赞 abarnert 7/27/2018
很好的工作,将实现排除在概念之外。Python 解释器可以将类似链映射的东西用于其本地环境(就像一些 Lisp 所做的那样),这将是完全有效的,并且您的解释仍然完全适合,而我的解释确实不会。(另外,关于函数对象也随身携带的好点。__globals__
0赞 leonbear 7/27/2018
精确!谢谢你的帮助!
0赞 Mad Physicist 7/27/2018
@abarnert。感谢您的高度赞扬