命名空间
一个程序具有多个变量,为了更好的组织这些变量,更好的控制变量之间的命名冲突以及更好的管理变量在程序中的角色,python使用了命名空间这样一种结构来实现这个目的。python中,一个命名空间由多个变量组成,且以字典的形式来保存这些变量,其中key是变量名称,value是变量名对应的对象。
同一个命名空间中的变量名是唯一的,不同命名空间中的变量相互独立(这里的相互独立指的是不会相互冲突相互覆盖,但是可能会有相同的对象引用(即指向同一个内存中的对象))。既然命名空间是为了更好的控制变量之间的命名冲突以及更好的管理变量在程序中的角色,那么什么情况下把两个变量放入同一个命名空间,即以什么样的逻辑和规则定义变量的命名空间就是最基本的问题。
python中约定了四种命名空间:内置的(Bulit-in)、全局的(Global)、外层函数的(Enclosing or Nonlocal)以及本地的(Local)。
这四种命名空间的定义规则和逻辑如下:
内置的(Bulit-in):python定义的所有的内置变量,比如abs、max这些内置函数以及预定义的异常等,这些变量都放在内置命名空间中,可以通过python的标准模块builtins获取所有的内置变量。
全局的(Global):在模块(脚本)顶层定义的所有变量,python把这些变量会放在该模块的全局命名空间中,可以通过globals()获取当前位置已经定义了的所有全局变量。
本地的(Local):python中函数的每次调用会新创建一个自己的命名空间,这个命名空间包含的变量为函数参数和函数中定义的所有变量,这些变量也成为该函数的本地变量。
外层函数的(Enclosing or Nonlocal):如果函数A中嵌套了一个函数B,那么对于内层函数B来说,外层函数A的本地命名空间就是B的外层函数命名空间。单纯从命名空间的角度看,似乎外层函数命名空间和本地命名空间重复定义了,因为其本身就是外层函数的本地命名空间。但从作用域和变量查找的角度看,并不会重复定义;实际上,在python2.2之前,这层命名空间是不存在的,2.2版本的python才加入了这层命名空间,加入后,使得函数B直接引用函数A的本地变量变为可能;因此,对于这种嵌套函数形式下的外层函数命名空间,python给了其一个单独的定义。
作用域和LEGB变量查找规则
定义好命名空间之后,我们知道具有同一个名称的不同变量可能同时存在于多个不同的命名空间中,那么问题来了:当我们引用一个变量x,x同时存在于多个命名空间中,python如何知道我们引用的是哪一个?python对此引入了作用域和变量查找规则来消除这种歧义性。
域( scope)指的就是程序的文本区域。python中的域指的就是变量的作用域,某个变量的作用域指的是这样一个区域在这个区域中,python可以直接获取到该变量,直接获取的意思是可以直接在该变量的命名空间中找到该变量,而不是通过属性访问等其他方式获取。 所以通过明确变量的作用域可以让我们知道某个变量在程序的哪些地方起作用(可以被直接获取),同时约定了变量作用域还可以消除部分的歧义性,因为对于某个变量并不起作用的区域,我们自然就不需要在该区域考虑该变量。
对于上述四种不同命名空间里的变量,其各自有自己的作用域,分别如下:
全局(变量)作用域:指的就是全局变量的作用域,也就是整个模块文件;
内置(变量)作用域:指的就是内置变量的作用域,包括整个解释器环境;
外层函数(变量)作用域:也指非本地作用域,指的就是外层函数中变量的作用域,包括整个外层函数下的代码块,同时也是外层函数的本地作用域;
本地(变量)作用域:就是一个函数下定义的本地变量的作用域,为函数下的代码块区域。
明确上述四种变量的各自作用域后,依然存在歧义性,因为不同命名空间中的相同名称的变量的作用域可能存在重复,比如有一个全局变量x,在一个函数中又定义了一个本地变量x,那么该函数中的代码块即是全局变量x的作用域,也是本地变量x的作用域。针对这种情况,python中规定了变量在不同命名空间中的查找顺序,依次为:本地命名空间(L)、外层函数命名空间(E or N)、全局命名空间(G)以及内置命名空间(B),这称为python变量查找的LEGB(LNGB)规则。如此,即使不同命名空间中存在相同名称的变量且作用域重复,根据该规则,就可以确定python查找的变量对应的值到底是哪一个,从而可以消除变量名的歧义性。
需要注意的是,LEGB(LNGB)规则是python将源码编译成字节码时的变量作用域解析规则,所以实际上,变量的命名空间在编译时就被静态确定了,从而在解释器执行编译后的字节码时,python会直接在变量对应的命名空间中去获取该对象。
最后强调一下外层函数命名空间和作用域,该命名空间是在python2.2版本加入的,在2.2之前的版本,如下代码如无法运行成功,因为对于inner函数中的x,python只会在本地命名空间、全局命名空间和内置命名空间中查找,outer函数的命名空间会被直接跳过,因此下面的代码会因为找不到x而报错。2.2之前的版本为了使用outer函数的x,一般需要把x作为参数传给inner。但是在2.2版本开始,加入了Enclsoing function概念,也就是outer函数,该函数的命名空间为外层函数命名空间,其变量的作用域包含了inner函数的代码块,因此python在查找x变量时,也会查找该外层函数的命名空间。