以7.3.2节定义的 out() 函数内的 inner() 函数为例,在 out() 函数所在的区域不能调用 inner() 函数(见7.3.2节中的报错信息),其根源即为这里介绍的作用域(Scope)。每个名称所引用的对象,都有各自的创建位置,也都有各自能够产生作用的区域,此区域称为作用域——在 Python 中,名称的作用域由其所在位置决定。Python 解释器会根据名称定义的位置和及其在代码中的引用位置来确定作用域,以下按照搜索顺序列出各个作用域(如图7-3-2所示):
x,解释器首先在该函数本地的最内部作用域内搜索它。x 不在本地作用域中,而是出现在嵌套函数内部的函数中,则解释器将搜索闭包的作用域。.py 文件中顶层所声明的变量能产生作用的区域。如果以上两个搜索都没有结果,那么解释器接下来会查看全局作用域。x ,那么解释器将尝试搜索内置作用域。
图7-3-2 作用域
这就是 Python 语言中关于作用域搜索的 LEGB 规则。按照此顺序,如果找不到该变量或名称,则会抛出 NameError 异常。比如:
>>> wo_xihuan_kan_laoqi_xiede_book
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'wo_xihuan_kan_laoqi_xiede_book' is not defined
变量 wo_xihuan_kan_laoqi_xiede_book 肯定不会出现在当前的交互模式中的任何地方,Python 解释器依照 LEGB 规则找不到它。
又如:
>>> x = 'global'
>>> def f():
... print(x) # (7)
...
>>> f()
global
根据 LEGB 规则,注释(7)中的 x 能在全局作用域中搜索到,故打印出 'global' 。如果创建一个嵌套函数,其内部的变量与全局作用域变量同名,如下述代码:
>>> x = 'global'
>>> def f():
... x = 'enclosing'
... def g():
... print(x) # (8)
... return g
...
>>> out = f()
>>> out()
enclosing
按照 LEGB 规则在闭包作用域中搜索变量 x ,则 注释(8)打印的结果是 'enclosing' ,而不是 'global' 。
如果在 g() 里面再定义 x ,结果会如何?
>>> def f():
... x = 'enclosing'
... def g():
... x = 'local' # (9)
... print(x)
... return g
...
>>> out = f()
>>> out()
local
在 g() 内部增加了注释(9),搜索的时候,先在本地作用域内找到了它,于是打印的结果为 'local' 。
理解了 Python 解释器对名称的搜索规则之后,再看如下示例:
>>> a = 1
>>> def foo():
... print(a + 1) # (10)
...
>>> foo()
2
>>> a
1
毫无疑问,注释(10)中的变量 a 即为全局作用域中的 a = 1 。但是,如果这样做:
>>> a = 1
>>> def bar():
... a = a + 1
... return a
...
>>> bar()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in bar
UnboundLocalError: local variable 'a' referenced before assignment
对比函数 foo() 和 bar() ,逻辑上似乎没有什么差别,都是要将 a 增加 1 ,在这里却报错了。
情节还会继续翻转,稍加改动,就免除了异常(请仔细观察下面代码与上述代码的异同):
>>> def bar():
... a = 1
... a = a + 1
... return a
...
>>> bar()
2
要想知其所以然,必须要从两个内置函数 globals() 和 locals() 说起。先看对这两个函数的最权威说明(分别来自之后 help(globals) 和 help(locals) 后的帮助文档)。
globals()
Return the dictionary containing the current scope's global variables.
# 返回含有当前作用域全局变量的字典
locals()
Return a dictionary containing the current scope's local variables.
# 返回含有当前作用域的局部(本地)变量的字典
下面就用这两函数一探究竟。为了看得清爽,请重启交互模式,再按照如下演示进行操作:
Python 3.9.4 (v3.9.4:1f2e3088f3, Apr 4 2021, 12:32:44)
[Clang 6.0 (clang-600.0.57)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> x = 'foo'
>>> globals()
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'x': 'foo'}
首先创建一个名为 x 的变量,然后执行 globals() 函数,返回的是一个字典,在这个字典中包含了刚才创建的变量及其所引用的对象。这就是 globals() 的作用,它以字典的形式返回当前全局作用域的成员。
通常,我们通过变量的名称 x 访问它引用的对象,现在看到了上述返回的字典,可以通过它间接得到:
>>> x
'foo'
>>> globals()['x']
'foo'
当然,一般的代码中是不会用 globals()['x'] 的,这里只是在说明 globals() 函数的返回值。由此肯定会想到,如果给这个字典增加一个键值对,是不是相当于增加了一个全局作用域的变量?
>>> globals()['y'] = 1000
>>> globals()
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'x': 'foo', 'y': 1000}
>>> y
1000
的确如此。因为 globals() 的返回值就是字典,甚至于还可以通过它修改全局作用域变量的值(建议读者自己尝试)。
另外一个内置函数 locals() ,与 globals() 类似:
>>> def f(p, q):
... s = "python"
... print(locals())
...
>>> m, n = 1, 2
>>> f(m, n)
{'p': 1, 'q': 2, 's': 'python'}
在函数 f() 中调用 locals() 时,locals() 返回了表示函数的本地作用域的字典。注意,除了本地定义的变量 s 之外,本地作用域还包括函数参数 p 和 q ,它们也在 f() 的本地作用域内。
现在回到前面反复翻转的问题上:
>>> a = 1
>>> def bar():
... a = 1
... print(locals()) # (11)
... a = a + 1
... print(locals()) # (12)
... return a
...
>>> bar()
{'a': 1}
{'a': 2}
2
>>> globals()
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'a': 1, 'bar': <function bar at 0x7fc34c65d430>}
为了观察方便,增加了注释(11)和(12)两行,分别打印出执行到该行时的本地作用域字典。从输出结果中可知,在 bar() 函数内的本地作用域中有变量 a 及其相应的值。此外,globals() 的返回值显示,在全局作用域中有 a = 1 。这说明,在本地作用域不能修改全局作用域中的变量。
如果去掉注释(11)前面 a = 1 ,即成为执行后会报错的那个函数:
>>> def bar():
... print(locals())
... a = a + 1 # (13)
... print(locals())
... return a
...
>>> bar()
{}
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in bar
UnboundLocalError: local variable 'a' referenced before assignment
注释(13)之前,在本地作用域中没有变量 a ,注释(13)试图通过赋值语句创建一个本地作用域的变量 a ,然而该赋值语句右侧又用到变量 a ,由于与在本地试图创建的变量同名,故将它视为本地作用域的变量,又因为这个变量此前没有定义,这就相当于用一个没有定义的变量与整数 1 做加法。故必然报错。
或许读者会说,“我的意思是注释(13)中等号右侧的变量 a 是全局作用域中定义的 a = 1”,可惜 Python “不懂我的心”。《Python 之禅》中有这样一句:“明瞭优于隐晦”(参阅第1章1.4节),所以那些“你懂得我的意思就是意思意思”的表述,不要出现在程序中。
>>> a = 1
>>> def bar():
... global a # (14)
... print(locals())
... a = a + 1
... print(locals())
... return a
...
>>> bar()
{}
{}
2
>>> globals()
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'a': 2, 'bar': <function bar at 0x7fc34c65d430>}
>>> a
2
注释(14)是一条语句,用以声明在 bar() 中出现的变量 a 指向全局作用域中的变量 a。虽然本地作用域中依然没有变量 a ,也不会影响 a = a + 1 的执行。并且,当使用 globals() 查看全局作用域时,发现 a 的值已经是 2 。
global 是 Python 关键词,它的作用就是声明某变量为全局作用域变量。再如:
>>> name
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'name' is not defined
>>> def book():
... global name
... name = 'learn python'
...
>>> book()
>>> name
'learn python'
本来全局作用域中不存在变量 name ,但是在函数 book() 中用 global 语句声明了一个名为 name 的全局作用域变量,当此函数执行之后,在全局作用域中就有 name 了。这说明 global 语句可以在任何需要的地方指定全局作用域的变量。
>>> def f():
... x = 20 # (15)
... def g():
... x = 40 # (16)
... g()
... print(x)
...
>>> f()
20
注释(15)的变量 x 在 f() 内(闭包作用域),不是全局作用域内。注释(16)的 x = 40(本地作用域)不会对注释(14)中的变量做出修改。当 g() 执行之后,闭包作用域中的 x 仍然是 20 。即使在注释(16)前面增加 global x 也不能修改 x 的值。
>>> def f():
... global x
... x = 20
... def g():
... x = 40
... g()
... print(x)
...
>>> f()
20
>>> def f():
... x = 20
... def g():
... global x
... x = 40
... g()
... print(x)
...
>>> f()
20
有没有办法在 g() 内部修改闭包作用域中的 x 呢?当然有,使用另外一个关键字 nonlocal ,用它发起一个语句。
>>> def f():
... x = 20
... def g():
... nonlocal x # (16)
... x = 40
... g()
... print(x)
...
>>> f()
40
在注释(16)之后,当 g() 中创建 x 时,它指的是最近的闭包作用域内的 x ,其定义在 f() 中。于是 print() 的结果显示为 40 。
必须要说明,在程序中使用 global 或 nonlocal 一定要谨慎,因为它们都会产生副作用。一般认为修改全局作用域中的变量不是明智之举,这不仅针对 Python ,其他编程语言亦如此。