浅谈Python3中nonlocal关键字

浅谈Python3中nonlocal关键字

旁白
旁白 Lv.5201314

变量作用域与闭包

在了解Python的nonlocal之前,我们首先要先来了解Variable Scope(变量作用域)与Closure(闭包)。

变量作用域

我们先看以下代码:

1
2
3
4
5
6
number = 5

def fun():
print(number)

fun()

但凡有点Python基础的应该都能知道它的运行结果是什么,我也就不再啰嗦了。

然而,以下代码运行结果或许会让你感到疑惑:

1
2
3
4
5
6
7
number = 5

def fun():
print(number)
number = 3

fun()

首先提问一下:请问该代码输出结果是什么?你可能会认为输出结果是:5。然而将代码运行后发现,它竟然报错了:

1
2
3
4
5
6
7
Traceback (most recent call last):
File "/demo2.py", line 7, in <module>
fun()
File "/demo2.py", line 3, in fun
print(number)
^^^^^^
UnboundLocalError: cannot access local variable 'number' where it is not associated with a value

看看错误提示:无法访问没有与值关联的局部变量“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
2
3
4
5
6
7
8
9
10
11
12
13
global_var = "This is a global variable" # 位于global scope

def func():
return global_var # 返回global_var

print(func()) # print了func()且成功显示global_var


"""
===OUTPUT===
This is a global variable
============
"""

这一段代码定义了一个global var,func中return这个variable。

查看OUTPUT发现,在函数内部是成功return了global_var的,这也就证明该variable能够被这个function找到,因为global var是位于global scope之中的。

这里需要注意,Python中的Global Scope通常指当前模块的全局命名空间,并不意味着整个Python程序中的所有模块都能直接访问这个变量。其他模块需要通过import等方式才能使用它。

然而下面这一段代码却报错了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def func():
in_local = "This is a local variable" # 位于local scope

print(in_local) # 这里尝试打印局部变量in_local

"""
===OUTPUT===
Traceback (most recent call last):
File "/example.py", line 4, in <module>
print(in_local)
^^^^^^^^^
NameError: name 'in_local' is not defined
============
"""

可以看到,因为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
2
3
4
5
for i in range(3):
local_var = i

print(i) # 2
print(local_var) # 2

可以看到,即使在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
2
3
4
5
6
7
8
&gt;&gt;&gt; dis.dis(fun)
2 0 RESUME 0

3 2 LOAD_GLOBAL 1 (NULL + print)
12 LOAD_GLOBAL 2 (number) # 这里加载global var——number
22 CALL 1
30 POP_TOP
32 RETURN_CONST 0 (None)

可以看到,demo1.py的number为global var。

再看看demo2.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
&gt;&gt;&gt; dis.dis(fun)
2 0 RESUME 0

3 2 LOAD_GLOBAL 1 (NULL + print)
12 LOAD_FAST_CHECK 0 (number) # 加载local var——number,这说明number被视为local var。
14 CALL 1
22 POP_TOP

4 24 LOAD_CONST 1 (3)
26 STORE_FAST 0 (number)

5 28 LOAD_GLOBAL 1 (NULL + print)
38 LOAD_FAST 0 (number) # 与上面的注释相同,不过这里的number已经完成赋值,因此可以直接读取。
40 CALL 1
48 POP_TOP
50 RETURN_CONST 0 (None)

这里补充一点:以上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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def f():
num = 520

def u():
return num

return u

my_fun = f()
print(my_fun())


"""
===OUTPUT===
520
============
"""

上述代码中的u函数引用了外层函数f中的num,这里就形成了闭包(Closure)。

以下是维基百科对Closure的介绍

Wiki百科

在计算机科学中,闭包是由一个函数和与其相关的引用环境组合而成的实体。该引用环境包含了这个函数所在的作用域中所有局部变量的引用。闭包允许函数访问其词法作用域外部的变量。

这个定义强调了Closure的两个关键要素:

  • 由函数和其相关的引用环境组成。
  • 引用环境包含了函数所引用的外层作用域中的变量。

