Chapter 2: Name Binding and Scope
读懂 Python 的名字规则,要能从一段代码判断三个问题:这个名字在哪里绑定,读取时沿哪条路径查找,编译器会把它归入哪一类符号。本章围绕这三个问题展开。读完后,你应能解释 UnboundLocalError、闭包计数器、global / nonlocal、推导式循环变量和 CPython symbol table 之间的关系。
贯穿本章的代码现象是一个计数器工厂。它同时包含模块变量、函数局部变量、闭包变量、内置名字和推导式作用域,足够覆盖本章的名字绑定路径。
count = 100
def make_counter(step):
total = 0
def inc():
nonlocal total
total = total + step
return total
values = [inc() for _ in range(3)]
return values, inc
values, counter = make_counter(2)
这段代码的关键点是:count 绑定在模块 namespace,step 和 total 绑定在 make_counter 的函数 namespace,inc 绑定在 make_counter 的 local namespace,同时 inc 自身又创建一个新的函数 namespace。inc 读取 step,并通过 nonlocal total 重绑定外层的 total。推导式中的 _ 位于推导式自己的隐式 scope 中,range 按普通名字查找路径进入 builtins。
Python 语言规范把名字绑定和解析放在 execution model 下说明;CPython 则在 AST 到 bytecode 之间通过 symbol table 先给名字分类。正文会以语言语义为主线,并在最后一节把它落到 CPython 的编译前分析形状上。涉及官方资料时,本章使用 Python 3.14 Language Reference: Execution model、Simple statements: global and nonlocal、Expressions: comprehensions 和 symtable 标准库文档 作为语义边界。
2.1 Name binding and namespace ownership
Name binding 是把一个名字记录到某个 namespace 中,并让这个名字指向一个对象。名字本身只是标识符,namespace 是保存标识符到对象引用映射的位置,scope 则决定代码文本中哪些区域能看到这些绑定。理解名字规则时,先判断绑定发生在哪个 namespace,再判断读取时查找哪些 namespace。
在 Python 中,赋值目标、函数参数、函数定义、类定义、import、for 目标、with ... as 目标、except ... as 目标、结构匹配捕获名和 assignment expression 都会引入名字绑定。del 在语义上解除绑定,但在编译器分类时也会让名字被视为当前 block 中的绑定目标,因为它同样改变当前 block 对这个名字的所有权判断。
最小示例如下。它展示同一个名字在两个 namespace 中可以各自拥有独立绑定。
value = "module"
def read_and_bind():
value = "function"
return value
result = read_and_bind()
模块级 value 属于模块 namespace。函数体里的 value = "function" 在 read_and_bind 的函数 namespace 中创建局部绑定。函数返回的是函数局部 value 指向的字符串对象,模块级 value 仍然指向 "module"。这里没有复制变量,发生的是两个 namespace 中的两个名字各自指向对象。
函数定义本身也会绑定名字。执行 def read_and_bind(): ... 时,解释器创建一个 function object,并把当前 block 中的名字 read_and_bind 绑定到这个 function object。调用这个 function object 时,才会创建新的 frame,并让参数和局部变量进入该调用对应的 local namespace。
类定义也会绑定名字,但 class body 的 namespace 后续会变成 class object 的属性字典。这个边界容易影响源码阅读:class body 中的赋值先进入临时 class namespace,类对象创建完成后,外层 block 中的类名再绑定到 class object。
class Config:
timeout = 30
Config.timeout
执行 class statement 时,timeout 先位于 class body 的 namespace。Config 这个名字在外层 block 绑定到类对象。后续 Config.timeout 是 attribute lookup,已经进入对象系统的属性查找范围;它和本章的普通名字查找路径不同,后续 Attribute Lookup System 章节会展开。
对贯穿示例来说,名字所有权可以这样判断:count 属于模块,make_counter 属于模块,step 属于 make_counter 的参数绑定,total 属于 make_counter 的局部绑定,inc 这个名字属于 make_counter 的局部绑定,values 在 make_counter 内部绑定一次,在模块尾部又绑定一次。名字相同只说明标识符文本相同,是否指向同一对象取决于它们所属 namespace。
2.2 LEGB lookup chain
LEGB 是普通名字读取时的查找顺序:local、enclosing、global、builtins。它回答的是“读取一个未加点号的名字时,从哪里找对象”。这个顺序只处理 ordinary name lookup;obj.attr 属于 attribute lookup,mapping[key] 属于 subscription,二者进入不同协议。
下面的代码展示四层查找路径。inner 中读取 message、prefix、len 和 items 时,解释器会根据编译器给出的名字分类选择相应查找方式。
message = "global"
def outer(items):
prefix = "count"
def inner():
return f"{prefix}: {len(items)} / {message}"
return inner
inner 的 local namespace 没有绑定 prefix、items、message、len。prefix 和 items 来自 enclosing function scope,也就是 outer。message 来自模块 global namespace。len 在模块 namespace 中找不到时进入 builtins namespace。这个顺序解释了为什么覆盖 builtins 名字会影响当前模块的普通名字读取。
len = 10
def size(items):
return len(items)
这段代码中,size 读取 len 时会在模块 global namespace 找到整数 10,随后调用 10(items) 触发 TypeError。问题来源于 global namespace 中的绑定遮蔽了 builtins 中的 len。判断这类错误时,先看当前函数是否绑定该名字,再看外层函数,再看模块,再看 builtins。
LEGB 还解释 UnboundLocalError。只要某个名字在函数 block 的任意位置出现绑定操作,编译器就把它归为该函数的 local。读取发生在赋值执行之前时,运行期会在 local namespace 中查找到“被分类为 local 但尚未绑定到值”的状态。
status = "ready"
def report():
print(status)
status = "done"
report 中的 status = "done" 让整个函数 block 内的 status 被归为 local。print(status) 读取的是 local status,此时该名字还没有运行到绑定语句,因此抛出 UnboundLocalError。这条规则来自编译期对整段 function block 的扫描,而非运行期逐行决定名字归属。
名字查找可以概括为下面的路径。图中“分类结果”来自编译期 symbol table;运行期查找按分类结果访问 frame、closure cell、module dict 或 builtins dict。
这张图的边界是普通名字读取。它没有覆盖属性查找、下标访问、pattern matching 的解构规则,也没有覆盖 eval / exec 自定义 globals 和 locals 后的动态路径。对源码阅读来说,先确认“这是普通名字读取”,再套用 LEGB,能减少很多错误归因。
2.3 Free variable, cell variable, and closure capture
Free variable 是当前 block 中被读取、但绑定来自外层 function scope 的名字。Cell variable 是当前 function block 中的局部绑定,同时被内层 function 捕获的名字。Closure capture 是把这些跨函数访问的绑定放入 cell object,让外层函数返回后,内层函数仍能访问对应对象。
回到贯穿示例,inc 中的 step 和 total 都来自 make_counter。对 inc 来说,它们是 free variables。对 make_counter 来说,step 和 total 需要被内层 inc 使用,因此它们被提升为 cell variables。这个“提升”是 CPython 的实现策略;语言层面要掌握的结论是:闭包捕获的是绑定位置里的对象引用通道,后续读取会通过这个通道取得当前对象。
def make_counter(step):
total = 0
def inc():
nonlocal total
total = total + step
return total
return inc
counter = make_counter(2)
first = counter()
second = counter()
第一次调用 make_counter(2) 时,step 绑定到整数对象 2,total 绑定到整数对象 0,inc function object 记录它需要的 closure cells。make_counter 返回后,它的普通 frame 已经结束,但 counter 持有的 function object 仍然持有 closure cells,所以 total 的状态可以在多次调用 counter() 之间延续。
nonlocal total 的作用是把 inc 中对 total 的绑定操作指向外层 cell。没有这条声明,total = total + step 会让 total 在 inc 中被归为 local,右侧读取 total 时就会出现 local 未绑定状态。
def make_broken_counter(step):
total = 0
def inc():
total = total + step
return total
return inc
这段错误样例的核心在编译期分类。inc 中出现 total = ...,所以 total 被归为 inc 的 local。右侧 total + step 读取 local total,执行到这一行时 local total 尚未有值,于是触发 UnboundLocalError。step 没有在 inc 中绑定,它仍然是 free variable,可以从外层 cell 读取。
闭包还会影响循环中的函数创建。下面的代码展示多个函数共享同一个外层 cell 的结果。
def build_functions():
functions = []
for index in range(3):
def reader():
return index
functions.append(reader)
return functions
readers = build_functions()
results = [reader() for reader in readers]
reader 捕获的是 index 这个绑定通道。for 循环每轮重绑定同一个 local 名字 index,三个 reader function object 共享这个 cell。循环结束时 index 的最后值是 2,所以三个函数后续读取都会得到 2。这说明 closure capture 关联的是变量绑定,后续读取会看到 cell 当前保存的对象引用。
需要给每个函数固定独立值时,可以用参数默认值在函数创建时建立新的绑定。
def build_functions_fixed():
functions = []
for index in range(3):
def reader(index=index):
return index
functions.append(reader)
return functions
这里 index=index 让内层函数拥有自己的参数默认值。默认值在函数对象创建时求值并保存在 function object 上,后续调用 reader() 时,参数 index 先在当前函数 local namespace 中绑定到对应默认对象。这个写法改变的是绑定位置:从共享外层 cell 改为每个 function object 的默认参数对象。
2.4 global, nonlocal, and comprehension scope
global 和 nonlocal 是编译期指令。它们改变当前 block 中指定名字的绑定归属,并且作用于整个当前 scope。理解这两个语句时,要把它们放在 symbol table 分类阶段,而非普通运行期分支中。
global name 指定当前 block 中的 name 读写都走模块 global namespace。它常见于函数内修改模块级状态。
mode = "idle"
def activate():
global mode
mode = "active"
activate 中的 mode = "active" 绑定目标被归入模块 namespace。没有 global mode 时,mode 会成为 activate 的 local。global 语句需要在同一 scope 内早于对该名字的使用和赋值声明;这一点由 parser / compiler 阶段检查,错误会以 SyntaxError 形式出现。
nonlocal name 指定当前 block 中的 name 来自最近的 enclosing function scope,并允许当前 block 重绑定那个外层绑定。它要求外层 function scope 已经存在这个名字的绑定。
def outer():
total = 0
def add(value):
nonlocal total
total += value
return total
return add
nonlocal total 使 add 中的 total += value 读取和写回都作用在外层 outer 的 cell 上。nonlocal 查找的是外层函数作用域;它的目标范围限于 enclosing function scope。某个名字在外层函数 scope 中没有绑定时,nonlocal 声明会在编译阶段触发 SyntaxError。
推导式 scope 解决的是另一个问题:列表、集合、字典推导式和生成器表达式会创建隐式嵌套 scope,让循环变量留在推导式内部。官方表达式规范同时规定,最左侧 for 的 iterable 表达式在外层 scope 中求值,然后传给隐式嵌套 scope。
index = "outer"
values = [index * 2 for index in range(3)]
after = index
执行后,after 仍然是 "outer"。推导式中的 index 是推导式内部绑定,不会覆盖外层 index。range(3) 这个最左侧 iterable 表达式在外层 scope 中求值,所以 range 的名字查找发生在外层环境里。随后每轮迭代的 index 绑定发生在推导式的隐式 scope 中。
推导式还能捕获外层变量。下面代码中,factor 来自外层函数,number 属于推导式内部 scope。
def scale(numbers, factor):
return [number * factor for number in numbers]
factor 在推导式内部被读取,但绑定来自 scale 的函数 scope,因此它是推导式隐式函数视角下的 free variable。number 是推导式迭代变量,属于隐式 scope 的 local。这个模型解释了为什么推导式变量不会泄漏,同时也解释了它为什么能读取外层函数参数。
class body 和推导式的组合有特殊边界。普通方法和推导式的嵌套函数 scope 不能直接读取 class body 中刚绑定的名字;class body 结束后,那些名字成为类对象属性,需要走 attribute lookup。
class Settings:
base = 10
values = [base + item for item in range(3)]
这段代码会在推导式内部读取 base 时触发名字解析问题。base 是 class namespace 中的名字,推导式的隐式嵌套 scope 不把 class namespace 当作普通 enclosing function scope。稳定写法是把依赖移到 class body 外部、使用字面值、或在类创建后通过 Settings.base 走属性访问。
2.5 Compile-time symbol table analysis
Python 的名字查找看起来发生在运行期,但名字类别先在编译期确定。CPython 从源文本得到 AST 后,会在生成 bytecode 之前构建 symbol table。symbol table 的职责是计算每个 identifier 在每个 block 中的作用域类别,例如 local、global、free、cell、parameter、imported、assigned。标准库 symtable 模块公开了这层分析结果的检查接口。
这个阶段解释了两个现象。第一,UnboundLocalError 由整段 block 的绑定扫描决定,所以赋值写在函数后半段,也会影响前半段的读取分类。第二,global 和 nonlocal 是 parser / compiler 指令,放在字符串传给 exec 的代码里时,只影响那段字符串编译出来的 code object,不会回头改变外层已经编译好的 block。
可以用 symtable 观察分类结果。下面代码只作为观察工具,不要求读者依赖它才能判断语义。
import symtable
source = """
name = 'module'
def outer(step):
total = 0
def inner():
nonlocal total
total = total + step
return total
return inner
"""
table = symtable.symtable(source, "example.py", "exec")
outer_table = next(child for child in table.get_children() if child.get_name() == "outer")
inner_table = next(child for child in outer_table.get_children() if child.get_name() == "inner")
print(outer_table.lookup("total").is_local())
print(inner_table.lookup("total").is_nonlocal())
print(inner_table.lookup("step").is_free())
outer 里的 total 是 local,同时被内层函数使用,所以在 CPython 后续编译中会成为 cell。inner 里的 total 被 nonlocal 声明指向外层绑定,step 在 inner 中只读取不绑定,因此是 free variable。不同 Python 版本中 symtable 暴露的枚举和值可能扩展,特别是 Python 3.12+ 的 annotation scope、Python 3.14 的部分 symbol 查询能力;本章使用它只说明“编译期先分类”这个模型。
CPython 源码形状也对应这个判断。Python/symtable.c 中的分析过程会处理 DEF_GLOBAL、DEF_NONLOCAL、DEF_BOUND 等标记,把名字放入 local、global、free 等集合;随后 compiler 根据这些分类选择不同的访问路径。这里不需要记住每个 C 函数名,当前章要建立的源码阅读入口是:名字规则的实现横跨 parser 产生的 AST、symbol table 分类和 bytecode 生成,运行期 frame 查找只执行已经分类后的结果。
把本章模型迁移到任意 Python 片段时,可以按这个顺序检查:先标出每个 block,包括 module、function、class、comprehension;再列出每个 block 内的绑定操作;接着应用 global 和 nonlocal 指令;然后判断内层读取是否形成 free variable;最后把普通名字读取映射到 local、cell/free、module global、builtins 的访问路径。这个顺序能同时解释语义错误和 CPython 编译结果。
最小自检任务
阅读下面代码,判断每个 print 输出或触发的异常,并说明 x、y、item 分别绑定在哪个 scope。
x = "module"
def outer():
x = "outer"
y = []
def inner():
nonlocal y
y.append(x)
return y
values = [item for item in range(2)]
print("after comprehension:", "item" in locals())
return inner, values
reader, values = outer()
print(reader())
print(values)
答案要点
outer 中的 x 绑定在 outer 的函数 local scope,inner 读取 x 时把它作为 free variable 捕获。y 也绑定在 outer 的函数 local scope,inner 中的 nonlocal y 让 y.append(x) 访问外层同一个 list 对象;这里没有重绑定 y,但声明仍然把名字归属固定到外层 scope。推导式中的 item 绑定在推导式隐式 scope 中,所以 "item" in locals() 在 outer 中得到 False。reader() 返回的 list 为 ["outer"],values 为 [0, 1]。
本章知识点总结
- 名字绑定:name binding 把标识符放入某个 namespace,并让它指向一个对象。
- Namespace 所有权:同名标识符可以位于不同 namespace,是否相关取决于绑定归属。
- Scope 可见性:scope 决定代码文本中哪些位置能读取某个 namespace 中的绑定。
- 函数定义:
def执行时创建 function object,并把函数名绑定到当前 block。 - 类定义:class body 使用临时 namespace,类创建后外层名字绑定到 class object。
- LEGB 路径:普通名字读取按 local、enclosing、global、builtins 的顺序取得对象。
- 局部分类:函数 block 中任意绑定操作都会让该名字在整个 block 内被归为 local。
- 闭包变量:free variable 由内层读取外层函数绑定形成,cell variable 由外层绑定被内层捕获形成。
- Closure cell:闭包通过 cell 保留跨函数访问通道,让外层函数返回后仍能读取或更新状态。
- global 指令:
global把当前 block 中指定名字的读写归入模块 namespace。 - nonlocal 指令:
nonlocal把当前 block 中指定名字的读写归入最近的外层函数 scope。 - 推导式 scope:推导式循环变量位于隐式嵌套 scope,最左侧 iterable 表达式在外层 scope 求值。
- Class 边界:class namespace 会变成类属性,普通嵌套函数和推导式读取它时不会把它当作函数 enclosing scope。
- 编译期分析:CPython 在生成 bytecode 前用 symbol table 给名字分类,运行期按分类结果访问 frame、cell、globals 或 builtins。
- 检查顺序:分析名字问题时,先找 block,再找绑定,再应用 scope 声明,再判断 free/cell,最后映射到运行期查找路径。