原文章 由一个例子到python的名字空间 。这篇文章对python的命名空间做出了非常好的介绍,这里是我自己的一些理解。

例子

# encoding: utf-8
def ():
x = 1
print globals()
print 'before func1:', locals()
def func2():
a = 1
print 'before func2:', locals()
a += x
print 'after func2:', locals()
func2()
print 'after func1:', locals()
print globals()
if __name__ == '__main__':
func1()

输出结果为:

{'func1': , '__builtins__': , '__file__': 'F:/Projects/python/1017test/namespaceTest.py', '__package__': None, '__name__': '__main__', '__doc__': None}
before func1: {'x': 1}
before func2: {'a': 1, 'x': 1}
after func2: {'a': 2, 'x': 1}
after func1: {'x': 1, 'func2': }
{'func1': , '__builtins__': , '__file__': 'F:/Projects/python/1017test/namespaceTest.py', '__package__': None, '__name__': '__main__', '__doc__': None}

可见结果可以正常输出。func1 在执行之前的本地变量只有 x 。而 func2 在执行之前的本地变量为 a,x 。

名字空间(Namespace)

对于一些语言如c,c++,java来说,变量名是内存地址别名,而在python中,变量名就是一个字符串,他与所指向的对象关联成名字空间里面的一个键值对{name: object},所以python的名字空间就是一个字典。

分类

python里面有很多名字空间,每个地方都有自己的名字空间,相互不能干扰。一般有四种命名空间:

locals:函数内部的名字空间,一般包括函数的局部变量以及形参

enclosing function:在嵌套函数中外部函数的名字空间,对于上面的func2来说func1就是嵌套函数

globals: 当前的模块空间,模块就是一些py文件。globals类似全局变量

__builtins__:内置模块空间,也就是内置变量或者内置函数的名字空间

当程序引用某个变量的名字时,就会从当前名字空间开始搜索。顺序便是LEGB

考虑上面的例子,访问到func1的x=1的时候,赋值语句会先把变量放到对应的名字空间中,此处是func1的locals,所以locals中会有x,然后让x指向一个数值为1的整数对象。执行到func2的a=1时,同样把a加入到func2的locals中,然后指向一个整数对象。下面的a += x看到了赋值语句,把a放在locals中,发现已经有了,所以就不需要放了。然后需要x的值就按照LEGB去找,locals里面没有,就去 enclosing function 里面找,找到了,把值拿到,然后完成运算。然后后续操作是把x放到func2的locals()里面

生命周期

每个名字空间都有自己的生命周期,如下:

__builtins__:在python解释器启动的时候,直到退出

globals:在模块定义被读入的时候创建,通常也一直保存到解释器退出

locals:在函数被调用时创建,直到函数返回,或者抛出异常之后,销毁。递归函数每一次递归均有自己的名字空间

值得指出的是,名字空间都是在代码编译时期确定的,而不是执行期间。这又解释了为什么在例1中,before func2的locals()里面包含了x:1这一项。

再举一个例子:

def test():
if False:
a = 1
print a
print locals()
test()

这段代码肯定是错的,但是错误不是NameError: global name 'a' is not defined,而是UnboundLocalError: local variable 'a' referenced before assignment。

虽然a=1永远不会执行,但是编译阶段,编译器会识别赋值语句,并把变量放在locals()里面。所以编译到print 的时候,程序不会报变量未定义错误,但是会报变量未赋值错误。由于变量未赋值,所以不会加入到locals()里面,所以打印的本地变量里面没有x。

再举一个例子:

# encoding: utf-8
def func1():
x = 1
print 'before func1:', locals()
def func2():
print 'before fun2:', locals()
x += x #就是这里使用x其余地方不变
print 'after fun2:', locals()
func2()
print 'after func1:', locals()
if __name__ == '__main__':
func1()

和最开始的例子唯一不同的就是少了a的赋值语句。x+=x编译到这里时,发现了赋值语句,于是把x

加入到最内层名字空间也就是func2中,即使上层函数已经存在了(其实不同名字空间中相同名字的变量没有任何关系)。但是代码执行的时候,需要得到x的值。在最内层的名字空间里面找到了,于是使用这个变量,但是发现这个变量没有赋值,于是就报UnboundLocalError: local variable 'x' referenced before assignment。也因为报错,func2中的locals()不会包括x。

赋值

我们之前已经看到,赋值操作会直接影响名字空间,但是不会修改对象本身。

a=1这样的赋值操作,也只是把a放在对应的名字空间里,然后让它指向一个值为1的整数对象。

a=[]同样就是把a放在对应的名字空间里面,然后让其指向一个列表对象。

之前的例子已经说明,内层函数可以访问外层函数的变量,但好像无法修改。但其实所谓的访问,也只是在自己的名字空间里面加入这个变量以及对应的值,然后之后的操作也就和外层函数无关了。但其实内层函数是可以改变外层函数的变量的。python是引用传递,这意味着我们可以借助引用实现对可变对象的操作。

def ():
x = [1,2]
print 'before func1:', locals()
def func2():
print 'before func2:', locals()
x[0] += x[0]
print 'after func2:', locals()
func2()
print 'after func1:', locals()
if __name__ == '__main__':
func1()
before func1: {'x': [1, 2]}
before func2: {'x': [1, 2]}
after func2: {'x': [2, 2]}
after func1: {'x': [2, 2], 'func2': }

和上一个例子不同的是,x现在是一个列表。没有报错的原因是x[0]+=x[0]不是赋值语句,只是对这个列表对象的操作,和x.append(3)是类似的操作。既然不是赋值操作,那么编译进行到这里时,便不会把x加入到func2的名字空间。然后便会按照LEGB的顺序去查找,然后在外层函数中找到了x=[1,2],然后将其复制到func2的名字空间,再进行之后的操作。要注意的是,这里复制的是x所指向列表对象的引用,因此,会修改这个列表对象的值。

一点点补充

a = 2
def ():
a = 1
print locals()
locals()['a'] = 2
print locals()
print globals()
globals()['a'] = 3
print globals()
()
print a
{'a': 1}
{'a': 1}
{'a': 2, 'func1': , '__builtins__': , '__file__': 'F:/Projects/python/1114test/testNamespace.py', '__package__': None, '__name__': '__main__', '__doc__': None}
{'a': 3, 'func1': , '__builtins__': , '__file__': 'F:/Projects/python/1114test/testNamespace.py', '__package__': None, '__name__': '__main__', '__doc__': None}
3

locals()和global()有很多相似的地方。但locals()是只读的,而globals()确实可以修改的。

最后对开始的例子进行分析

最开始的func1的locals()只有x:1,而在func2中,编译阶段会先确定名字空间,a=1这个赋值操作会把a加入到func2的名字空间中。而a+=x发现了x而本地变量没有,于是按照LEGB的顺序查找,在外层找到了x,将其加入到func2的名字空间中,所以func2的名字空间中,有a和x。