命名空间与作用域
引言
在 2.1 我们看到,名称住在帧(frame)里,而它们所指的对象住在堆(heap)里。一次正在运行的调用把自己的局部名称保存在它的帧里;模块则把它的名称保存在顶层。每一组这样的名称都有一个正式的名字:命名空间(namespace)。本页回答紧随其后的两个问题。同一时刻有多少个命名空间在起作用?以及,当你写下一个光秃秃的名字(比如 x)时,Python 会到哪个命名空间里去找它?第二个问题的答案是一条四个字母的规则——LEGB——你几乎会用它来推断你读到的每一个函数。
和往常一样,这里的代码可运行——按 Run、修改、再运行。
1. 命名空间就是一部名称的字典
命名空间正是 2.1 那些帧所暗示的东西:一个从名称到它们所指对象的映射——底下其实就是一部 Python 字典。写 x = 5 就是在你当前所处的那个命名空间里添加或更新条目 "x" → <对象 5>。没有比这更玄乎的了。
而且 Python 同时维护着好几个命名空间。其中两个你已经见过:全局(global)命名空间(模块顶层的名称,globals() 会把它返回)和一个局部(local)命名空间(正在运行的那次函数调用内部的名称,locals() 会把它返回)。
下面的示例表明,这两者确实是彼此独立的字典。
示例:globals() 与 locals()
核心概念:命名空间
命名空间是一个从名称到对象的映射(一部字典)。Python 同时维护着好几个—— 内置(built-in)、全局(global)、外层(enclosing)和局部 (local)——你用到的每一个名称,都是靠在它们之间逐层查找来解析的。
2. 函数可以定义在函数内部
函数体不过是普通代码,没有什么能阻止这段代码里再含一个 def。定义在另一个函数内部的函数叫做嵌套函数(nested function)——它能做一件新鲜事:它能看见包住它的那个函数的名称。
下面的示例把 inner 定义在 outer 内部。当 inner 运行时,它读取了 message——一个属于 outer 的局部名称——尽管 message 并不是 inner 自己的局部名称。
示例:内层函数看得见外层的名称
有两点要注意。第一,写 def inner 只是在每次 outer 运行时创建 inner;和任何函数一样,不调用它就什么都不做,所以我们接着写了 inner()。第二,inner 只在 outer 运行期间存在——它只是 outer 的一个局部名称,随那次调用而生、随它而灭。
inner 能看见的那个外围作用域——outer 的命名空间——叫做外层(enclosing)作用域。它就是我们马上要见到的 LEGB 规则里的“E”,也正是 nonlocal 存在的全部理由。眼下知道这些就够了;在 2.3,我们会让嵌套函数真正派上用场——把它们传来传去、把它们返回。
3. 四个命名空间
在任意一刻,最多可以有四种命名空间同时在作用域中:
- 局部(Local)——此刻正在运行的那次函数调用内部的名称:它的帧。调用开始时创建,返回时丢弃。
- 外层(Enclosing)——某个外层函数的局部名称,被定义在它内部的嵌套函数所看见(就是我们 §2 刚见过的情形)。那个外层命名空间就是内层函数的外层命名空间。我们在 2.3 让嵌套函数真正派上用场。
- 全局(Global)——在你模块顶层定义的名称。每个模块一个,模块启动时创建,整个程序期间存活。
- 内置(Built-in)——Python 预先定义、处处可用的名称:
print、len、range、max、True、None等等。它们住在builtins模块里。
下面的示例确认,内置名称确实住在它们自己的一个模块里。
示例:内置命名空间
4. 作用域与 LEGB 规则
作用域(scope)是程序中一个名称无需任何限定就能够到的范围。每个命名空间都有一个作用域。当你使用一个光秃秃的名字时,Python 按一个固定的顺序在各命名空间里搜索,并在第一个匹配处停下:
局部(L) → 外层(E) → 全局(G) → 内置(B)
这就是 LEGB 规则。如果这四个里都找不到,Python 就抛出 NameError。
这四个作用域层层嵌套,一个套在下一个里:名称搜索从最内层(局部)开始,一路向外直到内置。下面这张图就是地图。左边是作用域的名字;右边是住在各层里的代码。它还预告了 §6 的两个“逃生口”——global 让最内层的函数一直够到模块作用域去重新绑定一个名称,nonlocal 让它去重新绑定外层函数里的一个名称。
下面的示例把三个 x 放进三个不同的作用域;每一句 print(x) 都在搜索顺序里找到离它最近的那个。
示例:LEGB 实战
下面的图把 inner() 正在运行的那一刻定格下来。这里有三个不同的名称,都拼作 x,各自在自己的命名空间里,各自指向堆里自己的那个字符串。inner 的查找先找到局部的 x,从不去问其余的。(内置命名空间还在更外一层,图里没画。)
当一个名称在局部找不到时,搜索只是径直向外继续——这正是为什么一个函数不必任何仪式就能读取一个全局名称。
5. 遮蔽:当一个名称盖住另一个
在内层作用域里绑定一个外层也存在的名称,那么在这个作用域余下的部分里,内层的绑定就遮蔽(shadow)——盖住——外层的那个。通常这无害、甚至有用(每个函数都可以有自己的 i 或 result)。但偶尔它是个陷阱,尤其当你遮蔽了一个内置名称时最为痛苦。
示例:遮蔽内置的 max
易错点:别拿内置的名字给东西命名
人们很容易顺手用 list、dict、sum、max、id、type 或 str 当变量名——
而你一这么做,就在这个作用域余下的部分里失去了那个内置。症状令人摸不着头脑:
list(...) 突然抛出 TypeError: 'list' object is not callable,因为 list 现在
是你的数据、不再是那个类型。换个别的名字吧(items、total、kind、text)。
6. 在函数里赋值:默认就是局部
这条规则能解释大多数作用域上的意外:在一个函数里的任何地方给一个名称赋值,都会让那个名称在整个函数里都是局部的——哪怕存在一个同名的全局,哪怕是在赋值之前的行上。读取一个名称没问题(LEGB 会向外走、找到那个全局);但你一旦赋值,就等于声明了一个局部。
所以从函数内部读取一个全局,照样能行。
但给同一个名称赋值,会让它在整个函数里都是局部的——如果你又先读了它,就会冒出一个著名的错误。
易错点:UnboundLocalError
因为 counter 在 bump 内部被赋值,它在整个函数里都是局部的——于是右边的
counter + 1 读到的是一个还没有值的局部。Python 在这里不会回退到那个全局。
修法是把你的意图说明白:用 global 或 nonlocal。
要从函数内部重新绑定一个全局,用 global 声明它。要重新绑定一个外层函数里的名称,用 nonlocal 声明它。
示例:global 与 nonlocal
易错点:优先用 return,而不是 global
global 能用,但一个悄悄改写模块层变量的函数难以跟踪、也容易出错。更清楚的习惯
是把值作为实参传进来、用 return 把结果交回去,让每个函数自成一体。把
global/nonlocal 留给少数真正需要共享、可变状态的情形。
7. 到底为什么要有作用域?
深入了解:作用域给你带来了什么
作用域不是繁文缛节——它们物有所值。封装: 因为一个函数的局部名称在外面看不见,
你可以在每个函数里重复使用 i、x、result 这样朴素的名字而不必担心撞车。
信息隐藏: 一个函数的内部名称保持私有,于是调用者只依赖它返回什么,而不依赖它
怎么干。回收内存: 因为局部命名空间在调用返回时消失,那些只被它引用过的对象
失去了最后一个引用,可以立刻被释放——也就是我们在 2.1 提过的引用计数清理。小而
作用域清楚的函数之所以更易读、易测、易推理,正是因为它们的名称漏不出去。
小结
Python 通过一摞命名空间来解析每一个光秃秃的名字:
| 命名空间 | 适用范围 | 查看方式 | 生命期 |
|---|---|---|---|
| 局部 | 某一次正在运行的调用内部 | locals() |
那次调用 |
| 外层 | 嵌套函数内部 | — | 外层调用运行期间 |
| 全局 | 整个模块 | globals() |
整个程序 |
| 内置 | 处处(除非被遮蔽) | dir(builtins) |
整个程序 |
一个名字按 L → E → G → B 查找,在第一个匹配处停下(或抛 NameError)。赋值会让一个名称在整个函数里都是局部的,所以只有当你真的想重新绑定一个外层名称时,才动用 global 或 nonlocal——否则更推荐传入实参、返回结果。遮蔽你自己的名字没问题,但遮蔽内置则很危险。
接下来,2.3 函数是一等对象 会把我们刚刚倚靠的嵌套函数推上主舞台:把函数作为实参传入、把它们返回,以及那让内层函数记住其外层作用域的闭包。