跳转至

3.2.命名空间和作用域

命名空间

Namespace : namespace 是一个从 name 到 object 的映射。现在大部分的 namespace 通过Python字典1实现,将来可能会改变实现方式。

namespace:中文称为命名空间,或者名字空间。

命名空间最重要的作用是避免名字冲突,各个命名空间是独立的,因此不同的命名空间可以存在同名变量。

Namespace 的种类

一般而言,name 产生地点决定其所处的 namespace。例如,在函数内定义的 name 会位于(函数的)局部命名空间。但使用了globalnonlocal语句会改变这种情况,这个在后面会说到。

  1. built-in namespace:builtins 模块定义的所有名字存在内置命名空间,这些名字包括内置函数、内置异常、内置常量、内置类型。如果是Python 2,则是 __builtin__ 模块。

  2. global namespace:每个模块都有全局命名空间,包括所有在模块最外层的作用域中定义的名字,例如类、函数、常量、被导入的模块。

  3. local namespace:相对于全局命名空间,每一个局部作用域都有一个局部命名空间。例如,函数或类所定义的命名空间,记录了函数参数、函数内的变量、类属性、类方法等。

  4. enclosing namespace:闭包命名空间不仅记录了当前嵌套函数内定义的变量,还记录了嵌套函数引用的外部变量。

Namespace生命周期

不同类型的命名空间有不同的生命周期:

  1. built-in:在 Python 解释器启动时创建,解释器退出时销毁
  2. global:在模块定义被解释器读入时创建,通常也会一直保存到解释器退出,除非使用del语句。
  3. local:这里要区分function以及class定义:

    • 函数的局部命名空间:在函数调用时创建,函数返回或者产生未被捕获的异常时销毁。

    • 类定义的局部命名空间:在解释器读到类定义创建,离开类定义时创建class object。这个类对象实际上就是这个局部命名空间的包装(见官方对类定义的说明)。

查看Namespace

  • 局部命名空间可以通过locals()来访问

  • 全局 (模块级别)命名空间可以通过globals()来访问

虽然都是返回命名空间,但globals()locals()有一点不一样。locals()返回的是局部命名空间的副本,所以修改locals()对象并不会影响局部名字空间。globals返回是全局命名空间,而不是副本,所以修改globals()对象会影响全局命名空间。

# 修改局部命名空间无效
def change_local():
    x = 123
    print("当前的局部命名空间", locals(), sep=":")
    locals()["x"] = 6789
    print("修改locals(),并不影响x的值", f"x={x}", sep=":")

change_local()
当前的局部命名空间:{'x': 123}
修改locals(),并不影响x的值:x=123
# 修改全局命名空间生效
y = 123
globals()["y"] = 456
print("修改全局命名空间生效:", f"y={y}")
修改全局命名空间生效: y=456

作用域

scope : scope 是Python程序的一块文本区域(即一个代码区域),在这个区域可以直接访问namespace 。

直接访问意味着无须特性的指明引用。在Python中,直接访问是指直接使用name访问对象,如name,这会在命名空间搜索名字name;而间接访问是指使用形如objname.attrname的方式,即引用对象的属性,这不会在命名空间搜索名字attrname,而是搜索名字objname,再访问其属性。

换而言之,作用域其实就是一个命名空间可以发生作用的代码区域,发生作用是指命名空间可以被作用域直接访问。

作用域与命名空间的关系

命名空间保存着名字到对象的映射,映射关系在作用域中被定义,同时作用域可以通过引用命名空间的名字访问对象。代码区域可以直接引用哪个命名空间中的名字,它也就是哪个命名空间的作用域。有些代码区域可以访问多个命名空间的名字,那么它同时是多个命名空间的作用域。

作用域是静态的(它只是代码文本),而命名空间是动态的,命名空间随着解释器的执行而产生。

有些文章认为动态的作用域就是命名空间。其实并不是,命名空间是一种映射,作用域是一块代码区域。

Python对象通过命名空间被访问,而作用域则限制了Python对象的使用范围。

名字搜索顺序(LEGB)

