命名空间与作用域
引言
在 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 声明它。这两个关键字既是 UnboundLocalError 的修法,也是造出同一件小东西的两条路:一个在多次调用之间存活下来的计数器。
先看全局版本。count_calls 需要跨越彼此独立的调用记住自己被运行了多少次——而一个局部名称每次调用都会被重建又丢弃,所以这个计数只能住在函数外面。
示例:住在全局命名空间里的计数器
它能用,但看看代价:这个计数是一个模块层变量,任何东西都能读它、覆盖它,而这个函数也只有挨着它才说得通。计数器并不真的属于这个世界;它属于这个函数。
nonlocal 让我们把这句话原样说出来。把计数放进一个外层函数,内层函数就能重新绑定它——于是计数住在外层那次调用的帧里,只有 counter 够得着,别人都够不着。
示例:同一个计数器,绑定在内层函数上
def make_counter():
calls = 0 # 住在 make_counter 的帧里
def counter():
nonlocal calls # 'calls' 指 make_counter 的那个局部
calls += 1
print(f"calling {calls} times")
return counter
c = make_counter()
c() # calling 1 times
c() # calling 2 times
d = make_counter() # 第二个、互不相干的计数器
d() # calling 1 times
print("calls" in globals()) # False —— 什么都没漏进全局命名空间
有两点值得注意。名称 calls 从未出现在全局命名空间里,所以这个计数器是真正私有的。而 c 和 d 各数各的,因为每一次调用 make_counter 都造了自己的帧、自己的 calls。这就是“一个共享的全局”与“绑定在某个特定函数上的值”之间的差别——也正是 nonlocal 存在的全部理由。
这个写法有个名字,叫闭包(closure),2.3 会把它讲透。在那里我们还会把这同一个计数器扩展成能包住另一个函数,并把那个函数所需要的、不论多少个实参一路传过去。
6.1 回到那个函数工厂
现在我们可以修好 2.1 §7 留下的谜题了。回想那个工厂:循环里造出的五个函数,每一个都返回 4,因为 f 没有属于自己的 i,只在被调用时才去查这个名称——查到了全局命名空间里。
这一页讲的每样东西都解释了它。i 不是 f 的局部(f 里没有任何地方给它赋值),也不是外层的(循环并不是一个函数),于是 LEGB 一路向外走到了全局的 i,而它在调用时刻的值是 4。
诚实的修法,是让每个函数都拥有自己的外层帧——把它造在一个函数里面,而不是造在一个光秃秃的循环里:
示例:修好的工厂
这里没有任何花招。每一次调用 make_f 都造出一个帧,装着那一轮的 i,而它返回的 f 抓住的正是那个帧里的名称。五次调用,五个帧,五个互不相干的值——和上面的 c 与 d 一模一样。
易错点:优先用 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 函数是一等对象 会把我们刚刚倚靠的嵌套函数推上主舞台:把函数作为实参传入、把它们返回,以及那让内层函数记住其外层作用域的闭包。