命名空间、生命周期与作用域

本文主要讲解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()输出的内容一致