命名空间是从名称(names)到对象(objects)的映射。
本文主要讲解 Python 中命名空间、生命周期与作用域的概念,文章参考参考: Python 中的命名空间、生命周期与作用域

# 命名空间 Namespaces

目前,Python 中大多数的命名空间是通过 Python 字典来实现的,字典的键是变量的名称,字典的值是变量值。虽然这一点我们在常规使用中没法感受到。

另外,根据 Python3.9 的官方文档 ,未来有可能会改变命名空间的实现方式。

Python 的命名空间的类型包括:

  1. 内置(built-in)名称的集合,这个是 python 自带的命名空间,任何模块均可以访问,包括了像 abs () 这样的内置函数(function)和内置的异常(exception)名等。
# 在这里,abs 是一个内置函数,它存在于 Python 的内置命名空间中。
print (abs (-10))  # 输出: 10
  1. 模块(module)中的全局名称(global names)。全局命名空间在每个模块加载执行时被创建,记录了模块中定义的对象,包括模块中定义的函数、类、其他导入的模块、模块级的变量与常量。
# 在这里,greet 是 my_module 模块中的一个函数,它存在于 my_module 的全局命名空间中。
# 模块 my_module.py
def greet ():
    return "Hello, World!"
# 在另一个模块中导入 my_module
import my_module
print (my_module.greet ())  # 输出: Hello, World!
  1. 在函数中调用是的局部名称(local names)。局部命名空间,是每个函数所拥有的命名空间,记录了函数中定义的所有变量,包括函数的入参、内部定义的局部变量。
# 在这里,local_var 是 my_function 的局部变量,它只存在于 my_function 的局部命名空间中。
def my_function ():
    local_var = "I am local"
    print (local_var)
my_function ()  # 输出: I am local
# print (local_var)  # 这会引发一个 NameError,因为 local_var 是局部变量

命名空间的最重要特性是:不同命名空间的变量名没有任何的关系。

例如,在两个不同的模块中,我们可以分别定义一个叫 maximize 的函数,只要在调用的时候,分别在 maximize 前面加上模块名做前缀,就不会造成冲突。

# module1 和 module2 都有一个名为 maximize 的函数,但由于它们存在于不同的全局命名空间中,所以不会发生冲突。我们在调用时通过模块名来区分它们。  
# 模块 module1.py
def maximize (x, y):
    return max (x, y)
# 模块 module2.py
def maximize (x, y):
    return x * y
# 在主程序中导入两个模块
import module1
import module2
print (module1.maximize (10, 20))  # 输出: 20
print (module2.maximize (10, 20))  # 输出: 200

因为,这两个模块有自己的全局命名空间,我们是从两个命名空间中调用两个函数,不同的命名空间的变量名没有任何关系。

# 属性 Attribute

从某种意义上说,一个对象的属性(attribute)的集合也构成一个名称空间,它是从模块的属性到模块的 global names 的映射。

在 Python 中,我们把位于英文句号。后面的名称成为属性(attribute)。例如,在表达式 z.real 中, real 是对象 z 的一个属性。

严格地说,对于模块中的名称的引用是属性引用(attribute references)。例如,在表达式 modname.funcname 中, modname 是一个模块对象,而 funcname 是这个对象的一个属性。

在这个例子中,这个模块的属性和定义在这个模块中的 global names 之间存在直接的映射关系,他们共享同一个命名空间。

例如,前面的 modname.funcname ,指定了模块 modnamefuncname 属性;而 funcname 是这个模块中的一个 global name 。我们从模块的属性,映射到了模块的 global name

属性既可以是只读的(read-only),也可以是可写的(writable)的。如果一个属性是可写的的,我们就可以对它进行赋值。

而模块的属性就是可写的。例如, modname.the_answer = 42 我们也可以用 del 语句来删除一个可写的属性。例如, del modname.the_answer 将会从 modname 中删除 the_answer 这个属性。

# module1.py
the_answer = 42
import module1
# 访问模块属性
print (module1.the_answer)   # 输出 42
# 属性的可写性
module1.the_answer = 43 
print (module1.the_answer)   # 输出 43 
# 删除 module1 的 the_answer 属性
del module1.the_answer
try:
    print (module1.the_answer)
