Python的命名空间解析
摘要:
- 什么是命名空间
- 命名空间有哪些
- 变量查找原则
- 分析一个UnboundLocalError的例子
什么是命名空间
首先说什么是命名空间呢!我们知道,在Python中,一切都是对象,然后通过 名字 去引用对象。例如我们执行了一条语句a = 3
,Python做的工作就是让名字a
去引用一个 Int 对象3
。这种名字和对象的对应关系都存储在一个字典中,而这个字典,就叫做命名空间。任何一个模块,类,实例,函数,都有其命名空间,这个命名空间可以通过__dict__
来访问。比如下面这段代码:
>>> import sys
>>> m = sys.modules[__name__]
>>> print(m.__dict__ is globals())
True
>>> a = 3
>>> globals().get('a')
32
在上面的例子中,我们首先通过sys.modules
获取了当前运行的模块,然后通过__dict__
成员获取了当前模块的命名空间,然后发现globals
函数返回的就是当前模块的命名空间。当我们执行了a = 3
语句以后,可以看到,当前模块的命名空间中多了一个a
成员,其值为3。
命名空间有哪些
前面我们讲到,每个类,函数,实例,模块都有其命名空间,可以通过__dict__
成员来访问,如下面代码所示:
>>> import sys
>>> main_module = sys.modules[__name__]
>>> module_member = 'module'
>>> class C:
... class_member = 'class'
>>> c = C()
>>> c.instance_member = 'instance'
>>> def func():
... func_member = 'function'
... fls = sys._getframe(0).f_locals
... print('fls and locals() are same object? %s' % (fls is locals()))
... print(fls['func_member'])
>>>
>>> print(main_module.__dict__['module_member'])
module
>>> print(C.__dict__['class_member'])
class
>>> print(c.__dict__['instance_member'])
instance
>>> func()
fls and locals() are same object? True
function
在上面的代码中,我们分别访问了,模块,类,实例,函数的命名空间,并在其中取出了我们命名的变量。其中,访问函数的命名空间有些特殊,我们是通过_getframe
函数访问了当前栈帧,然后在栈帧中找到了当前函数的命名空间。当然这个函数的命名空间也可以通过locals
函数访问到。
变量查找原则
从上面的例子中,我们了解到,命名空间其实就是存储名字和对象的对应关系的,那么当我们通过一个名字来获取一个对象的时候,其查找命名空间的顺序是怎样的呢?在Python中遵循着 LEGB 的查找原则。
LEGB的意思就是Local -> Enclosing Function -> Global -> Builtins
。
遇到一个名字的时候,Python解释器首先会去本地命名空间(Local
)中查找,然后再去其所在函数的作用域(Enclosing Function
)中查找,如果还没找到,就去全局命名空间(Global
)中查找,最后会去__builtin__
这个模块中查找,__builtin__
模块在 Python3 中已经重命名成了builtins
,详见 stackoverflow 上关于builtin
的提问,感谢@欧阳杰童鞋指出这个问题。
例如下面的一段代码:
>>> x = 1234
... __builtin__.s = 'Hello, World!'
... def test():
... y = 'abc'
... print(x)
... print(y)
... print(s)
...
... test()
...
1234
abc
Hello, World!
在上面的代码中,s 是builtins
命名空间中的名字,y 是local
命名空间中的名字,x 是Global
命名空间中的名字,它们都能够被正确找到并打印出来。
分析一个UnboundLocalError例子
在最后,我们来分析这样一段代码:
x = 1234
def test():
print(x)
x = 'abc'
test()
---------------------------------------------------------------------------
UnboundLocalError Traceback (most recent call last)
<ipython-input-45-9f45a812fe7a> in <module>()
4 x = 'abc'
5
----> 6 test()
<ipython-input-45-9f45a812fe7a> in test()
1 x = 1234
2 def test():
----> 3 print(x)
4 x = 'abc'
5
UnboundLocalError: local variable 'x' referenced before assignment
在上面的函数中,我们要打印一个名字x
,它首先会去本地命名空间中查找,没有找到。然后去当前函数test
的作用域中查找,找到了。此时Python解释器就会发现x
这个名字还没有添加到local本地命名空间中,就被引用了。所以就会抛出一个异常,说x
还未被赋值就被引用了。如果我们把代码改成下面这种形式,发现就可以正常运行了:
x = 1234
def test():
print(x)
test()
1234
为什么删除了x的赋值语句,这个函数就能正常运行了呢?
这是因为此时Python解释器查找命名空间的顺序变了。解释器首先会去查找本地命名空间,发现没有,然后去查找函数test
的作用域,发现也没有,接着再去查找全局命名空间,此时找到了,就会打印出x的值。
import dis
x = 1234
def test_right():
print(x)
dis.dis(test_right)
print('-' * 20)
x = 1234
def test_error():
print(x)
x = 'abc'
dis.dis(test_error)
5 0 LOAD_GLOBAL 0 (print)
3 LOAD_GLOBAL 1 (x)
6 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
9 POP_TOP
10 LOAD_CONST 0 (None)
13 RETURN_VALUE
--------------------
13 0 LOAD_GLOBAL 0 (print)
3 LOAD_FAST 0 (x)
6 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
9 POP_TOP
14 10 LOAD_CONST 1 ('abc')
13 STORE_FAST 0 (x)
16 LOAD_CONST 0 (None)
19 RETURN_VALUE
通过查看这两个函数的反汇编出来的代码可以看到,这两个函数访问x的时候,一个是通过LOAD_GLOBAL
指令,访问的全局命名空间,另外一个则是通过LOAD_FAST
指令访问的本地命名空间。通过这些反汇编出来的代码,可以部分地证明我们上面的猜想。