Closure允许在函数内部访问其外部词法作用域中的variable。即使外部函数已经执行完毕,闭包仍然能够保持对相关变量的引用,这使得Closure具有捕获和保持外部状态的能力。

我们还可以通过Python提供的attribute来查看闭包捕获的free variable:

1
2
print(my_fun.code.co_freevars)
print(my_fun.closure)

输出大致如下:

1
2
('num',)
(<cell at ...: int object at ...>,)

其中,co_freevars可以查看函数引用的自由变量名称,而__closure__则可以查看与这些自由变量对应的cell对象。

好的,那么基本了解完这两个概念之后,我们开始讲解本章的主角nonlocal。


nonlocal 的介绍及其使用

nonlocal是在Python 3.0时加入的。我们可以使用它在一个嵌套function内部声明一个variable,并规定这个variable来自最近的、包含该名字绑定的外层函数作用域。

我们可以使用nonlocal在嵌套function中重新绑定外层函数作用域中的变量。

请看以下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def f():
x = 1

def u():
x = 2
print("u:", x)

u()
print("f:", x)

f()


"""
===OUTPUT===
u: 2
f: 1
============
"""

我们定义了一个函数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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def f():
x = 1

def u():
nonlocal x # 使用nonlocal告诉Python,我们要引用最近的外层函数作用域中的x
x = 2 # 这样,这里的x = 2就变成了重新绑定外层作用域中的x
print("u:", x)

u()
print("f:", x)

f()


"""
===OUTPUT===
u: 2
f: 2
============
"""

成功解决!

不过这里需要注意,nonlocal只能引用已经存在于外层函数作用域中的名字,不能用来创建一个新的外层变量。

例如:

1
2
3
4
5
6
7
8
def f():
def u():
nonlocal x
x = 520

u()

f()

这段代码会直接产生SyntaxError,因为外层函数作用域中并不存在已经绑定的x。

另外,nonlocal指向的是最近的、包含该名字绑定的外层函数作用域,而不一定是最外层函数。我们看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def outer():
x = 1

def middle():
x = 2

def inner():
nonlocal x
x = 3

inner()
print("middle:", x)

middle()
print("outer:", x)

outer()


"""
===OUTPUT===
middle: 3
outer: 1
============
"""

可以看到,inner中的nonlocal x修改的是middle中的x,而不是outer中的x。这就是nonlocal的最近外层函数作用域规则。

Free Variable与nonlocal

在前文提到的Closure中,Wiki介绍到:闭包是由一个函数和与其相关的引用环境组合而成的实体。该引用环境包含了函数所引用的外层作用域中的变量。

这些在函数内部引用、但在当前函数中没有绑定,而是在外层函数作用域中定义的变量,就是Free Variable(自由变量)。

nonlocal

使用nonlocal关键字就是为了处理free variable。刚才提到:u里的x创建了local variable,而非修改f的x,但使用nonlocal就告诉了Python,要使用最近的、包含该名字绑定的外层函数作用域中的变量。

此外,还有一点需要注意:nonlocal主要用于重新绑定外层作用域中的名字,并非修改对象的必要条件

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def outer():
items = []

def inner():
items.append(520)

inner()
print(items)

outer()


"""
===OUTPUT===
[520]
============
"""

可以看到,这里并没有使用nonlocal,但是依然成功修改了外层的列表。

这是因为items.append(520)修改的是列表对象本身,并没有重新绑定items这个名字。

而下面的代码则不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def outer():
items = []

def inner():
items = [520]

inner()
print(items)

outer()


"""
===OUTPUT===
[]
============
"""

这里的items = [520]是一次新的名字绑定操作,因此Python会将items视为inner函数中的local variable。

如果希望重新绑定外层的items,就需要使用nonlocal:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def outer():
items = []

def inner():
nonlocal items
items = [520]

inner()
print(items)

outer()


"""
===OUTPUT===
[520]
============
"""

小结

变量作用域(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.
Comments

Sign inSign up to comment.

0 Comments
  • Latest
  • Oldest
  • Most liked
Loading comments…
Powered by Django
On this page
浅谈Python3中nonlocal关键字