except Exception as e:
    print (e)   # module 'module1' has no attribute 'the_answer'

# 生命周期 lifetime

不同的命名空间会在不同的时间点被创建,并且拥有不同的生命周期(lifetimes)。具体规律概括如下:

  1. 包含了 built-in names 的命名空间在 Python 解释器启动时被创建,在解释器退出之前永远不会被删除。(实际上,built-in names 也是储存在一个叫 “builtins” 的模块中的)。

  2. 一个模块的 global 命名空间在这个模块的定义被读入的时候被创建,并且模块的命名空间也会一直存在,直到解释器退出。不论是从脚本文件中读取的还是在交互状态下读取的语句,由 Python 解释器的顶层调用执行的语句,会被认为是一个叫 "__main__" 的模块中的一部分。

  3. local 命名空间在一个函数被调用的时候被创建。如果这个函数 return 或者是 raise 了一个不由该函数处理的异常,则该命名空间被删除。注意,每个递归调用都有它们自己的 local 命名空间。

注意,我们在 "__main__" 模块中,可能会 import 很多其他的模块。此时,如果我们 import 了 n 个模块,就有 n+1 个命名空间。

# 作用域 Scope

# 作用域的定义

作用域是一个 Python 程序中的一个文本区域,在这个区域中,一个命名空间是可直接访问的(directly accessible)。其中,直接访问的意思是,当我们对于一个变量名尝试 unqualified reference 时,python 解释器会在这个命名空间中寻找这个变量名。

其中, unqualified reference 可以理解为,我们在调用 funcname 的时候,直接运行 funcname ,而不是运行 modname.funcname

# 下例代码中,local_var 是在 my_function 函数内部定义的,因此它具有局部作用域。在函数内部可以直接通过 local_var 访问它,这是一个 unqualified reference。
def my_function (): 
    local_var = “I am local” 
    print (local_var) # 直接访问局部变量
my_function () # 输出: I am local

# 作用域的搜索顺序

尽管作用域是静态地定义的,但是他们在使用的时候是动态的。在程序被执行的时候,不论什么时候都存在 3-4 个嵌套的作用域,他们的命名空间是可直接访问的:

最内部的作用域是最先被搜索的,这个作用域中包括了 local names;
任何封闭的函数(封闭的作用域),它其中包括虽然 non-local 但是又 non-global 的 names。其中封闭函数是从最近的封闭作用域搜索得到的。(这一点有点绕,请看第五部分的例子)
倒数第二个作用域包含当前 module 的 global names。
最外层的作用域(即最晚被搜索)是包含 built-in names 的命名空间。

在 Python 中,作用域(Scope)决定了变量名的可见性和如何解析名称。作用域是基于命名空间和 LEGB 规则(Local, Enclosing, Global, Built-in)来确定的。

# 控制我们对于变量的定义达到哪一个作用域

如果没有 globalnonlocal 语句,那么对名称的赋值总是进入可以达到的最内部的作用域。

如果一个变量名被声明(declared)为 global,那么所有的引用和赋值都会直接去到包含模块的 global names 的中间的作用域。

global 语句可用于指示特定变量存在于全局作用域中,并应在那里被重新绑定。

注意,关于 global 声明

  1. 只有在函数和类里面,才要在定义或者修改全局变量时,使用 global 声明。

  2. 在函数外部,local 作用域和 global 作用域拥有一样的命名空间,即这个模块的命名空间。

  3. 也就是说,在函数和类的外部(即 module 层级),如果要定义或者修改全局变量,我们不需要用 global 声明,此时对于任何变量的修改和定义都是对于全局变量的操作。

如果要重新绑定(rebind)在最内层作用域之外发现的变量,可以使用 nonlocal 语句。

nonlocal 语句指定特定变量存在于封闭作用域内,并应该在那里被重新绑定。

如果没有声明 nonlocal ,那么在最内层作用域之外的变量就是只读的,如果我们尝试对这个变量进行写入,那么实际上我们在最内层的作用域创建了一个新的 local 变量,同时保持同名的外层变量不变。(这一点有点绕,请看第五部分的例子)

