KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA

01 · 作用域 — keel 龙骨

类型:主题概念 · 应用课对应:02 章 · 函数:参数规则、LEGB 与一等函数

类型:主题概念 · 应用课对应:02 章 · 函数:参数规则、LEGB 与一等函数

这一页只回答四件事:它为什么会出现、它是什么、它长什么样、它怎么工作。

一、它为什么会出现

一个动作,两个反应:

① 在函数里写 total = total + 1 —— 报错说「局部变量 total 没有值」
② 两个函数里各写一个 x,它们互不打架

同一个名字,在同一个文件里要同时做到两件相反的事:函数里面是私有的,外面又要有一些是共用的。
如果只有一套命名空间,①和②就都做不到。

问题在于:「这个名字从哪来」必须有一个确定的答案,而且答案不能取决于谁先跑过、谁后改过——
否则同一段函数在两次调用里会得到不同结果。

二、它是什么

一句话:作用域是一个名字可以被查到的范围;Python 在编译期就定好每个名字属于哪一层,运行时按固定的四层顺序去查。

由此有两个直接后果:

它不是这两样东西——混淆会带来误判:

三、它长什么样

以下是 CPython 3.13.14(Windows)上的输出。

第一种形态是报错原文——它把「这个操作数在函数里可见」记录下来,这是执行器唯一能捕捉的错误证据:

>>> def scope_demo():
...     print(x)
...     x = 1
>>> scope_demo()
Traceback (most recent call last):
  ...
UnboundLocalError: cannot access local variable 'x' where it is not associated with a value

第二种形态是四层里各放一个名字,看谁被查到:

>>> G = "global-G"
>>> def outer():
...     E = "enclosing-E"
...     def inner():
...         return G, E, L
...     L = "local-L"
...     return inner()
>>> outer()
('global-G', 'enclosing-E', 'local-L')

两种形态合起来说明一件事:inner 里三个名字都不在它自己的函数体里被赋值,
所以它们顺着 L → E → G 一路找到了 L(外层函数)、E(外层函数的局部)、G(模块全局)。

四、它怎么工作

一次名字查找是四步,前两步在编译期完成:

① 扫描函数体   凡是「被赋值、被 import、被 for 绑定、被 with as 绑定、是参数」的名字,全部登记为本函数的局部名
② 划界         这一步定死了:这名字属于本层,运行时不再改判
③ 运行时查找   Local(本函数)→ Enclosing(外层函数)→ Global(模块)→ Builtins(内置)
④ 真正绑定     赋值语句**执行到**的那一刻,这个名字才有值;在此之前读它 → UnboundLocalError

第 ①② 步解释了三件经常让人困惑的事:

现象 原因
上面能读、下面报未绑定 扫描结果让整个函数都把它当局部名,而绑定发生在赋值执行到的那一刻
内层函数改了外层变量,外层没变 内层那句赋值创建了一个新的局部名,不是改外层那个
类体里的名字,类里别处读不到 类体不参与 LEGB 这条链,它不构成一层作用域

最后一条真的会报错:

>>> class C:
...     tag = "class-tag"
...     listing = [tag for _ in range(1)]
NameError: name 'tag' is not defined

原因是列表推导式有自己的作用域(它像一个隐式函数),而它只看得见 LEGB 那四层——
类体不在里面。同一段代码改成方法里读 self.tag 就没事。

五、它会带出哪些概念

六、回到应用课

应用课里的那句话 现在你知道它为什么成立
"函数内赋值会让这个名字变成本地的" 编译期扫描就定了归属,与执行顺序无关;所以「先读后赋值」必然报未绑定
"要改外层变量得用 nonlocal" 不用声明的话,那句赋值是在本层登记一个新名字,外层那个从头到尾没被碰过
"LEGB 是查找顺序" 四层是词法的,查的是定义处的外层,不是调用栈上的上一层

这一页改变了你的哪个判断:遇到 UnboundLocalError,不要先去怀疑初始化顺序或全局变量,而是去函数体里找有没有一句对它赋值——找到了,问题就结束了:要么加 global/nonlocal,要么把名字改掉。

自测题

  1. 一个函数里写了 print(total),函数末尾有一句 total = 1。调用它会怎样?把末尾那句删掉又会怎样?
  2. 类体里 x = 1,紧接着的列表推导式里想读 x,为什么会报 NameError?改成方法里 self.x 为什么就行?

进入 keel 阅读