前言
python
里面最核心的内容就是:名字空间(namespace)
例子引入
例1
1 | #!/usr/bin/env python |
可以正常输出结果: 并且需要注意,在func2
使用x
变量之前的名字空间就已经有了'x':1
. 1
2
3
4before func1: {'x': 1}
before fun2: {'a': 1, 'x': 1}
after fun2: {'a': 2, 'x': 1}
after func1: {'x': 1, 'func2': <function func2 at 0x7f7c89700b90>}
稍微改一点:如下
例2:
1 | #!/usr/bin/env python |
输出就开始报错: 而且在before func2
也没有了x
. 1
2
3
4
5
6
7
8
9
10
11before func1: {'x': 1}
before fun2: {}
Traceback (most recent call last):
File "test.py", line 18, in <module>
func1()
File "test.py", line 14, in func1
func2()
File "test.py", line 11, in func2
x += x
UnboundLocalError: local variable 'x' referenced before assignmentpython
里面最核心的内容:名字空间,正好总结一下,然后在解释这几个例子。
名字空间(Namespace)
比如我们定义一个"变量" 1
2In [14]: a
NameError: name 'a' is not defined
所以,这里更准确的叫法应该是名字
。 一些语言中比如c,c++,java
变量名是内存地址别名, 而Python 的名字就是一个字符串,它与所指向的目标对象关联构成名字空间里面的一个键值对{name: object}
,因此可以这么说,python的名字空间
就是一个字典.。
分类
python里面有很多名字空间,每个地方都有自己的名字空间,互不干扰,不同空间中的两个相同名字的变量之间没有任何联系一般有4种: LEGB
四种 - locals
: 函数内部的名字空间,一般包括函数的局部变量以及形式参数 - enclosing function
: 在嵌套函数中外部函数的名字空间, 对fun2
来说, fun1
的名字空间就是。 - globals
: 当前的模块空间,模块就是一些py
文件。也就是说,globals()类似全局变量。 - __builtins__
: 内置模块空间, 也就是内置变量或者内置函数的名字空间。
当程序引用某个变量的名字时,就会从当前名字空间开始搜索。搜索顺序规则便是: LEGB
. 1
locals -> enclosing function -> globals -> __builtins
NameError
的异常。这里暂时先不讨论赋值
操作。 比如例1中的a = x + 1
这行代码,需要引用x
, 则按照LEGB
的顺序查找,locals()也就是func2
的名字空间没有,进而开始E
,也就是func1
,里面有,找到了,停止搜索,还有后续工作,就是把x
也加到自己的名字空间,这也是为什么fun2
的名字空间里面也有x
的原因。
访问方式
其实上面都已经说了,这里暂时简单列一下 1. 使用locals()
访问局部命名空间 2. 使用globals()
访问全局命名空间 这里有一点需要注意,就是涉及到了from A import B
和import A
的一点区别。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15#!/usr/bin/env python
# encoding: utf-8
import copy
from copy import deepcopy
def func():
x = 123
print 'func locals:',locals()
s = 'hello world'
if __name__ == '__main__':
func()
print 'globals:', globals()
1 | func locals: {'x': 123} |
从输出结果可以看出globals()
包含了定义的函数,变量等。对于'deepcopy': <function deepcopy at 0x7f1c3d6177d0>
可以看出deepcopy
已经被导入到自己的名字空间了,而不是在copy
里面。 而导入的import copy
则还保留着自身的名字空间。因此要访问copy
的方法,就需要使用copy.function
了。这也就是为什么推荐使用import module
的原因,因为from A import B
这样会把B
引入自身的名字空间,容易发生覆盖或者说污染。
生存周期
每个名字空间都有自己的生存周期,如下: - __builtins__
: 在python
解释器启动的时候,便已经创建,直到退出 - globals
: 在模块定义被读入时创建,通常也一直保存到解释器退出。 - locals
: 在函数调用时创建, 直到函数返回,或者抛出异常之后,销毁。 另外递归函数每一次均有自己的名字空间。
看着没有问题,但是有很多地方需要考虑。比如名字空间都是在代码编译时期确定的,而不是执行期间。这个也就可以解释为什么在例1中,before func2:
的locals()里面包含了x: 1
这一项。再看下面这个, 1
2
3
4def func():
if False:
x = 10
print x
1 | NameError: global name 'x' is not defined |
而是:
1 | UnboundLocalError: local variable 'x' referenced before assignment |
虽然x = 10
永远不会执行,但是在执行之前的编译阶段,就会把x
作为locals
变量,但是后面编译到print
的时候,发现没有赋值,因此直接抛出异常,locals()
里面便不会有x
。这个就跟例子2中,before func2
里面没有x
是一个道理。
赋值
为什么要把赋值单独列出来呢,因为赋值操作对名字空间的影响很大,而且很多地方需要注意。 核心就是: 赋值修改的是命名空间,而不是对象, 比如: 1
a = 10
a
放入到了对应的命名空间, 然后让它指向一个值为10的整数对象。
1 | a = [] |
这个就是把a
放入到名字空间,然后指向一个列表对象, 然而后面的a.append(1)
这句话只是修改了list
的内容,并没有修改它的内存地址。因此 并没有涉及到修改名字空间。 赋值操作有个特点就是: 赋值操作总是在最里层的作用域.也就说,只要编译到了有赋值操作,就会在当前名字空间内新创建一个名字,然后开始才绑定对象。即便该名字已存在于赋值语句发生的上一层作用域中;
总结
分析例子
现在再看例子2, 就清晰多了, x += x
编译到这里时,发现了赋值语句,于是准备把x
新加入最内层名字空间也就是func2
中,即使上层函数已经存在了; 但是赋值的时候,又要用到x
的值, 然后就会报错:
1 | UnboundLocalError: local variable 'x' referenced before assignment |
这样看起来好像就是 内部函数只可以读取外部函数的变量,而不能做修改,其实本质还是因为赋值
涉及到了新建locals()
的名字。 在稍微改一点: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18#!/usr/bin/env python
# encoding: utf-8
def func1():
x = [1,2]
print 'before func1:', locals()
def func2():
print 'before fun2:', locals()
x[0] += x[0] #就是这里使用x[0]其余地方不变
print 'after fun2:', locals()
func2()
print 'after func1:', locals()
if __name__ == '__main__':
func1()
1 |
|
咋正确了呢---这不应该要报错吗? 其实不然,就跟上面的a.append(1)
是一个道理。 x[0] += x[0]
这个并不是对x
的赋值操作。按照LEGB
原则, 搜到func1
有变量x
并且是个list
, 然后将其加入到自己的locals()
, 后面的x[0] += x[0]
, 就开始读取x
的元素,并没有影响func2
的名字空间。另外无论func1
与func2
的名字空间的x
没有什么关系,只不过都是对[1, 2]
这个列表对象的一个引用。 这个例子其实也给了我们一个启发,我们知道内部函数无法直接修改外部函数的变量值,如例2,如果借助list
的话, 就可以了吧!比如把想要修改的变量塞到一个list
里面,然后在内部函数里面做改变!当然python3.x
里面有了nonlocal
关键字,直接声明一下就可以修改了。看到这里,对作用域理解应该有一点点了吧。
延伸
与闭包的不同
我们都知道闭包是把外部函数的值放到func.func_closure
里面,为什么不像上面的例子一样直接放到函数的名字空间呢? 这是因为locals()
空间是在函数调用的时候才创建! 而闭包只是返回了一个函数, 并没有调用,也就没有所谓的空间。
locals()与globals()
在最外层的模块空间里locals()
就是globals()
1 | In [2]: locals() is globals() |
另外我们可以手动修改globals()
来创建名字
1 | In [3]: globals()['a'] = 'abcde' |
但是locals()
在函数里面的话, 貌似是不起作用的,如下:
1 | In [5]: def func(): |
这是因为解释器会将 locals 名字复制到 一个叫FAST
的 区域来优化访问速度,而实际上解释器访问对象时,是从FAST
区域里面读取的,而非locals()
。所以直接修改locals()
并不能影响x
(可以使用exec
动态访问,不再细述)。 不过赋值操作,会同时刷新locals()
和FAST
区域。
查了不少资料,说了这么多,我只想说,作为python
最核心的东西,名字空间还有很多很多地方需要探究,比如 - 作用域(scope)与名字空间, 这里只是模糊了二者的区别 - 面向对象,也就是类的名字空间, 又有不一样的地方。。。
学一点记录一点吧。