这里,“重新绑定” 的意思就是让原来这个变量名指向一个新的内存地址。本文提到的所有” 绑定 “、” 重新绑定 “都是这个意思。

通常来说,local 作用域引用(reference)当前函数的文本范围内的 local names。我们通过定义类(class),在 local 作用域中放置了另一个命名空间。

# Python 中的作用域的特性

注意,作用域是由文本决定的,即在模块中定义的函数的 global 作用域是这个模块的命名空间,无论这个函数从哪里调用,或使用什么别名调用。

即,一个变量的作用域和这个变量被谁调用没关系,而是完全取决于这个变量被写在了哪段代码、哪个文件里。

名称的搜索实际上是在运行时动态完成的,然而,语言定义正在向在 “编译” 时的静态名称解析发展,所以我们不应依赖动态名称解析(实际上,局部变量已经是静态确定的)。

赋值并不会复制一份数据,它只是把名称和对象绑定。删除也是一样,语句 “del x” 从由 local 所引用的命名空间中删除了 x 的绑定。

事实上,所有引入新的 names 的操作都会使用 local 作用域。特别地,import 语句和函数定义在 local 作用域中绑定模块名或者函数名。

# 命名空间和作用域的举例:一

# 示例代码及运行结果

我们通过下面的例子来理解如何引用不同的作用域和命名空间,以及 globalnonlocal 如何影响变量的绑定(这里绑定的意思就是把变量名绑定到内存地址上)。

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

# 程序运行流程解析

# 开始运行

首先,程序看到第 1 行,然后跳到 21 行,发现调用函数,之后跳到第 1 行,运行这个函数。

从第 2 行开始,程序读取 3 个函数之后,快速跳到第 13 行。然后,开始往下逐行运行。

第 13 行,scope_test 函数下的 local name “spam” 指向的内存地址存着 “test spam” 这个字符串。

# local 赋值

第 14 行,执行 do_local 中的代码。

因此,程序跳到第 2 行,然后将一个 local name ”spam “绑定到存着” local spam“的内存地址中。

当第 3 行执行完时,整个 do_local function 也就执行完了,因此 do_local 的 local 命名空间也被删除。

此时,由于没有 name 绑定到”local spam“这个内存地址,python 的垃圾回收机制会起作用,删除这个字符串对象。

总结:do_local 这个函数中的赋值由于是 local 赋值,因此没有改变它的外层函数 scope_test 中的 spam 的值。
注意,在没有任何声明的时候,默认赋值方式是 local 赋值。

第 15 行,打印 spam 的值。

此时最先搜索 scope_test 函数的 local names。

由于在 13 行定义了 spam 指向存着”test spam“的内存地址,而 do_local 函数没有改变 cope_test 下的 spam 的值,因此上图中的第一行打印了”test spam“。

# nonlocal 赋值

第 16 行,执行 do_nonlocal 中的代码。

因此,程序跳到第 5 行,然后声明了一个 nonlocal 变量,并给这个变量赋值”nonlocal spam“。

由于有了 nonlocal 声明,因此这个变量的作用域会达到离它最近的封闭函数,即 scope_test 这个函数。因此,运行完第 6 行的语句后,scope_test 中的 spam 已经指向了”nonlocal spam“。

第 17 行,打印 spam 的值。

此时最先搜索 scope_test 函数的 local names。由于第 24 行的命令改变了 scope_test 下的 spam 的值,因此打印”nonlocal spam“。

此时我们可以再理解一下 nonlocal 的含义。

对于 do_nonlocal 来说,local 指的是这个函数里面的命名空间,而 global 指的是这个函数所在的模块的命名空间。

而我们发现,经过 nonlocal 声明后,spam 的作用域既不是 do_nonlocal 内部,也不是整个模块,而是离 do_nonlocal 最近的、往外一层的函数 scope_test。

因此,引入 nonlocal 这个声明,就是填补了这一个空白,让我们可以清楚地指定这个层次的作用域。

# global 赋值

第 18 行,执行 do_global 中的代码。

因此,程序跳到第 9 行,然后声明了一个 global 变量,并给这个变量赋值”global spam“。

由于有了 global 声明,因此这个 spam 变量会成为一个模块级的变量。因此,运行完第 19 行的语句后,scope_test 中的 spam 依旧指向”nonlocal spam“。

