浅谈Python3中nonlocal关键字

浅谈Python3中nonlocal关键字

旁白 Lv.5201314

变量作用域与闭包

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

变量作用域

我们先看以下代码:

1
2
3
4
5
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是指变量能够在代码中生效的区域。比如局部变量只能在函数、方法、循环等区域中生效,而全局变量可以在整个程序中生效。

变量总共分为三类:Global Variable(全局变量)Local Variable(局部变量)Free Variable(自由变量)

Variable Scope也能分为四类:Global Scope(全局作用域)Local Scope(局部变量)Enclosing Scope(嵌套作用域)Built-in Scope(Built-in 作用域)。我们会在讲解变量时提到这几个作用域,但是后面两个(嵌套作用域和Built-in 作用域)不会提到

Global Variable与Local Variable

现在我们先来讲解global var:

1
2
3
4
5
6
7
8
9
10
11
12
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之中的。

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

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

print(local_var) # 这里尝试打印局部变量local_var
"""
===OUTPUT===
Traceback (most recent call last):
File "/exmaple.py", line 4, in <module>
print(local_var)
^^^^^^^^^
NameError: name 'local_var' 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声明在某个特定局部作用域中,所以它只能在该作用域中使用,且当该区域代码执行完毕后,区域中的local var的生命周期也会结束。

回到开头的demo2.py,你应该也了解了为什么会报错了,因为number被视为了local var。但是为什么number demo1.py中还是global var,而在demo2.py中赋了个值就成local var了呢?其实是因为Python编译器会在函数定义时编译函数,而在编译过程中发现了number在函数主体内赋了值,所以Python视其为local var。

为了更好理解,我们查看一下两文件的bytecode:

1
2
3
4
5
6
7
8
>>> dis.dis(fun)
2 0 RESUME 0

3 2 LOAD_GLOBAL 1 (NULL + print) #注释1
12 LOAD_GLOBAL 2 (number) #注释2
22 CALL 1
30 POP_TOP
32 RETURN_CONST 0 (None)

#注释1:这里加载global var——print

#注释2:加载global var——number

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

再看看demo2.py:

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

3 2 LOAD_GLOBAL 1 (NULL + print)
12 LOAD_FAST_CHECK 0 (number) #注释1
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) #注释2
40 CALL 1
48 POP_TOP
50 RETURN_CONST 0 (None)

#注释1:加载local var——number。这说明number被视为local var

#注释2:与#注释1相同(题外话:在python3.12中LOAD_FAST不能触发UnboundLocalError)

这下你应该知道了为什么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
def f():
num=520
def u():
return num

return u

my_fun = f()
print(my_fun())


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

上述代码中的u函数与num=520,就是Closure(闭包)

base3

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

Wiki百科

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

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

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

Closure允许在函数内部访问该函数外部的variable,函数外部作用域已经结束的情况下依旧可以访问。这使得Closure具有捕获和保持外部状态的能力。

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


nonlocal 的介绍及其使用

nonlocal是在Python 3.2时加入的。我们拿它在一个function内部声明一个variable,并且规定这个variable是来自最近且非全局的作用域,我们可以使用nonlocal在嵌套function中修改Closure作用域的变量。

请看以下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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。这个在作用域那也提到过:u函数先被编译,所以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
def f():
x = 1
def u():
nonlocal x # 使用nonlocal告诉Python,我们要引用最近作用域中的x而不是创建一个x
x = 2 # 这样,这里的x = 2就变成了让closure中的x = 2
print("u:", x)

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

f()


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

成功解决!

Free Variable与nonlocal

前文提到的Closure中Wiki介绍到:闭包是由一个函数和与其相关的引用环境组合而成的实体。该引用环境包含了函数所在的作用域中所有局部变量的引用。这些在函数内部引用但在函数外部定义的变量就是Free Variable(自由变量)

base3

使用nonlocal关键字就是为了处理free variable。刚才提到:u里的x创建了local variable,不是修改f的x。但使用nonlocal就告诉了Python,要使用最近的且不在全局作用域中的变量,即使用free var。

小结

变量作用域(Variable Scope):

  • 全局变量(Global Variable):在整个程序中都有效的变量。
  • 局部变量(Local Variable):只在定义它的局部作用域内有效的变量,生命周期仅限于该区域。
  • 自由变量(Free Variable):闭包中函数内部引用但在函数外部定义的变量。
  • 全局作用域(Global Scope):处于这个作用域中的任何变量、函数、类等在整个程序都有效。
  • 局部作用域(Local Scope):处于这个作用域中的变量等等只在定义区域有效(如只在函数中有效)。

闭包(Closure):

  • 由函数和其相关的引用环境组成的实体。
  • 引用环境包含了函数所在作用域中的局部变量的引用。
  • 允许函数内部访问函数外部的变量。即使外部作用域已经结束,闭包也能够保持对这些变量的引用。

nonlocal关键字:

  • 用于在嵌套函数中修改闭包作用域的变量。
  • 解决在闭包函数内部创建局部变量而不是修改闭包函数外部变量的问题。
  • 告诉Python使用最近的且不在全局作用域中的变量,也就是使用free var。
  • 标题: 浅谈Python3中nonlocal关键字
  • 作者: 旁白
  • 创建于 : 2024-01-13 01:45:10
  • 更新于 : 2024-08-05 03:07:59
  • 链接: https://xn--vfv958a.com/2024/01/13/浅谈Python3中nonlocal关键字/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论