跳转至

命名空间与作用域

引言

在 2.1 我们看到,名称住在帧(frame)里,而它们所指的对象住在堆(heap)里。一次正在运行的调用把自己的局部名称保存在它的帧里;模块则把它的名称保存在顶层。每一组这样的名称都有一个正式的名字:命名空间(namespace)。本页回答紧随其后的两个问题。同一时刻有多少个命名空间在起作用?以及,当你写下一个光秃秃的名字(比如 x)时,Python 会到哪个命名空间里去找它?第二个问题的答案是一条四个字母的规则——LEGB——你几乎会用它来推断你读到的每一个函数。

和往常一样,这里的代码可运行——按 Run、修改、再运行。

1. 命名空间就是一部名称的字典 课件

命名空间正是 2.1 那些帧所暗示的东西:一个从名称到它们所指对象的映射——底下其实就是一部 Python 字典。写 x = 5 就是在你当前所处的那个命名空间里添加或更新条目 "x" → <对象 5>。没有比这更玄乎的了。

而且 Python 同时维护着好几个命名空间。其中两个你已经见过:全局(global)命名空间(模块顶层的名称,globals() 会把它返回)和一个局部(local)命名空间(正在运行的那次函数调用内部的名称,locals() 会把它返回)。

下面的示例表明,这两者确实是彼此独立的字典。

示例:globals() 与 locals()
title = "第 2 章"             # 一个全局名称

def show():
    note = "在 show 内部"      # 一个局部名称
    print("locals:", list(locals()))                # ['note']
    print("title 在全局里吗:", "title" in globals())  # True
    print("note 在全局里吗:", "note" in globals())    # False

show()
名称指向对象:命名空间不过是记录“哪个名称指向哪个对象”的那本账——还是我们一直在画的那些箭头,如今被收进了一部带标签的字典。
核心概念:命名空间

命名空间是一个从名称到对象的映射(一部字典)。Python 同时维护着好几个—— 内置(built-in)全局(global)外层(enclosing)局部 (local)——你用到的每一个名称,都是靠在它们之间逐层查找来解析的。

2. 函数可以定义在函数内部

函数体不过是普通代码,没有什么能阻止这段代码里再含一个 def。定义在另一个函数内部的函数叫做嵌套函数(nested function)——它能做一件新鲜事:它能看见包住它的那个函数的名称。

下面的示例把 inner 定义在 outer 内部。当 inner 运行时,它读取了 message——一个属于 outer 的局部名称——尽管 message 并不是 inner 自己的局部名称。

示例:内层函数看得见外层的名称
def outer():
    message = "hello from outer"
    def inner():
        print(message)      # inner 读取 outer 的局部名称 'message'
    inner()

outer()                     # hello from outer

有两点要注意。第一,写 def inner 只是在每次 outer 运行时创建 inner;和任何函数一样,不调用它就什么都不做,所以我们接着写了 inner()。第二,inner 只在 outer 运行期间存在——它只是 outer 的一个局部名称,随那次调用而生、随它而灭。

inner 能看见的那个外围作用域——outer 的命名空间——叫做外层(enclosing)作用域。它就是我们马上要见到的 LEGB 规则里的“E”,也正是 nonlocal 存在的全部理由。眼下知道这些就够了;在 2.3,我们会让嵌套函数真正派上用场——把它们传来传去、把它们返回。

名称指向对象:这里没有魔法——`inner` 只是没有自己的 `message`,所以当它需要这个名字时,Python 就到它周围的作用域里去找,找到了 `outer` 的那个。

3. 四个命名空间

在任意一刻,最多可以有四种命名空间同时在作用域中:

  • 局部(Local)——此刻正在运行的那次函数调用内部的名称:它的帧。调用开始时创建,返回时丢弃。
  • 外层(Enclosing)——某个外层函数的局部名称,被定义在它内部的嵌套函数所看见(就是我们 §2 刚见过的情形)。那个外层命名空间就是内层函数的外层命名空间。我们在 2.3 让嵌套函数真正派上用场。
  • 全局(Global)——在你模块顶层定义的名称。每个模块一个,模块启动时创建,整个程序期间存活。
  • 内置(Built-in)——Python 预先定义、处处可用的名称:printlenrangemaxTrueNone 等等。它们住在 builtins 模块里。

下面的示例确认,内置名称确实住在它们自己的一个模块里。

示例:内置命名空间
import builtins
print("print" in dir(builtins))   # True
print("len" in dir(builtins))     # True
print(len(dir(builtins)) > 100)   # True —— 内置名称有很多

4. 作用域与 LEGB 规则 课件