注意:此时在 scope_test 函数之外,在模块的层级上(即 "__main__" 模块上),已经新定义了一个叫 spam 的变量,它指向的内存地址存着”global spam“这个字符串。

第 19 行,打印 spam 的值。

此时最先搜索 scope_test 函数的 local names。由于第 26 行的命令是在模块层级新建了一个变量,没有改变 scope_test 下的 spam 的值,因此依旧打印”nonlocal spam“。

到此,第 21 行运行完了。

运行第 22 行

打印 "__main__" 模块下的 spam 变量的值。

此时最先搜索 scope_test 函数的 local names。由于第 10 行的命令在 "__main__" 模块层级新建了一个变量 spam,并给它赋值”global spam“,因此打印”global spam“。

到此,整个程序运行完了。

# 命名空间和作用域的举例:二

前面我们说到,一个变量的作用域和这个变量被谁调用没关系,而是完全取决于这个变量被写在了哪段代码、哪个文件里。

下面,我们会举一个例子来说明上面这一特性。

首先,作为看懂下面例子的课前预习,我们先了解两个 Python 的内置函数:globals () 和 locals ()。

globals () 会返回一个表示当前的全局代号表的字典,即这个字典里面存着当前所有的全局变量。

这个字典存着的全局变量永远是当前模块的全局变量,其中,” 当前模块 “指的是这个变量在哪个模块被定义(代码写在哪个 py 文件里面),而不是在哪个模块被调用。

locals () 会更新并且返回一个表示当前局部代号表的字典。当我们在函数块中调用 locals () 时,会返回自由变量,但是当我们在类块中调用时不会返回自由变量。

注意:

在 module 的层面,locals () 和 globals () 返回的字典是完全一样的。
我们不应修改 locals () 返回的字典。而且,我们对这一字典的更改可能不会影响解释器使用的局部变量和自由变量的值。
课前预习完成,下面我们正式开始我们的例子。

我们在一个 demo_test.py 文件中写入下面代码,这个 py 文件稍后会被 import。

def demo_global (param = None) -> dict:
    """
    接受参数 param, 返回 globals () 字典中 param 对应的值
    如果 globals () 中 不存在 param, 返回空字典
    如果 param 为空,返回整个 globals () 字典
    """
    print ("imported module - global:", end='')
    global_vars = globals ()
    if not param:
        return global_vars
    if param in global_vars:
        return {param: global_vars [param]}
    else:
        return {}
def demo_local (param = None) -> dict:
    """
    接受参数 param, 返回 locals () 字典中 param 对应的值
    如果 locals () 中 不存在 param, 返回空字典
    如果 param 为空,返回整个 locals () 字典
    """
    b = '200'
    local_vars = locals ()
    if not param:
        return local_vars
    print ("imported module - local:", end='')
    if param in local_vars:
        return {param: local_vars [param]}
    else:
        return {}
a = 100

然后在另一个 illustration.py 文件中写入下面代码,这个模块作为我们主运行的模块。

import demo_test
c='xxx'
print (demo_test.demo_local ())
print (demo_test.demo_global ('__name__'))
print (demo_test.demo_global ('a'))
print (locals ())
print (globals ())

当我们打印 demo_test 模块中的 local 变量的时候,只有一个 demo_local 函数里面的 b='200'。因为,在没有任何声明的时候,默认赋值方式是 local 赋值,而 local 作用域引用(reference)当前函数文本范围内的 local names。

当我们打印 demo_test 模块中的 global 变量的时候,会打印出比较多的内容。所以这里我们只输出了部分内容, '__name__' 变量等于这个模块的名字 demo_test ,且内容包含全局变量 a 的值 100,不包含 demo_test 的局部变量 b 和 illustration.py 中的全局变量 c

我们打印 illustration 模块中的 global 变量,首先可以看到 '__name__' ='__main__' ,此外,可以看到,在 illustration 模块中的 global 变量里面,没有 demo_test 模块中的全局变量 a=100。因为,这个 a=100 的 global 作用域只取决于它的文本位置,即它被写在 demo_test 模块中,而不取决于它被谁调用。

illustration 模块中的 globals () 变量和 locals () 输出的内容一致