加载中...

9.2. Python 作用域和命名空间


在介绍类之前,我首先介绍一些有关 Python 作用域的规则。类的定义非常巧妙的运用了命名空间,要完全理解接下来的知识,需要先理解作用域和命名空间的工作原理。另外,这一切的知识对于任何高级 Python 程序员都非常有用。

让我们从一些定义说起。

命名空间 是从命名到对象的映射。当前命名空间主要是通过 Python 字典实现的,不过通常不关心具体的实现方式(除非出于性能考虑),以后也有可能会改变其实现方式。以下有一些命名空间的例子:内置命名(像 abs() 这样的函数,以及内置异常名)集,模块中的全局命名,函数调用中的局部命名。某种意义上讲对象的属性集也是一个命名空间。关于命名空间需要了解的一件很重要的事就是不同命名空间中的命名没有任何联系,例如两个不同的模块可能都会定义一个名为 maximize的函数而不会发生混淆-用户必须以模块名为前缀来引用它们。

顺便提一句,我称 Python 中任何一个“.”之后的命名为 属性 --例如,表达式 z.real 中的 real 是对象 z 的一个属性。严格来讲,从模块中引用命名是引用属性:表达式 modname.funcname中,modname 是一个模块对象,funcname 是它的一个属性。因此,模块的属性和模块中的全局命名有直接的映射关系:它们共享同一命名空间![1]

属性可以是只读过或写的。后一种情况下,可以对属性赋值。你可以这样作:modname.the_answer = 42 。可写的属性也可以用 del 语句删除。例如: del modname.the_answer 会从 modname 对象中删除 the_answer 属性。

不同的命名空间在不同的时刻创建,有不同的生存期。包含内置命名的命名空间在 Python 解释器启动时创建,会一直保留,不被删除。模块的全局命名空间在模块定义被读入时创建,通常,模块命名空间也会一直保存到解释器退出。由解释器在最高层调用执行的语句,不管它是从脚本文件中读入还是来自交互式输入,都是 main 模块的一部分,所以它们也拥有自己的命名空间(内置命名也同样被包含在一个模块中,它被称作 builtin )。

当调用函数时,就会为它创建一个局部命名空间,并且在函数返回或抛出一个并没有在函数内部处理的异常时被删除。(实际上,用遗忘来形容到底发生了什么更为贴切。)当然,每个递归调用都有自己的局部命名空间。

作用域 就是一个 Python 程序可以直接访问命名空间的正文区域。这里的直接访问意思是一个对名称的错误引用会尝试在命名空间内查找。

尽管作用域是静态定义,在使用时他们都是动态的。每次执行时,至少有三个命名空间可以直接访问的作用域嵌套在一起:

  • 包含局部命名的使用域在最里面,首先被搜索;其次搜索的是中层的作用域,这里包含了同级的函数;最后搜索最外面的作用域,它包含内置命名。
  • 首先搜索最内层的作用域,它包含局部命名任意函数包含的作用域,是内层嵌套作用域搜索起点,包含非局部,但是也非全局的命名
  • 接下来的作用域包含当前模块的全局命名
  • 最外层的作用域(最后搜索)是包含内置命名的命名空间

如果一个命名声明为全局的,那么所有的赋值和引用都直接针对包含模全局命名的中级作用域。另外,从外部访问到的所有内层作用域的变量都是只读的。(试图写这样的变量只会在内部作用域创建一个  局部变量,外部标示命名的那个变量不会改变)。

通常,局部作用域引用当前函数的命名。在函数之外,局部作用域与全局使用域引用同一命名空间:模块命名空间。类定义也是局部作用域中的另一个命名空间。

重要的是作用域决定于源程序的意义:一个定义于某模块中的函数的全局作用域是该模块的命名空间,而不是该函数的别名被定义或调用的位置,了解这一点非常重要。另一方面,命名的实际搜索过程是动态的,在运行时确定的——然而,Python 语言也在不断发展,以后有可能会成为静态的“编译”时确定,所以不要依赖动态解析!(事实上,局部变量已经是静态确定了。)

Python 的一个特别之处在于:如果没有使用 global 语法,其赋值操作总是在最里层的作用域。赋值不会复制数据,只是将命名绑定到对象。删除也是如此:del x 只是从局部作用域的命名空间中删除命名 x 。事实上,所有引入新命名的操作都作用于局部作用域。特别是 import 语句和函数定将模块名或函数绑定于局部作用域(可以使用 global 语句将变量引入到全局作用域)。

global 语句用以指明某个特定的变量为全局作用域,并重新绑定它。nonlocal 语句用以指明某个特定的变量为封闭作用域,并重新绑定它。

9.2.1. 作用域和命名空间示例

以下是一个示例,演示了如何引用不同作用域和命名空间,以及 global 和 nonlocal 如何影响变量绑定:


def scope_test():
    def do_local():
        spam = "local spam"
    def do_nonlocal():
        nonlocal spam
        spam = "nonlocal spam"
    def do_global():
        global spam
        spam = "global spam"

    spam = "test spam"
    do_local()
    print("After local assignment:", spam)
    do_nonlocal()
    print("After nonlocal assignment:", spam)
    do_global()
    print("After global assignment:", spam)

scope_test()
print("In global scope:", spam)

以上示例代码的输出为:

After local assignment: test spam
After nonlocal assignment: nonlocal spam
After global assignment: nonlocal spam
In global scope: global spam

注意:local 赋值语句是无法改变 _scopetest 的 spam 绑定。nonlocal 赋值语句改变了 _scopetest 的spam 绑定,并且 global 赋值语句从模块级改变了 spam 绑定。

你也可以看到在 global 赋值语句之前对 spam 是没有预先绑定的。


还没有评论.