作用域(scope)是程序中一个名称无需任何限定就能够到的范围。每个命名空间都有一个作用域。当你使用一个光秃秃的名字时,Python 按一个固定的顺序在各命名空间里搜索,并在第一个匹配处停下:

局部(L)外层(E)全局(G)内置(B)

这就是 LEGB 规则。如果这四个里都找不到,Python 就抛出 NameError

这四个作用域层层嵌套,一个套在下一个里:名称搜索从最内层(局部)开始,一路向外直到内置。下面这张图就是地图。左边是作用域的名字;右边是住在各层里的代码。它还预告了 §6 的两个“逃生口”——global 让最内层的函数一直够到模块作用域去重新绑定一个名称,nonlocal 让它去重新绑定外层函数里的一个名称。

LEGB 作用域层层嵌套:内置 ⊃ 全局 ⊃ 外层 ⊃ 局部 四个嵌套的方框。内置(解释器)包含全局(模块),全局包含外层(嵌套函数),外层包含局部(正在运行的函数)。一条标着 global 的箭头从局部函数一直够到全局/模块作用域;一条标着 nonlocal 的箭头从局部够到外层作用域。 内置 全局 外层 局部 解释器 模块 嵌套函数 函数 global nonlocal

下面的示例把三个 x 放进三个不同的作用域;每一句 print(x) 都在搜索顺序里找到离它最近的那个。

示例:LEGB 实战
x = "全局"

def outer():
    x = "外层"
    def inner():
        x = "局部"
        print(x)        # 局部
    inner()
    print(x)            # 外层

outer()
print(x)                # 全局

下面的图把 inner() 正在运行的那一刻定格下来。这里有三个不同的名称,都拼作 x,各自在自己的命名空间里,各自指向堆里自己的那个字符串。inner 的查找先找到局部的 x,从不去问其余的。(内置命名空间还在更外一层,图里没画。)

memory: 堆 stack: 调用栈 objects: s1: '全局' s2: '外层' s3: '局部' globals: 全局命名空间 x -> s1 frame: outer() x -> s2 frame: inner() x -> s3

当一个名称在局部找不到时,搜索只是径直向外继续——这正是为什么一个函数不必任何仪式就能读取一个全局名称。

示例:查找一路落到全局
greeting = "hi"          # 全局

def f():
    print(greeting)      # 没有局部的 'greeting' —— 在全局作用域里找到

f()
格言的现身:“全局”没什么特别——它只是搜索在局部和外层之后才到达的那个命名空间。内置名称不过是它最后才到达的那一批。

5. 遮蔽:当一个名称盖住另一个 课件

在内层作用域里绑定一个外层也存在的名称,那么在这个作用域余下的部分里,内层的绑定就遮蔽(shadow)——盖住——外层的那个。通常这无害、甚至有用(每个函数都可以有自己的 iresult)。但偶尔它是个陷阱,尤其当你遮蔽了一个内置名称时最为痛苦。

示例:遮蔽内置的 max
print(max(1, 2))   # 2  —— 内置的 max

max = min          # 把全局名称 'max' 绑定到 min 函数上
print(max(1, 2))   # 1  —— 'max' 现在先找到我们的全局,而不是内置!

del max            # 删掉我们的全局;内置又变得可达了
print(max(1, 2))   # 2
易错点:别拿内置的名字给东西命名

人们很容易顺手用 listdictsummaxidtypestr 当变量名—— 而你一这么做,就在这个作用域余下的部分里失去了那个内置。症状令人摸不着头脑: list(...) 突然抛出 TypeError: 'list' object is not callable,因为 list 现在 是你的数据、不再是那个类型。换个别的名字吧(itemstotalkindtext)。

6. 在函数里赋值:默认就是局部

这条规则能解释大多数作用域上的意外:在一个函数里的任何地方给一个名称赋值,都会让那个名称在整个函数里都是局部的——哪怕存在一个同名的全局,哪怕是在赋值之前的行上。读取一个名称没问题(LEGB 会向外走、找到那个全局);但你一旦赋值,就等于声明了一个局部。

所以从函数内部读取一个全局,照样能行。

示例:读取一个全局
counter = 0

def show():
    print(counter)   # 读取那个全局 —— 打印 0

show()

同一个名称赋值,会让它在整个函数里都是局部的——如果你又先读了它,就会冒出一个著名的错误。

易错点:UnboundLocalError
counter = 0

def bump():
    counter = counter + 1   # UnboundLocalError
    return counter

# bump()

因为 counterbump 内部被赋值,它在整个函数里都是局部的——于是右边的 counter + 1 读到的是一个还没有值的局部。Python 在这里不会回退到那个全局。 修法是把你的意图说明白:用 globalnonlocal