当一行代码需要使用名字X所对应的值,Python会从当前层级的Namespace开始,并根据以下顺序,去查找名字X

  1. local namespace:包含局部名字的最内层(innermost)作用域,如函数、方法、类的内部局部作用域。
  2. Enclosing:根据嵌套层次从内到外搜索,包含非局部(nonlocal)非全局(nonglobal)名字的任意封闭函数的作用域。如两个嵌套的函数,内层函数的作用域是局部作用域,外层函数作用域就是内层函数的 Enclosing 作用域。
  3. global
  4. built-in
  5. NameError:如果 Python 在以上4个namespace找不到X,将放弃搜索并抛出 NameError 异常:NameError: name 'a' is not defined

程序在运行时,local、global、built-in三个namespace一定存在,但Enclosing namespace不一定存在。

Namespace和scope总结

  1. 不能在名字未定义前引用该名字(命名空间不存在该映射关系)。
  2. built-in、global两个命名空间的引入是不能够通过代码操作的,Python 解释器会自动引入它们。注意,这里说的是引入,而不是修改。built-in命名空间是不能被修改的(已经预定义好),但global命名空间可以被Python代码修改。
  3. 类定义、函数定义、推导式会引入local命名空间 ,闭包函数定义会引入enclosing命名空间。
  4. 会导致命名空间被修改的情况:

    • 类定义、函数定义

    • 赋值语句

    • import语句、if语句、for语句、while语句

  5. ifforwhile语句并不会引入新的命名空间。

  6. 作用范围最大的命名空间是global namespace,但global namespace也只是模块级别的。a模块不能直接引用在b模块定义的name,即使a模块已经导入了整个b模块:import b。除非直接导入name:from b import name
  7. 导入b模块中其中一个name:from b import name,整个b模块都会被执行。因为 Python 并不知道name在 b.py 文档的何处,为了能够找到 name,Python 需要执行整个 b.py。
  8. import语句不一定会改变global namespace,例如import语句写在函数内。
  9. 根据LEGB搜索规则,外层作用域不能引用内层作用域的变量。
def try_to_define_name():
    '''函数中定义了名字i,并绑定了一个整数对象1'''
    i = 1
# 引用名字i之前,先调用函数定义i
try_to_define_name()
# 在引用名字i之前,明明调用了函数,定义了名字i,可是还是找不到这个名字。
print(i)
NameError: name 'i' is not defined

虽然定义了名字i,但是定义在了函数的局部作用域对应的局部命名空间中,按照LEGB搜索规则,在全局作用域中自然访问不到局部作用域的命名空间;再者,函数调用结束后,这个命名空间被销毁了。

globalnonlocal

根据LEGB搜索规则,任何时候,Python程序都可以直接读取全局变量,但是却不可以在内层作用域直接改写上层变量。

可以读取全部变量,这容易理解,因为任何时候,global namespace都可以被访问。但为什么在内层作用域全局变量可以被访问,却不能被修改呢?这看起来像是Python的一个规定,但其实是很自然的事情。在Python中修改变量意味着将一个名字绑定到另一个对象,这需要使用assignment运算符=这意味着=左边的名字会被加入当前的命名空间,名字对应着=右边的对象。

例如下面的代码:

a = 1
def change_a():
    a = 10
change_a()
a # 1
1

a = 10语句在函数作用域中,当它被Python执行时,Python会将a加入函数的局部命名空间,而不会将a视作全局命名空间的a。当我们在最外层作用域引用a时,按照LEGB搜索规则,Python会从a所在的作用域对应的命名空间(即全局命名空间)开始搜索a对应的对象,而不会搜索局部命名空间。再者,此时函数change_a()执行完之后,局部命名空间已经被销毁,即使想搜索也搜索不到。

相信你从上面的叙述已经意识到,如果想让Python在内层作用域修改外层作用域的变量,那就需要让Python知道这个变量来自外层作用域。global语句和nonlocal语句正是可以起到这样的作用。

global语句声明列在其后的所有标识符将被解析为全局变量。 使用global时有以下两条限制:

  1. 在同一代码块中,列在global语句中的所有标识符不能在该global语句前出现。
  2. 列在global 语句后的标识符不能被定义成形参,不能出现在for循环控制的目标、类定义和函数定义,或者import语句中。

