命名空间和作用域的概念我们之前也提到过,比如内置函数globals(),函数中变量的作用域,模块使用的import等等。这些可能让我们对这两个概念有了大致的理解。本节再详细探讨一下。
Python命名空间
命名空间,就是一个从名称到对象的映射关系。
对于这个概念的理解,我们打个比方:河西村有个人(对象)叫张三(名称),河东村有个人(对象)也叫张三(名称),俩人虽然都叫张三(名称),但是他们俩不是同一个人(对象),因为他们属于不同的村(命名空间)。有一天,河西村的张三名声大了,传播到镇上了(名称被import到“镇”这个空间),镇上的人讲起“张三”时,就是说的河西村的,要是说河东村的张三,就要特别说“河东村的张三”(河东村.张三)。这就是命名空间的意思——映射了名称到对象的名称范围。
目前,大部分的命名空间都是由Python的字典实现的,通常我们不会去关注它们,处理要面对性能问题时,并且这种实现可能在将来改变。所以说,我们不需要深究命名空间的内部实现,但要搞明白它的使用。
下面是几个命名空间的例子:
- 内置函数的集合(包含print()等内置函数和内置异常等);
- 模块中的全局名称;
- 函数调用中的本地名称。
另外,从某种含义上说,对象的属性集合也是一种命名空间的形式。正如我们前面举的张三的例子那样,不同命名空间中的名称之间没有任何关系。比如,两个不同模块都可以定义函数max()而不会产生混淆,模块的用户要调用某个max()函数就要在其前面加上模块名称。(详见import的使用)
Python属性
我们把任何跟在一个点号之后的名称都称为属性。例如,在表达式a.name中,real是对象a的一个属性。同样对模块中函数的引用也是属性引用,在表达式modname.funcname中,modname是一个模块对象,而funcname是它的一个属性。
属性可以是只读的也可以是可写的。如果是可写的,那么我们就可以对属性进行赋值,比如,modname.name = '猿人学Python'。可写的属性同样可以用del语句删除,比如del modname.name将把modname对象的name属性移除。
不同时刻创建的命名空间有不同的生存期:
- 包含内置名称的命名空间是在Python解释器启动时创建的,永远不会被删除(除非退出解释器);
- 模块的全局命名空间在模块定义被读入(import)时创建,通常,模块命名空间也会持续到解释器退出;
- 从脚本文件(.py或.pyc)读取或交互式(解释器shell)读取而被解释器的顶层调用执行的语句,被认为是__main__模块调用的一部分,它们有自己的全局命名空间;
- 函数的本地命名空间创建于该函数被调用的时刻,并且在函数返回或抛出一个不在函数内部处理的异常时被删除。递归函数的每次递归调用都会创建它自己的本地命名空间;
内置名称实际上也存在于一个模块中,它叫做builtins。
Python作用域
作用域,是一个命名空间可直接发放完的Python代码的文本区域。这里的“可直接访问”的意思是,对名称的不加点号(非限定性)引用会尝试在命名空间中查找该名称。
尽管作用域是静态确定的,但它们是动态使用的。在执行期间的任何时刻,至少有三个嵌套的作用域,它们的命名空间可以直接访问:
- 最内部作用域:最先搜索该作用域,包含局部名称
- 封闭函数作用域:从最近的封闭作用域开始搜索,包含非局部名称,也包括非全局名称
- 倒数第二个作用域:包含当前模块的全局名称
- 最外面的作用域:最后搜索,是包含内置名称的命名空间
如果一个名称被声明为全局变量,则所有引用和赋值将直接指向包含该模块的全局名称的中间作用域。 要重新绑定在最内层作用域以外找到的变量,可以使用nonlocal语句声明为非本地变量。 如果没有被声明为非本地变量,这些变量将是只读的(尝试写入这样的变量只会在最内层作用域中创建一个新的局部变量,而同名的外部变量保持不变)。
很重要的一点:作用域是按文本方式确定的,模块内定义的函数的全局作用域就是该模块的命名空间,无论该函数从什么地方或以什么别名被调用。另一方面,实际的名称搜索是在运行时动态完成的。
Python 的一个特殊之处在于: 如果不存在生效的global语句,对名称的赋值总是进入最内层作用域。 赋值不会复制数据,它们只是将名称绑定到对象。删除也是如此,语句del x会从局部命名空间的引用中移除对x的绑定。事实上,所有引入新名称的操作都使用局部作用域,特别是import语句和函数定义会在局部作用域中绑定模块或函数名称。
global语句可被用来表明特定变量生存于全局作用域并且应当在其中被重新绑定;nonlocal语句表明特定变量生存于外层作用域中并且应当在其中被重新绑定。
下面我们来看一个作用域和命名空间的例子,它演示流量如何引用不同作用域和命名空间以及global和nonlocal如何影响变量绑定:
思考一下,上面的代码会输出怎样的结果?如果你对上面的讲解理解透了,你的思考结果应该是这样的:
这里要说明的是,do_global()函数修改了全局变量name,并没有对scope_demo()函数的局部变量name做修改,所以打印了After global assignment: nonlocal name。
局部赋值(默认情况)不会改变scope_demo对name的绑定;nonlocal赋值会改变函数scope_demo对name的绑定,而global赋值会改变模块层级的绑定(不是scope_demo内部的name,而是其之外的全局作用域下的name)。
命令空间和作用域总结:
命名空间定义了一个名称的范围,作用域指定了能看到命名空间的文本区域(代码)。代码执行时,名称搜索的顺序和范围如下:
- 最内部作用域:最先搜索该作用域,包含局部名称
- 封闭函数作用域:从最近的封闭作用域开始搜索,包含非局部名称,也包括非全局名称
- 倒数第二个作用域:包含当前模块的全局名称
- 最外面的作用域:最后搜索,是包含内置名称的命名空间
相关练习题
参照scope_demo(),练习局部赋值、nonlocal赋值、global赋值。