1 引言
命名空间与作用域是程序设计中的基础概念,深入理解有助于理解变量的生命周期,减少代码中的莫名其妙bug。Python的命名空间与作用域与Java、C++等语言有很大差异,若不注意,就可能出现莫名其妙的问题。
2 命名空间
2.1 什么是命名空间
命名空间,即Namespace,也成为名称空间或名字空间,指的是从名字到对象的一个映射关系,类似于字典中的键值对,实际上,Python中很多命名空间的实现用的就是字典。
不同命名空间是相互独立的,没有任何关系的,所以同一个命名空间中不能有重名,但不同的命名空间是可以重名而没有任何影响。
2.2 命名空间的类型
Python命名空间按照变量定义的位置,可以划分为以下3类:
Built-in,内置命名空间,python自带的内建命名空间,任何模块均可以访问,存放着内置的函数和异常。
Global,全局命名空间,每个模块加载执行时创建的,记录了模块中定义的变量,包括模块中定义的函数、类、其他导入的模块、模块级的变量与常量。
Local,局部命名空间,每个函数、类所拥有的命名空间,记录了函数、类中定义的所有变量。
一个对象的属性集合,也构成了一个命名空间。但通常使用objname.attrname的间接方式访问属性,而不是直接访问,故不将其列入命名空间讨论。(直接访问:直接使用名字访问的方式,如name,这种方式尝试在名字空间中搜索名字name。间接访问:使用形如objname.attrname的方式,即属性引用,这种方式不会在命名空间中搜索名字attrname,而是搜索名字objname,再访问其属性。)
2.3 命名空间的生命周期
不同类型的命名空间有不同的生命周期:
内置命名空间在Python解释器启动时创建,解释器退出时销毁;
全局命名空间在模块被解释器读入时创建,解释器退出时销毁;
局部命名空间,这里要区分函数以及类定义。函数的局部命名空间,在函数调用时创建,函数返回结果或抛出异常时被销毁(每一个递归函数都拥有自己的命名空间);类定义的命名空间,在解释器读到类定义(class关键字)时创建,类定义结束后销毁。(*)
3 作用域
3.1 什么是作用域
作用域是针对命名空间而言,指命名空间在程序里的可应用范围,或者说是Python程序(文本)的某一段或某几段,在这些地方,某个命名空间中的名字可以被直接引用。这部分程序就是这个命名空间的作用域。只有函数、类、模块会产生新的作用域,代码块(例如if、for代码块)不会产生新的作用域。
另外,python中变量的作用域是由它在源代码中的位置决定的(*)。由一个赋值语句引进的名字在这个赋值语句所在的作用域里是可见(起作用)的,而且在其内部嵌套的每个作用域内也可见,除非它被嵌套于内部的且引进同样名字的赋值语句所遮蔽。
3.2 命名空间的查找顺序
上述作用域的定义中表名了命名空间与作用于之间的关系:作用于是命名空间的可见范围。那么,在程序中访问某个名称时,是怎样一个搜索顺序呢?按照LEGB顺序搜索:
Local:首先搜索,包含局部名字的最内层(innermost)作用域,如函数/方法/类的内部局部作用域;
Enclosing:根据嵌套层次从内到外搜索,包含非局部(nonlocal)非全局(nonglobal)名字的任意封闭函数的作用域。如两个嵌套的函数,内层函数的作用域是局部作用域,外层函数作用域就是内层函数的 Enclosing作用域;
Global:倒数第二次被搜索,包含当前模块全局名字的作用域;
Built-in:最后被搜索,包含内建名字的最外层作用域。
Python按照以上LEGB的顺序依次在四个作用域搜索名字,没有搜索到时,Python抛出NameError异常。所以:
在局部作用域中,可以看到局部作用域、嵌套作用域、全局作用域、内建作用域中所有定义的变量。
在全局作用域中,可以看到全局作用域、内建作用域中的所有定义的变量,无法看到局部作用域中的变量。
在Python中,类定义所引入的作用域对于成员函数是不可见的,这与C++或者Java是很不同的,因此在Python中,成员函数想要引用类体定义的变量,必须通过self或者类名来引用它。(我的理解是Python类中所有变量有一个作用域,每个成员函数都有各自都作用域,这些作用域都是Local,且是平级的*)
用一个类比来理解命名空间与作用域: 四种作用域相当于我们生活中的国家(Built-in)、省(Global)、市(Enclosing)、县(Local),命名空间相当于公务员花名册,记录着哪个职位是哪个人。*公务员服务于全国
民众(全国老百姓都可以喊他办事),省级公务员只服务于本身民众(国家层面的人或者其他省的人我不管),市(Enclosing)、县(Local)也是一个道理。当我们要找某一类领导(例如想找
个警察帮我打架)时(要访问某个名称),如果我是在县(Local)里头,优先在县里的领导花名册中找(优先在自己作用域的命名空间中找),县里花名册中没警察没有就去市里的花名册找(往
上一层作用域命名空间找),知道找到*都还没找到,那就会报错。如果省级民众想找个警察帮忙大家,不会找市里或者县里的,只会找自己省里的(其它省都不行),或者找*的。国家、
省、市、县肯定一直都在那里,可不会移动(作用域是静态的);领导可以换届,任期移到就换人(命名空间是动态的,每次调用函数都会新的命名空间,函数执行结束,命名空间销毁)。
3.3 glocal与nonlocal
当在一个函数内部为一个变量赋值时,并不是按照上面所说LEGB规则来首先找到变量,之后为该变量赋值。在Python中,在函数中为一个变量赋值时,有下面这样一条规则:
“当在函数中给一个变量名赋值是(而不是在一个表达式中对其进行引用),Python总是创建或改变本地作用域的变量名,除非它已经在那个函数中被声明为全局变量. ”
那么,若想要在函数中修改全局变量,而不是在函数中新建一个变量,此时便要用到关键字global
了。
i = 1 def func(): global i print(i) #输出1 i = 2 func() print(i) #输出2
关键字nonlocal的作用与关键字global类似,使用nonlocal关键字可以在一个嵌套的函数中修改嵌套作用域中的变量,示例如下:
def f1(): i = 1 def f2(): nonlocal i print(i) #输出1 i = 2 f2() print(i) f1() #输出2
第一,两者的功能不同。global关键字修饰变量后标识该变量是全局变量,对该变量进行修改就是修改全局变量,而nonlocal关键字修饰变量后标识该变量是上一级函数中的局部变量,如果上一级函数中不存在该局部变量,nonlocal位置会发生错误(最上层的函数使用nonlocal修饰变量必定会报错)。
第二,两者使用的范围不同。global关键字可以用在任何地方,包括最上层函数中和嵌套函数中,即使之前未定义该变量,global修饰后也可以直接使用,而nonlocal关键字只能用于嵌套函数中,并且外层函数中定义了相应的局部变量,否则会发生错误。
对上面代码略作修改:
i = 0 def f1(): i = 1 def f2(): global i #此处改为glocal print(i) #输出0 i = 2 f2() print(i) f1() #输出2
3.4 globals()和locals()函数
根据调用地方的不同,globals()和locals()函数可被用来返回全局和局部命名空间里的名字。
如果在函数内部调用locals(),返回的是所有能在该函数里访问的命名。
如果在函数内部调用globals(),返回的是所有在该函数里能访问的全局名字。
两个函数的返回类型都是字典。所以名字们能用keys()函数摘取。
4 易错情况
上文介绍了变量名的搜索顺序是LEGB的,其中G、B两个作用域的引入在不能够通过代码操作的,能够通过语句引入的作用域只有E和L。Python中也只能函数和类的定义能引入新作用域。另外,在实际开发中,一定要主要函数定义引入local作用域或者Enclosing作用域中对应命名空间的声明周期。下面列举Python中的几例特殊情况。如果你觉得已经理解并掌握了上面命名空间与作用于的知识,请尝试解释下面的情况:
(1)情况1:
def test(): i = 0 test() print(i)
推测出输出结果了吗?没错,会报错:NameError: name 'i' is not defined。切记:函数的命名空间在函数被调用时创建,函数执行完毕,命名就也被销毁。另外,LEGB搜索法则也不会让全局作用域去局部作用域寻找。
(2)情况2:
if True: i = 1 print(i) # 可以正常输出i的值1,不会报错
if条件判断语句不会引入新的作用域,所以,语句“i=1”与“print(i)”属于同一作用域,既然同属于一个作用域,也不存在说if代码块运行完之后,作用域销毁,所以i一直存在,可以正常执行。
(3)情况3:
for i in range(10): pass print(i) #输出结果是9,而不是NameError
for循环不会引入新的作用域,所以,循环结束后,继续执行print(i),可以正常输出i,原理上与情况3中的if相似。这一点Python就比较坑了,因此写代码时切忌for循环名字要与其他名字不重名才行。
(4)情况4
list_1 = [i for i in range(5)] print(i)
情况3中说到过,for循环不会引入新的作用域,那么为什么输出报错呢?真相只有一个:列表生成式会引入新的作用域,for循环是在Local作用域里面的。事实上,lambda、生成器表达式、列表解析式也是函数,都会引入新作用域。
(5)情况5:
def import_sys(): import sys import_sys() print(sys.path) # 报错:NameError: name 'sys' is not defined
在函数内部进行模块导入时,导入的模块只在函数内部作用域生效。这个算非正常程序员的写法了,import语句在函数import_sys中将名字sys和对应模块绑定,那sys这个名字还是定义在局部作用域,跟上面的例子没有任务区别。要时刻切记Python的名字,对象,这个其他编程语言不一样。
(6)情况6:
只引用上层作用域中的值时:
def test(): print(i)# 可正常输出0 i = 0 test()
在局部作用域中可以引用全局作用域中的命名空间。
注:可不要认为i=0这行必须写在def test()前面,事实上只需要在test()函数调用前写i=0即可,因为函数的命名空间是在函数被调用时创建的。
继续上面的例子,若是对值进行修改:
def test(): print(i) i= 2 i = 0 test()
报错:UnboundLocalError: local variable 'i' referenced before assignment
Python对局部作用域情有独钟,解释器执行到print(i),i在局部作用域没有。解释器尝试继续执行后面定义了名字i,解释器就认为代码在定义之前就是用了名字,所以抛出了这个异常。如果解释器解释完整个函数都没有找到名字i,那就会沿着搜索链LEGB往上找了,最后找不到抛出NameError异常。
是不是觉得另有所悟,对上面的代码稍作修改,能否推测出结果:
def test(): i = [2 , 2] i = [1 , 2] test() print(i) 输出结果: [1 , 2]
我想你应该猜到了结果,这个和上面的例子基本是一样的。再改一下:
def test(): i[0] = 2 i = [1 , 2] test() print(i)
输出结果:
[2, 2]
猜到了吗?是不是有些懵逼。list作为一个可变对象,l[0] = 2并不是对名字l的重绑定,而是对l的第一个元素的重绑定,所以没有新的名字被定义。因此在函数中成功更新了全局作用于中l所引用对象的值。
(7)情况7:
请对比下面几种示例代码:
第一种:
i = 1 def f1(): print(i) def f2(): i = 2 f1() f2() print(i)
第二种:
i = 1 def f1(): print(i) def f2(): i = 2 return f1 ret = f2() ret() print(i)
第三种:
i = 1 def f1(): i = 2 def f2(): print(i) return f2 func = f1() func() print(i)
先别看答案,想想输出结果!
第一种输出结果:
1
1
第二种输出结果:
1
1
第三种输出结果:
2
1
为什么会这样呢?上面说到过,函数的作用域是静态的,由函数声明的位置决定,在哪里声明,就决定了它的上层作用域是谁,这与调用函数的位置无关。无论在哪里调用,它都会去函数本身的作用域中的命名空间找,找不到在去上一层的命名空间找,切记未必是在调用该函数的作用域的命名空间找。对于第三种情况,是最让我费解的地方,func = f1()执行完之后,f1的命名空间被销毁,按理说就找不到i=2了,但是输出结果确实是2,所以我只能用LEGB搜索法则解释。(如果你知道为什么,请给我留言,感激不尽……)
(8)情况8:
class A(object): a = 2 def fun(self): print(a) new_class = A() new_class.fun()
代码运行后报错:NameError: name 'a' is not defined。上文中说过,Python类成员变量与成员函数都有自己的作用域,且各作用域平级。(用作用域的生命周期来解释也行,但是真心觉得不对劲)。
5 总结
Python的作用域与命名空间有的时候真的让人很费解,我本以为与Java等语言类似的,没想多还是挺有区别的。有些情况我到现在也没想通,例如作用域与命名空间的生命周期,用生命周期来解释上面的一些例子,总觉得不对劲。期间翻阅了n多前辈的博客资料,到各有说法,或许是我没理解到位,若有前辈看到这里,又刚好知道原因,请为晚辈留言解惑,感激不尽!
参考资料:
https://www.jb51.net/article/114951.htm
http://python.jobbole.com/86465/
http://python.jobbole.com/81367/?utm_source=blog.jobbole.com&utm_medium=relatedPosts