我并不喜欢 Python 的作用域的设计,但这门语言是如此流行,以至于很多时候你不得不去了解它. 本文试图比较全面地总结 Python 的作用域的相关规则.
本文基于 Python 3.6.8.
1、作用域划分
Python 中作用域的划分大致以“块”为单位. 什么是“块”呢?主要是模块、函数体、类定义(还有一些其他情况,例如函数 eval() 和 exec() 的字符串参数等). 所以,在 if / while / for 等语句中引入的变量会污染整个“块”:
>>>
2、名字查找
Python 的名字查找规则是符合直觉的:按照 local -> enclosing functions -> global -> built-in 的顺序由内而外查找,并选择最近的一个. 有一种说法叫 LEGB 规则(Local,Enclosing,Global,Built-in),可能是为了便于记忆,但我个人觉得直接按照直觉记忆即可.
在 Python 中,名字通过“绑定操作”引入. 以下这些结构执行“绑定操作”:函数参数、import 语句、类定义、函数定义,以及赋值、for 循环、with 或 except 中引入的新变量. 另外,del 语句可以取消一个名字的绑定.
如果在当前块中绑定一个名字,这个名字会被默认为是当前块中的,除非被声明为 nonlocal 或 global.
然而,Python 不区分“通过赋值操作引入新变量”和“给已有的变量赋值”. 当我们试图在内层作用域中修改外层变量时,这个特性就会将问题搞复杂. 比如说:
>>>
在这里,g 中的语句 a = 1 实际上是在 g 中引入了一个新变量,所以外层的 a 并没有被改变. 要想改变外层的 a, 就需要使用 nonlocal 语句,它会在 enclosing functions 中由内而外查找相应的变量:
>>>
相应地,要想在内层作用域中改变全局变量,就要使用 global 语句,它会按照 global -> built-in 的顺序由内而外查找相应的变量.
然而,在内层作用域中可以直接读取最近的外层 / 全局变量(只要当前作用域内没有重名的变量),而不需要 nonlocal / global 之类的语句:
>>>
注意,类定义也是一个名字空间,但它的影响范围不会延伸到方法中。所以,一个方法如果想要使用类变量或类中的其它方法,必须通过 self 参数:
>>>
3、名字绑定的影响是“前后双向”的
如果你在一个“块”中的任何地方绑定一个名字,该“块”中所有对该名字的使用都会被当成是指向当前“块”的. 这意味着以下的代码会报“赋值前使用”的错,因为这里 print(a) 中的 a 被认为是之后的 for 循环中的 a,即使外层有一个 a 也无济于事:
>>>
这时候,只要将 for 循环中的变量改个名字,print(a) 中的 a 就会被正常的名字查找过程在外层找到:
>>>
4. Lexical Scoping or Dynamic Scoping
Python 是 lexical scoping. 比如说:
>>>
虽然作用域是被静态决定的,但它们是被“动态使用”的。考虑下面的例子:
>>>
在这里,g 被定义时尚未有 x 的存在,但当 g 被调用时却可以使用 g 定义后引入的 x. 不过,g 中使用的 x 仍然属于 g 被定义时的外层作用域(即 f 的内部),从这个意义上说,此处的作用域规则仍然是 lexical scoping.
要注意的是,根据 Python 官方 Tutorial 的说法,Python 正在向“静态名字解析”方向演化,所以请勿依赖这种动态名字解析!
References
4. Execution model
7. Simple statements
9. Classes - Python 3.6.8 documentation
https://en.wikipedia.org/wiki/Scope_(computer_science)