3.2.命名空间和作用域
命名空间
Namespace : namespace 是一个从 name 到 object 的映射。现在大部分的 namespace 通过Python字典1实现,将来可能会改变实现方式。
namespace:中文称为命名空间,或者名字空间。
命名空间最重要的作用是避免名字冲突,各个命名空间是独立的,因此不同的命名空间可以存在同名变量。
Namespace 的种类
一般而言,name 产生地点决定其所处的 namespace。例如,在函数内定义的 name 会位于(函数的)局部命名空间。但使用了global
、nonlocal
语句会改变这种情况,这个在后面会说到。
-
built-in namespace:builtins 模块定义的所有名字存在内置命名空间,这些名字包括内置函数、内置异常、内置常量、内置类型。如果是Python 2,则是 __builtin__ 模块。
-
global namespace:每个模块都有全局命名空间,包括所有在模块最外层的作用域中定义的名字,例如类、函数、常量、被导入的模块。
-
local namespace:相对于全局命名空间,每一个局部作用域都有一个局部命名空间。例如,函数或类所定义的命名空间,记录了函数参数、函数内的变量、类属性、类方法等。
-
enclosing namespace:闭包命名空间不仅记录了当前嵌套函数内定义的变量,还记录了嵌套函数引用的外部变量。
Namespace生命周期
不同类型的命名空间有不同的生命周期:
- built-in:在 Python 解释器启动时创建,解释器退出时销毁
- global:在模块定义被解释器读入时创建,通常也会一直保存到解释器退出,除非使用
del
语句。 -
local:这里要区分
function
以及class
定义:-
函数的局部命名空间:在函数调用时创建,函数返回或者产生未被捕获的异常时销毁。
-
类定义的局部命名空间:在解释器读到类定义创建,离开类定义时创建class object。这个类对象实际上就是这个局部命名空间的包装(见官方对类定义的说明)。
-
查看Namespace
-
局部命名空间可以通过
locals()
来访问 -
全局 (模块级别)命名空间可以通过
globals()
来访问
虽然都是返回命名空间,但globals()
和locals()
有一点不一样。locals()
返回的是局部命名空间的副本,所以修改locals()
对象并不会影响局部名字空间。globals
返回是全局命名空间,而不是副本,所以修改globals()
对象会影响全局命名空间。
# 修改局部命名空间无效
def change_local():
x = 123
print("当前的局部命名空间", locals(), sep=":")
locals()["x"] = 6789
print("修改locals(),并不影响x的值", f"x={x}", sep=":")
change_local()
当前的局部命名空间:{'x': 123}
修改locals(),并不影响x的值:x=123
# 修改全局命名空间生效
y = 123
globals()["y"] = 456
print("修改全局命名空间生效:", f"y={y}")
修改全局命名空间生效: y=456
作用域
scope : scope 是Python程序的一块文本区域(即一个代码区域),在这个区域可以直接访问namespace 。
直接访问意味着无须特性的指明引用。在Python中,直接访问是指直接使用name访问对象,如
name
,这会在命名空间搜索名字name
;而间接访问是指使用形如objname.attrname
的方式,即引用对象的属性,这不会在命名空间搜索名字attrname
,而是搜索名字objname
,再访问其属性。
换而言之,作用域其实就是一个命名空间可以发生作用的代码区域,发生作用是指命名空间可以被作用域直接访问。
作用域与命名空间的关系
命名空间保存着名字到对象的映射,映射关系在作用域中被定义,同时作用域可以通过引用命名空间的名字访问对象。代码区域可以直接引用哪个命名空间中的名字,它也就是哪个命名空间的作用域。有些代码区域可以访问多个命名空间的名字,那么它同时是多个命名空间的作用域。
作用域是静态的(它只是代码文本),而命名空间是动态的,命名空间随着解释器的执行而产生。
有些文章认为动态的作用域就是命名空间。其实并不是,命名空间是一种映射,作用域是一块代码区域。
Python对象通过命名空间被访问,而作用域则限制了Python对象的使用范围。
名字搜索顺序(LEGB)
当一行代码需要使用名字X
所对应的值,Python会从当前层级的Namespace开始,并根据以下顺序,去查找名字X
:
- local namespace:包含局部名字的最内层(innermost)作用域,如函数、方法、类的内部局部作用域。
- Enclosing:根据嵌套层次从内到外搜索,包含非局部(nonlocal)非全局(nonglobal)名字的任意封闭函数的作用域。如两个嵌套的函数,内层函数的作用域是局部作用域,外层函数作用域就是内层函数的 Enclosing 作用域。
- global
- built-in
- NameError:如果 Python 在以上4个namespace找不到
X
,将放弃搜索并抛出 NameError 异常:NameError: name 'a' is not defined
程序在运行时,local、global、built-in三个namespace一定存在,但Enclosing namespace不一定存在。
Namespace和scope总结
- 不能在名字未定义前引用该名字(命名空间不存在该映射关系)。
- built-in、global两个命名空间的引入是不能够通过代码操作的,Python 解释器会自动引入它们。注意,这里说的是引入,而不是修改。built-in命名空间是不能被修改的(已经预定义好),但global命名空间可以被Python代码修改。
- 类定义、函数定义、推导式会引入local命名空间 ,闭包函数定义会引入enclosing命名空间。
-
会导致命名空间被修改的情况:
-
类定义、函数定义
-
赋值语句
-
import
语句、if
语句、for
语句、while
语句
-
-
if
、for
、while
语句并不会引入新的命名空间。 - 作用范围最大的命名空间是global namespace,但global namespace也只是模块级别的。
a
模块不能直接引用在b
模块定义的name,即使a
模块已经导入了整个b
模块:import b
。除非直接导入name:from b import name
。 - 导入
b
模块中其中一个name:from b import name
,整个b
模块都会被执行。因为 Python 并不知道name
在 b.py 文档的何处,为了能够找到name
,Python 需要执行整个 b.py。 import
语句不一定会改变global namespace,例如import
语句写在函数内。- 根据LEGB搜索规则,外层作用域不能引用内层作用域的变量。
def try_to_define_name():
'''函数中定义了名字i,并绑定了一个整数对象1'''
i = 1
# 引用名字i之前,先调用函数定义i
try_to_define_name()
# 在引用名字i之前,明明调用了函数,定义了名字i,可是还是找不到这个名字。
print(i)
NameError: name 'i' is not defined
虽然定义了名字i
,但是定义在了函数的局部作用域对应的局部命名空间中,按照LEGB
搜索规则,在全局作用域中自然访问不到局部作用域的命名空间;再者,函数调用结束后,这个命名空间被销毁了。
global
和 nonlocal
根据LEGB搜索规则,任何时候,Python程序都可以直接读取全局变量,但是却不可以在内层作用域直接改写上层变量。
可以读取全部变量,这容易理解,因为任何时候,global namespace都可以被访问。但为什么在内层作用域全局变量可以被访问,却不能被修改呢?这看起来像是Python的一个规定,但其实是很自然的事情。在Python中修改变量意味着将一个名字绑定到另一个对象,这需要使用assignment运算符=
。这意味着=
左边的名字会被加入当前的命名空间,名字对应着=
右边的对象。
例如下面的代码:
a = 1
def change_a():
a = 10
change_a()
a # 1
1
a = 10
语句在函数作用域中,当它被Python执行时,Python会将a
加入函数的局部命名空间,而不会将a
视作全局命名空间的a
。当我们在最外层作用域引用a
时,按照LEGB搜索规则,Python会从a
所在的作用域对应的命名空间(即全局命名空间)开始搜索a
对应的对象,而不会搜索局部命名空间。再者,此时函数change_a()
执行完之后,局部命名空间已经被销毁,即使想搜索也搜索不到。
相信你从上面的叙述已经意识到,如果想让Python在内层作用域修改外层作用域的变量,那就需要让Python知道这个变量来自外层作用域。global
语句和nonlocal
语句正是可以起到这样的作用。
global
语句声明列在其后的所有标识符将被解析为全局变量。 使用global时有以下两条限制:
- 在同一代码块中,列在global语句中的所有标识符不能在该global语句前出现。
- 列在global 语句后的标识符不能被定义成形参,不能出现在for循环控制的目标、类定义和函数定义,或者import语句中。
CPython实现细节
当前实现并未强制履行上面两条限制2,但程序不应该滥用这种自由,因为未来的版本可能会强制履行它们或者不留痕迹的改变程序含义。
nonlocal
语句声明列在其后的所有标识符关联到最近的 enclosing 作用域里定义过的同名变量(不包括全局变量,只是最近的 enclosing 作用域的变量)。
def scope_test():
def do_local():
spam = "local spam of do_local"
def do_nonlocal():
nonlocal spam
spam = "nonlocal spam of do_nonlocal"
def do_global():
global spam
spam = "global spam of do_global"
spam = "test spam of scope_test"
# do_local函数内部的赋值不影响scope_test作用域的spam
do_local()
print("After local assignment:", spam)
# do_nonlocal函数内部的赋值影响scope_test作用域的spam,但是不影响全局的spam
do_nonlocal()
print("After nonlocal assignment:", spam)
# do_global 函数内声明的spam存在于全局作用域,而不影响scope_test作用域的
do_global()
print("After global assignment:", spam)
scope_test()
print("In global scope:", spam)
After local assignment: test spam of scope_test
After nonlocal assignment: nonlocal spam of do_nonlocal
After global assignment: nonlocal spam of do_nonlocal
In global scope: global spam of do_global
nonlocal
语句和global
语句有两个不同点:
其一,global
语句只是声明标识符来存在于全局命名空间,并不会在当前作用域创建该标识符;nonlocal
语句则会在子函数命名空间中创建与父函数变量同名的标识符:
gv = 'a global var'
def func():
global gv
lv = 'a local var'
print(locals())
def inn_func():
nonlocal lv
global gv
print(locals())
return inn_func
a = func()
a()
{'lv': 'a local var'}
{'lv': 'a local var'}
之所以 nonlocal
语句与 global
语句的处理不同,是因为全局变量的作用域生存期很长,在模块内随时都可以访问,而父函数的局部作用域在父函数执行完毕后便会直接释放,因此 nonlocal
语句必须将父函数的标识符和引用写入enclosing namespace。
其二,global
语句可以声明全局作用域还没存在的名字,而nonlocal
语句只能将它声明的名字关联到enclosing作用域中已经存在的绑定:
# nonlocal 语句之前的eclosing作用域不存在spam 则报错
def scope_test():
def do_nonlocal():
# 父函数作用域要先定义spam,否则报错
nonlocal spam
spam = "nonlocal spam of do_nonlocal"
scope_test()
File "<ipython-input-1-e77162de9535>", line 5
nonlocal spam
^
SyntaxError: no binding for nonlocal 'spam' found
nonlocal
语句只能声明的名字会绑定到离nonlocal
语句最近的enclosing 作用域(如果enclosing 作用域存在nonlocal
语句声明的名字):
def scope_test():
spam = "local spam of scope_test"
def do_local():
spam = "local spam of do_local"
print("After local assignment:", spam)
def do_nonlocal():
# 通过nonlocal声明将spam和最近的enclosing作用域的spam关联
# 所以关联的是do_local函数的spam
nonlocal spam
spam = "nonlocal spam of do_nonlocal"
do_nonlocal()
print("After nonlocal assignment:",spam)
do_local()
print("After nonlocal assignment, scope_test's spam:",spam)
scope_test()
After local assignment: local spam of do_local
After nonlocal assignment: nonlocal spam of do_nonlocal
After nonlocal assignment, scope_test's spam: local spam of scope_test
因此nonlocal
语句重新绑定的name的真实作用域是不清楚的,这取决于已经存在的name本来位于那一层作用域。
nonlocal
语句和global
语句有一个相同点:
当前作用域不能在nonlocal
语句和global
语句之前绑定两者声明的标识符:
# global
def scope_test():
spam = "conflict"
global spam
spam = "nonlocal spam of do_nonlocal"
scope_test()
# nonlocal
def scope_test():
spam = "local spam of scope_test"
def do_nonlocal():
spam = "conflict"
nonlocal spam
spam = "nonlocal spam of do_nonlocal"
do_nonlocal()
print("After nonlocal assignment, scope_test's spam:",spam)
scope_test()
File "<ipython-input-1-d9548726e53c>", line 5
global spam
^
SyntaxError: name 'spam' is assigned to before global declaration
类的作用域和命名空间
类定义的作用域和函数定义的作用域都可以产生局部命名空间,但它们执行机制不一样,有不小的区别。
其一,函数初始化时不会被执行,类初始化时会被执行。
Python读入函数定义时,函数不会被执行:
def func():
print("executed")
Python读入类定义时,类作用域的代码会被执行:
class A:
print("executed_A")
def A_func(self):
print("executed_A_func")
class B:
print("executed_B")
class C:
print("executed_C")
executed_A
executed_B
executed_C
这看起来感觉有点不可思议,大家都是定义,为什么类定义会被执行,难道不应该是定义被引用时才执行吗?
其实这也是很自然的事情。Python解释器读入函数定义时,只需要在当前命名空间绑定函数名,不需要创建函数的局部命名空间,自然不需要执行函数作用域的代码。
命名空间的动态性:命名空间在作用域被执行时才产生。
而类定义实际上是类局部命名空间的包装,因此Python解释器读入类定义时,不仅需要在当前命名空间绑定类名,还需要创建类局部命名空间。而命名空间在作用域被执行时才会产生,因此类定义需要在读入的时候就被执行。注意:类方法内的作用域和函数一样,是不会被执行的。
为什么读入类定义就需要创建类局部命名空间?
因为类具有属性和方法,创建类属性就必须执行代码(进行赋值)。那为什么在读入类定义就创建类属性呢?因为类是创建实例、生成其他类(涉及继承、重载等)的对象,必须在实例之前被创建,在代码加载时就被创建有助于提高效率和降低逻辑复杂度。
另外,类还有一个特点:类可以直接被调用,不一定要先创建实例。这个特点就要求类初始化时就创建类命名空间。例如,创建一个类用于统一管理某一类常量:
class Math_Constant:
pi = 3.1415926
e = 2.7182818
sqrt_2 = 1.4142135
rho = 1.3247195
如果Math_Constant
的类属性需要创建一个实例才能被调用,那就太麻烦了,而且类方法、静态方法也就没了意义。
而方法本质是函数,不会在读入定义时被执行。
为什么读入函数定义时不需要创建函数局部命名空间?
一般来说,函数的局部命名空间主要与参数有关,而参数在函数被调用时才传入,创建了命名空间也意义不大。另外函数不像类那么复杂,也就没必要读入定义时就创建局部命名空间。
其二,类的局部命名空间不在名字搜索路径中。
例如:
class A:
pi = 3.1415926
def print_pi(self):
print(pi)
A().print_pi()
NameError: name 'pi' is not defined
类A
是print_pi
方法的外层作用域,按照设想,print_pi
方法中找不到变量pi
,应该会到上一层作用域中查找,但是print_pi
方法并没有在类A
的作用域中查找,而是直接抛出异常。
又例如:
class A():
a = 1
b = [a + i for i in range(3)] #NameError: name 'a' is not defined
执行上段代码,我们可以发现在类 A
内列表推导式无法调取 a
的值。
Python3的列表推导式也会产生局部命名空间。
但在函数中,完全没问题:
def func():
a = 1
b = [a + i for i in range(3)]
print(b)
func() #[1, 2, 3]
因此,类A
中的 a
不同于函数 func
中的 a
在局部命名空间中可以被任意引用。之所以强调”不可以被任意读取”,原因在于在类A
的局部空间内,a
在当前层级的作用域是可以被直接引用的:
class A():
a = 1
c = a + 2
A.c # 3
3
因此,类内可以产生局部命名空间的代码(方法、推导式等)严格上不是作用域。