CPython实现细节

当前实现并未强制履行上面两条限制2,但程序不应该滥用这种自由,因为未来的版本可能会强制履行它们或者不留痕迹的改变程序含义。

nonlocal语句声明列在其后的所有标识符关联最近的 enclosing 作用域里定义过的同名变量(不包括全局变量,只是最近的 enclosing 作用域的变量)。

def scope_test():
    def do_local():
        spam = "local spam of do_local"

    def do_nonlocal():
        nonlocal spam
        spam = "nonlocal spam of do_nonlocal"

    def do_global():
        global spam
        spam = "global spam of do_global"

    spam = "test spam of scope_test"

    # do_local函数内部的赋值不影响scope_test作用域的spam
    do_local()
    print("After local assignment:", spam)

    # do_nonlocal函数内部的赋值影响scope_test作用域的spam,但是不影响全局的spam
    do_nonlocal()
    print("After nonlocal assignment:", spam)

    # do_global 函数内声明的spam存在于全局作用域,而不影响scope_test作用域的
    do_global()
    print("After global assignment:", spam)

scope_test()
print("In global scope:", spam)
After local assignment: test spam of scope_test
After nonlocal assignment: nonlocal spam of do_nonlocal
After global assignment: nonlocal spam of do_nonlocal
In global scope: global spam of do_global

nonlocal语句和global语句有两个不同点:

其一,global语句只是声明标识符来存在于全局命名空间,并不会在当前作用域创建该标识符;nonlocal 语句则会在子函数命名空间中创建与父函数变量同名的标识符:

gv = 'a global var'

def func():
    global gv
    lv = 'a local var'
    print(locals())
    def inn_func():
        nonlocal lv
        global gv
        print(locals())
    return inn_func

a = func()
a()
{'lv': 'a local var'}
{'lv': 'a local var'}

之所以 nonlocal 语句与 global 语句的处理不同,是因为全局变量的作用域生存期很长,在模块内随时都可以访问,而父函数的局部作用域在父函数执行完毕后便会直接释放,因此 nonlocal 语句必须将父函数的标识符和引用写入enclosing namespace。

其二,global语句可以声明全局作用域还没存在的名字,而nonlocal语句只能将它声明的名字关联到enclosing作用域中已经存在的绑定:

# nonlocal 语句之前的eclosing作用域不存在spam 则报错
def scope_test():
    def do_nonlocal():
        # 父函数作用域要先定义spam,否则报错
        nonlocal spam
        spam = "nonlocal spam of do_nonlocal"

scope_test()
  File "<ipython-input-1-e77162de9535>", line 5
    nonlocal spam
    ^
SyntaxError: no binding for nonlocal 'spam' found

nonlocal语句只能声明的名字会绑定到离nonlocal语句最近的enclosing 作用域(如果enclosing 作用域存在nonlocal语句声明的名字):

def scope_test():
    spam = "local spam of scope_test"

    def do_local():
        spam = "local spam of do_local"
        print("After local assignment:", spam)

        def do_nonlocal():
            # 通过nonlocal声明将spam和最近的enclosing作用域的spam关联
            # 所以关联的是do_local函数的spam
            nonlocal spam
            spam = "nonlocal spam of do_nonlocal"

        do_nonlocal()
        print("After nonlocal assignment:",spam)

    do_local()
    print("After nonlocal assignment, scope_test's spam:",spam)

scope_test()
After local assignment: local spam of do_local
After nonlocal assignment: nonlocal spam of do_nonlocal
After nonlocal assignment, scope_test's spam: local spam of scope_test

因此nonlocal语句重新绑定的name的真实作用域是不清楚的,这取决于已经存在的name本来位于那一层作用域。

nonlocal语句和global语句有一个相同点:

当前作用域不能在nonlocal语句和global语句之前绑定两者声明的标识符:

# global
def scope_test():
    spam = "conflict"

    global spam
    spam = "nonlocal spam of do_nonlocal"

scope_test()

# nonlocal
def scope_test():
    spam = "local spam of scope_test"

    def do_nonlocal():
        spam = "conflict"
        nonlocal spam
        spam = "nonlocal spam of do_nonlocal"

    do_nonlocal()
    print("After nonlocal assignment, scope_test's spam:",spam)