要从函数内部重新绑定一个全局,用 global 声明它。要重新绑定一个外层函数里的名称,用 nonlocal 声明它。这两个关键字既是 UnboundLocalError 的修法,也是造出同一件小东西的两条路:一个在多次调用之间存活下来的计数器

先看全局版本。count_calls 需要跨越彼此独立的调用记住自己被运行了多少次——而一个局部名称每次调用都会被重建又丢弃,所以这个计数只能住在函数外面。

示例:住在全局命名空间里的计数器
calls = 0

def count_calls():
    global calls            # 'calls' 指模块层那个名称
    calls += 1
    print(f"calling {calls} times")

count_calls()               # calling 1 times
count_calls()               # calling 2 times
print(calls)                # 2

它能用,但看看代价:这个计数是一个模块层变量,任何东西都能读它、覆盖它,而这个函数也只有挨着它才说得通。计数器并不真的属于这个世界;它属于这个函数。

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 从未出现在全局命名空间里,所以这个计数器是真正私有的。而 cd 各数各的,因为每一次调用 make_counter 都造了自己的帧、自己的 calls。这就是“一个共享的全局”与“绑定在某个特定函数上的值”之间的差别——也正是 nonlocal 存在的全部理由。

这个写法有个名字,叫闭包(closure),2.3 会把它讲透。在那里我们还会把这同一个计数器扩展成能包住另一个函数,并把那个函数所需要的、不论多少个实参一路传过去。

6.1 回到那个函数工厂

现在我们可以修好 2.1 §7 留下的谜题了。回想那个工厂:循环里造出的五个函数,每一个都返回 4,因为 f 没有属于自己的 i,只在被调用时才去查这个名称——查到了全局命名空间里。

这一页讲的每样东西都解释了它。i 不是 f 的局部(f 里没有任何地方给它赋值),也不是外层的(循环并不是一个函数),于是 LEGB 一路向外走到了全局i,而它在调用时刻的值是 4

诚实的修法,是让每个函数都拥有自己的外层帧——把它造在一个函数里面,而不是造在一个光秃秃的循环里:

示例:修好的工厂
def make_f(i):            # 每次调用都拿到自己的帧,和自己的 i
    def f():
        return i          # 现在是外层的,不是全局的——每次调用各自捕获
    return f

ff = {i: make_f(i) for i in range(5)}

print(ff[3]())            # 3
print(ff[0]())            # 0

这里没有任何花招。每一次调用 make_f 都造出一个帧,装着那一轮的 i,而它返回的 f 抓住的正是那个帧里的名称。五次调用,五个帧,五个互不相干的值——和上面的 cd 一模一样。

名称指代对象,而一个名称只有在某个命名空间里才有意义。2.1 里循环的 `i` 和 `f` 内部的 `i` 是同一个全局名称,所以五个函数共用了它。把定义包进 `make_f`,才让每个函数各有一个私有的、外层的 `i`。
易错点:优先用 return,而不是 global

global 能用,但一个悄悄改写模块层变量的函数难以跟踪、也容易出错。更清楚的习惯 是把值作为实参传进来、用 return 把结果交回去,让每个函数自成一体。把 global/nonlocal 留给少数真正需要共享、可变状态的情形。

7. 到底为什么要有作用域? 课件

深入了解:作用域给你带来了什么

作用域不是繁文缛节——它们物有所值。封装: 因为一个函数的局部名称在外面看不见, 你可以在每个函数里重复使用 ixresult 这样朴素的名字而不必担心撞车。 信息隐藏: 一个函数的内部名称保持私有,于是调用者只依赖它返回什么,而不依赖它 怎么干。回收内存: 因为局部命名空间在调用返回时消失,那些只被它引用过的对象 失去了最后一个引用,可以立刻被释放——也就是我们在 2.1 提过的引用计数清理。小而 作用域清楚的函数之所以更易读、易测、易推理,正是因为它们的名称漏不出去。

小结

Python 通过一摞命名空间来解析每一个光秃秃的名字:

命名空间 适用范围 查看方式 生命期
局部 某一次正在运行的调用内部 locals() 那次调用
外层 嵌套函数内部 外层调用运行期间
全局 整个模块 globals() 整个程序
内置 处处(除非被遮蔽) dir(builtins) 整个程序

一个名字按 L → E → G → B 查找,在第一个匹配处停下(或抛 NameError)。赋值会让一个名称在整个函数里都是局部的,所以只有当你真的想重新绑定一个外层名称时,才动用 globalnonlocal——否则更推荐传入实参、返回结果。遮蔽你自己的名字没问题,但遮蔽内置则很危险。

接下来,2.3 函数是一等对象 会把我们刚刚倚靠的嵌套函数推上主舞台:把函数作为实参传入、把它们返回,以及那让内层函数记住其外层作用域的闭包。