浅谈Python3中nonlocal关键字
变量作用域与闭包
在了解Python的nonlocal之前,我们首先要先来了解Variable Scope(变量作用域)与Closure(闭包)。
变量作用域
我们先看以下代码:
1 |
number = 5
|
但凡有点Python基础的应该都能知道它的运行结果是什么,我也就不再啰嗦了。
然而,以下代码运行结果或许会让你感到疑惑:
1 |
number = 5
|
首先提问一下:请问该代码输出结果是什么?你可能会认为输出结果是:5。然而将代码运行后发现,它竟然报错了:
1 |
Traceback (most recent call last): |
看看错误提示:无法访问没有与值关联的局部变量“number”,什么情况?
这就是我们要先了解的东西之一:Variable Scope
Variable Scope是指名字(Name)能够在代码中被访问和解析的区域。比如局部变量只能在对应的局部作用域中使用,而全局变量则属于模块级作用域。
在讨论Python的名字解析时,我们经常会遇到三种概念:Global Variable(全局变量)、Local Variable(局部变量)与Free Variable(自由变量)。它们描述的是一个名字在特定代码块中的绑定和解析关系,而不是三种互斥的数据类型。
Variable Scope也通常按照LEGB规则分为四个层次:Local Scope(局部作用域)、Enclosing Scope(嵌套作用域)、Global Scope(全局作用域)和Built-in Scope(内置作用域)。我们会在讲解变量时提到这几个作用域,但是后面两个(嵌套作用域和Built-in作用域)不会提到
Global Variable与Local Variable
现在我们先来讲解global var:
1 |
global_var = "This is a global variable" # 位于global scope
|
这一段代码定义了一个global var,func中return这个variable。
查看OUTPUT发现,在函数内部是成功return了global_var的,这也就证明该variable能够被这个function找到,因为global var是位于global scope之中的。
这里需要注意,Python中的Global Scope通常指当前模块的全局命名空间,并不意味着整个Python程序中的所有模块都能直接访问这个变量。其他模块需要通过import等方式才能使用它。
然而下面这一段代码却报错了:
1 |
def func():
|
可以看到,因为in_local在func内部,所以该variable被视为local variable,并且该variable也位于local scope中,只能在func内部使用,所以处于func之外的print就无法找到这个local variable进而报错。
这便是Variable Scope:名字只能在符合其作用域规则的区域中被解析。global var声明在全局作用域中,所以它能被当前模块中的function、class、method等代码访问;local var则属于某个特定的局部作用域,只能在对应的代码块中使用。
这里还有一点需要注意:Python中的if、for、while等普通控制流语句并不会创建新的局部作用域。例如:
1 |
for i in range(3):
|
可以看到,即使在for循环外部,我们依然能够访问i和local_var。
这是因为Python的普通循环和条件语句不会像函数一样创建新的局部作用域,它们仍然使用当前代码块的作用域。
另外,名字的作用域与对象的生命周期并不是同一个概念。函数执行结束后,函数内部的局部名字就不能再通过该函数的局部作用域访问了,但这并不意味着对应的对象一定会被销毁。比如闭包就能够保留对外部变量的引用,我们后面会讲到。
回到开头的demo2.py,你应该也了解了为什么会报错了,因为number被视为了local var。
但是为什么number在demo1.py中还是global var,而在demo2.py中赋了个值就成local var了呢?
其实是因为Python在编译代码块时,会对其中的名字进行Symbol Table Analysis(符号分析)。在分析过程中,如果发现number在函数主体内存在赋值等绑定操作,且没有global或nonlocal声明,那么Python就会将number视为该函数的local var。
也就是说,并不是等到赋值语句执行时,number才变成local var,而是在编译阶段就已经确定了它的局部属性。
为了更好理解,我们查看一下两份代码的bytecode:
1 |
>>> dis.dis(fun)
|
可以看到,demo1.py的number为global var。
再看看demo2.py:
1 |
>>> dis.dis(fun)
|
这里补充一点:以上bytecode以特定版本的CPython为例。Python 3.12引入了LOAD_FAST_CHECK来检查局部变量是否已经初始化;LOAD_FAST则是读取已经确定可用的局部变量。
这下你应该知道了为什么demo2中会报错了。
至于free var是什么,我们在讨论nonlocal时会提到。
也许你在阅读刚才那段文字的时候会有一个问题:为什么Python要在执行函数之前分析函数中的变量?
因为Python在解析名字时采用就近原则(LEGB),即先在L(Local Scope)中查找,再在E(Enclosing Scope)中查找,再在G(Global Scope)中查找,最后在B(Built-in Scope)中查找。
闭包
如果你学习过或者写过Python的decorator(装饰器),那么你一定会了解到Closure。但是,本章并不讨论装饰器,所以不会出现什么看不懂的地方。下面我们开始介绍Closure。
假如说我们有一个函数f,函数的里面又嵌套一个函数u,我们要在函数u内return函数f中的variable,该如何处理?
可以这么干:
1 |
def f():
|
上述代码中的u函数引用了外层函数f中的num,这里就形成了闭包(Closure)。
以下是维基百科对Closure的介绍
Wiki百科
在计算机科学中,闭包是由一个函数和与其相关的引用环境组合而成的实体。该引用环境包含了这个函数所在的作用域中所有局部变量的引用。闭包允许函数访问其词法作用域外部的变量。
这个定义强调了Closure的两个关键要素:
- 由函数和其相关的引用环境组成。
- 引用环境包含了函数所引用的外层作用域中的变量。
Closure允许在函数内部访问其外部词法作用域中的variable。即使外部函数已经执行完毕,闭包仍然能够保持对相关变量的引用,这使得Closure具有捕获和保持外部状态的能力。
我们还可以通过Python提供的attribute来查看闭包捕获的free variable:
1 |
print(my_fun.code.co_freevars)
|
输出大致如下:
1 |
('num',) |
其中,co_freevars可以查看函数引用的自由变量名称,而__closure__则可以查看与这些自由变量对应的cell对象。
好的,那么基本了解完这两个概念之后,我们开始讲解本章的主角nonlocal。
nonlocal 的介绍及其使用
nonlocal是在Python 3.0时加入的。我们可以使用它在一个嵌套function内部声明一个variable,并规定这个variable来自最近的、包含该名字绑定的外层函数作用域。
我们可以使用nonlocal在嵌套function中重新绑定外层函数作用域中的变量。
请看以下例子:
1 |
def f():
|
我们定义了一个函数f,f之中有u,u中print x的值,f中也print x的值。可以发现,输出了2和1而不是2和2,这是因为u里面的x其实是创建了一个新的local variable,而不是修改了f的x。
这个在作用域那也提到过:Python在编译u函数时发现了x = 2这个绑定操作,所以u中的x则被视为该函数的local variable,而不会被当成f的variable。
所以,为了解决上述问题,我们就可以使用nonlocal关键字:
1 |
def f():
|
成功解决!
不过这里需要注意,nonlocal只能引用已经存在于外层函数作用域中的名字,不能用来创建一个新的外层变量。
例如:
1 |
def f():
|
这段代码会直接产生SyntaxError,因为外层函数作用域中并不存在已经绑定的x。
另外,nonlocal指向的是最近的、包含该名字绑定的外层函数作用域,而不一定是最外层函数。我们看一个例子:
1 |
def outer():
|
可以看到,inner中的nonlocal x修改的是middle中的x,而不是outer中的x。这就是nonlocal的最近外层函数作用域规则。
Free Variable与nonlocal
在前文提到的Closure中,Wiki介绍到:闭包是由一个函数和与其相关的引用环境组合而成的实体。该引用环境包含了函数所引用的外层作用域中的变量。
这些在函数内部引用、但在当前函数中没有绑定,而是在外层函数作用域中定义的变量,就是Free Variable(自由变量)。