scope_test()
  File "<ipython-input-1-d9548726e53c>", line 5
    global spam
    ^
SyntaxError: name 'spam' is assigned to before global declaration

类的作用域和命名空间

类定义的作用域和函数定义的作用域都可以产生局部命名空间,但它们执行机制不一样,有不小的区别。

其一,函数初始化时不会被执行,类初始化时会被执行。

Python读入函数定义时,函数不会被执行:

def func():
    print("executed")

Python读入类定义时,类作用域的代码会被执行:

class A:
    print("executed_A")

    def A_func(self):
        print("executed_A_func")

    class B:
        print("executed_B")

        class C:
            print("executed_C")
executed_A
executed_B
executed_C

这看起来感觉有点不可思议,大家都是定义,为什么类定义会被执行,难道不应该是定义被引用时才执行吗?

其实这也是很自然的事情。Python解释器读入函数定义时,只需要在当前命名空间绑定函数名,不需要创建函数的局部命名空间,自然不需要执行函数作用域的代码。

命名空间的动态性:命名空间在作用域被执行时才产生。

而类定义实际上是类局部命名空间的包装,因此Python解释器读入类定义时,不仅需要在当前命名空间绑定类名,还需要创建类局部命名空间。而命名空间在作用域被执行时才会产生,因此类定义需要在读入的时候就被执行。注意:类方法内的作用域和函数一样,是不会被执行的。

为什么读入类定义就需要创建类局部命名空间?

因为类具有属性和方法,创建类属性就必须执行代码(进行赋值)。那为什么在读入类定义就创建类属性呢?因为类是创建实例、生成其他类(涉及继承、重载等)的对象,必须在实例之前被创建,在代码加载时就被创建有助于提高效率和降低逻辑复杂度。

另外,类还有一个特点:类可以直接被调用,不一定要先创建实例。这个特点就要求类初始化时就创建类命名空间。例如,创建一个类用于统一管理某一类常量:

class Math_Constant:
    pi = 3.1415926
    e = 2.7182818
    sqrt_2 = 1.4142135
    rho = 1.3247195

如果Math_Constant的类属性需要创建一个实例才能被调用,那就太麻烦了,而且类方法、静态方法也就没了意义。

而方法本质是函数,不会在读入定义时被执行。

为什么读入函数定义时不需要创建函数局部命名空间?

一般来说,函数的局部命名空间主要与参数有关,而参数在函数被调用时才传入,创建了命名空间也意义不大。另外函数不像类那么复杂,也就没必要读入定义时就创建局部命名空间。

其二,类的局部命名空间不在名字搜索路径中。

例如:

class A:
    pi = 3.1415926
    def print_pi(self):
        print(pi)

A().print_pi()
NameError: name 'pi' is not defined

Aprint_pi方法的外层作用域,按照设想,print_pi方法中找不到变量pi,应该会到上一层作用域中查找,但是print_pi方法并没有在类A的作用域中查找,而是直接抛出异常。

又例如:

class A():
    a = 1
    b = [a + i for i in range(3)]  #NameError: name 'a' is not defined

执行上段代码,我们可以发现在类 A 内列表推导式无法调取 a 的值。

Python3的列表推导式也会产生局部命名空间。

但在函数中,完全没问题:

def func():
    a = 1
    b = [a + i for i in range(3)]
    print(b)

func()  #[1, 2, 3]

因此,类A 中的 a 不同于函数 func 中的 a 在局部命名空间中可以被任意引用。之所以强调”不可以被任意读取”,原因在于在类A 的局部空间内,a 在当前层级的作用域是可以被直接引用的:

class A():
    a = 1
    c = a + 2

A.c # 3
3

因此,类内可以产生局部命名空间的代码(方法、推导式等)严格上不是作用域。

脚注

参考

  1. python的嵌套函数中局部作用域问题?
  2. Python进阶 - 对象,名字以及绑定

  1. {name:object}。 

  2. 当前的CPython已经实现第一条限制,至少在0.29.14版本的CPython中已经实现。