概念
命名空间(namespace):是name到object的映射的集合,在Python中是基于字典实现。
作用域(scope):是Python程序的文本区域。在该区域,某个命名空间中的名字可以被直接引用。
命名空间
当一段代码在Python中执行时,它存在四个命名空间:local,nonlocal,global和built-in。
在执行过程中遇到了某个命名(name)(通俗来说,即Python中的变量)时,Python首先尝试在local命名空间中查找它,如果没有找到,就在nonlocal中查找。之后在global命名空间中查找,如果还是没有找到,接着在built-in命名空间中查找。如果都不存在,则被认为是一个错误,会抛出一个“NameError”异常。[1]
那么,何为local,global,built-in呢?local namespace:比如说一个函数里所有的局部变量。这个命名空间在函数调用时被创建,在函数返回的时候被删除。
nonlocal namespace: 比如外函数中所有的命名。
global namespace: 比如一个模块中定义的所有全局变量。这个命名空间在模块被import的时候创建,在解释器退出时退出。
built-in namespace:包括内置的函数命名(如abs(), str())和异常命名。它在Python解释器启动的时候被创建,在解释器退出的时候才被删除。[2]
作用域
作用域是一个和命名空间紧密结合的概念。如果在python程序中的某一段,这一段中的某一种命名空间(local, global, built-in)中的对象都可以直接访问,那么这个一段区域就被称作一个作用域.[3]
所以,在Python中,说某个变量的作用域是不准确的。准确的说法应该是,在作用域内,该命名空间中的这个对象可以被直接访问到。
在Python程序执行的过程中,至少有以下几个作用域,他们的命名空间(中的对象)可以直接访问:最内层作用域,最先被搜索,包括所有的局部命名。(local names)(Python中不适用变量,因为python中一切皆对象的思想,命名包括各种变量以及函数)
外层函数的局部作用域,包含了外层函数的所有局部命名。(not global, not local)
包含模块中所有全局命名的作用域。
最外层的作用域是包含了内置命名的命名空间。
在两个函数嵌套的时候,对于内部函数,它的局部作用域会引用当前函数内所有的局部命名。而对于外部函数,它的局部作用域会引用的命名空间就是整个模块的命名空间。
global 与 nonlocal
global和nonlocal的具体的作用必须结合Python中作用域理解。通常情况下,对于一个命名到底绑定了哪个对象,是按照从小的作用域到大的作用域的一个查找的过程,但有的时候,我们需要越过中间的层级,直接去上上级的作用域修改命名的绑定。
global:如果一个命名被声明为global,那么对于这个命名的所有的引用与赋值都会作用于模块的全局命名。也就是说,这个命名就会和global命名空间中的相同名字的命名绑定到内存中同一个对象。
nonlocal:nonlocal声明也是用于重新绑定命名,只不过它是用来重新绑定中间层次作用域中的变量。如果你想要在内层函数中使用外层函数中的变量,这个时候只是只读的。如果直接做赋值操作,相当于重新创建了一个相同名称,但作用域不同的命名。而如果使用+=这类的符号更是GG,因为这相当于使用了一个未声明的变量!使用nonlocal声明,就可以直到此时想要重新绑定的是既不是全局命名空间中的命名,也不是局部命名空间中的命名,而是外部函数中的命名。
命名空间与作用域的关系
作用域物理上指的是一段程序区域,在这个区域里的所有命名构成一个命名空间,在这个区域里,这个命名空间包含的所有命名都可以直接访问。
例子
a = 10
def has_local_a():
a = 15
print(locals())
def has_local_a_also():
a = 25
print(locals())
has_local_a()
has_local_a_also()
print(globals().keys())
#--------output------------
{'a': 15}
{'a': 25}
dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__builtins__', '__file__', '__cached__', 'a', 'has_local_a', 'has_local_a_also']))
在上面,has_local_a和has_local_a_also构成两个局部的作用域,其中的局部命名为a,所以当在这两个方程中使用的时候,解释器首先会在这个局部作用域中找a。如果找不到,就会去引用全局命名空间中的同名的变量。global 与 nonlocal
def test():
def do_local():
spam = "local spam"
def do_nonlocal():
nonlocal spam
spam = "nonlocal spam"
def do_global():
global spam
spam = "global spam"
spam = "test spam"
do_local()
print("after local assignment:", spam) #输出:test spam
do_nonlocal()
print("after nonlocal asssignment:", spam) #输出:nonlocal spam
do_global()
print("after global assignment:", spam) #输出:nonlocal spam
test()
print("in global scope:", spam) #输出:global spa
#--------output------------
after local assignment: test spam
after nonlocal asssignment: nonlocal spam
after global assignment: nonlocal spam
in global scope: global spam
这里例子许多人不理解的地方在于会什么调用do_global()后,spam竟然还是nonlocal spam.原因在于do_nonlocal与do_global中绑定的spam是两个不同的spam,一个spam存在于全局命名空间,一个变量存在于局部的命名空间。而spam是为全局命名spam绑定了一个新的global spam而局部的spam并没有改变。我们可以在do_global()后调用globals()与locals()查看。
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x000001D1EA0F4F98>, '__spec__': None, '__annotations__': {}, '__builtins__': , '__file__': 'D:/Work/Research/Tutorial/Python/test.py', '__cached__': None, 'test': } # globals
{'do_global': .do_global at 0x000001D1EA365840>, 'do_nonlocal': .do_nonlocal at 0x000001D1EA3657B8>, 'do_local': .do_local at 0x000001D1EA365730>, 'spam': 'nonlocal spam'} # locals
参考文献
[1] Python变量的作用域和命名空间 杨冬