使用nonlocal关键字就是为了处理free variable。刚才提到:u里的x创建了local variable,而非修改f的x,但使用nonlocal就告诉了Python,要使用最近的、包含该名字绑定的外层函数作用域中的变量。
此外,还有一点需要注意:nonlocal主要用于重新绑定外层作用域中的名字,并非修改对象的必要条件
例如:
1 |
def outer():
|
可以看到,这里并没有使用nonlocal,但是依然成功修改了外层的列表。
这是因为items.append(520)修改的是列表对象本身,并没有重新绑定items这个名字。
而下面的代码则不同:
1 |
def outer():
|
这里的items = [520]是一次新的名字绑定操作,因此Python会将items视为inner函数中的local variable。
如果希望重新绑定外层的items,就需要使用nonlocal:
1 |
def outer():
|
小结
变量作用域(Variable Scope):
- 全局变量(Global Variable):在模块级作用域中绑定的变量,可以被当前模块中的函数等代码通过全局名字解析规则访问。
- 局部变量(Local Variable):在当前代码块中绑定的变量。对于函数来说,局部变量属于该函数的局部作用域。
- 自由变量(Free Variable):在当前函数中被引用,但在当前函数中没有绑定,而是在外层函数作用域中绑定的变量。
- 全局作用域(Global Scope):模块级命名空间。
- 局部作用域(Local Scope):当前函数的局部命名空间。
- 嵌套作用域(Enclosing Scope):外层函数的作用域,可以存在多层。
- 内置作用域(Built-in Scope):Python内置命名空间,例如print、len等。
闭包(Closure):
- 由函数和其相关的引用环境组成。
- 引用环境包含了函数所引用的外层作用域中的变量。
- 允许函数访问其外部作用域中的变量,即使外部函数执行完了,闭包也能继续对相关变量的引用。
nonlocal关键字:
- 用于在嵌套函数中重新绑定外层函数作用域中的变量。
- 解决在嵌套函数内部赋值时,Python将名字视为当前函数局部变量的问题。
- 告诉Python使用最近的、包含该名字绑定的外层函数作用域中的变量。
- 如果不存在对应外层变量,则产生SyntaxError。
- 它处理的是名字的重新绑定,不是修改对象本身,修改列表、字典等可变对象一般不需要nonlocal。
- Title: 浅谈Python3中nonlocal关键字
- Author: 旁白
- Created at: 2024-01-13 09:45:00
- Updated at: 2026-11-08 00:41:25
- Link: https://xn--vfv958a.com/article/浅谈python3中nonlocal关键字/
- License: This work is licensed under CC BY-NC-SA 4.0.
Sign in
Authentication required to comment
Delete this comment?