跳转到内容
新建笔记

Python 作用域:名字绑定、global 与 nonlocal

名字绑定在哪一层,比缩进深浅更重要

跳转到“名字绑定在哪一层,比缩进深浅更重要”

Python 的变量名指向对象。判断一行代码会读到哪个对象、赋值会改变哪个名字,需要区分当前代码所在的模块、函数或类,以及这行代码是读取名字、重新绑定名字,还是修改对象。

本文以 Python 3.11 为准,整合原来的循环作用域片段与 global / nonlocal 笔记。普通 if、for、while 不创建新的局部作用域,但“它们里面定义的变量都是全局变量”不成立:在函数内赋值通常属于该函数,在模块顶层赋值才属于模块。Python 其他版本增加的注解作用域等规则需另查对应版本。

场景名字通常绑定的位置
模块顶层赋值,包括顶层循环体当前模块的命名空间
函数参数、函数体内赋值或导入当前函数的局部名字,除非声明了 global / nonlocal
嵌套函数读取未在自身绑定的名字按词法嵌套关系查找外层函数
类体内赋值类体执行时的命名空间,随后通常成为类属性
列表、集合、字典推导式中的循环目标推导式自己的作用域,不泄漏普通循环目标

循环不会建立新作用域,也不保证一定赋过值

跳转到“循环不会建立新作用域,也不保证一定赋过值”

下面修正原片段缺少冒号、混入输出标记的问题,并同时验证函数内循环和零次循环。

for i in range(4):
buffer = "22"
assert i == 3 and buffer == "22"
def last_value(count):
for index in range(count):
result = index
return result
assert last_value(4) == 3
try:
last_value(0)
except UnboundLocalError:
print("zero iterations: local result was never assigned")
else:
raise AssertionError("expected an unbound local")
assert "result" not in globals()
index = 99
squares = [index * index for index in range(3)]
assert squares == [0, 1, 4] and index == 99
print("module loop:", i, buffer)

result 是函数局部名字,但 count=0 时没有实际绑定值。把 return result 放在循环后,不会自动产生默认值。处理可能为空的数据时,应明确选择初值、返回 None、拒绝空输入或其他符合业务语义的分支。

在普通模块级循环中,循环变量可能继续存在;函数内的普通循环变量则仍属于函数。推导式的循环目标又有独立规则,所以不能只用“缩进块外还能不能访问”概括所有语法。Python 3.11 名字绑定与解析

LEGB 是普通名字查找的起点

跳转到“LEGB 是普通名字查找的起点”

常见函数中的普通名字按 Local、Enclosing、Global、Builtins 查找,即当前局部、词法外层函数、当前模块全局、内置名字。这里的 Global 是定义函数的模块,不是调用者的局部变量,也不是整个进程共享的一张“万能变量表”。

赋值还会影响编译器对名字的判断:在函数体中某处出现对 a 的绑定,未声明 global / nonlocal 时,该函数中的 a 就按局部名字处理,不会因为某次读取发生在赋值前而临时回退到模块值。

a = 0
def broken_increment():
a += 1
try:
broken_increment()
except UnboundLocalError:
print("a += 1 reads an unbound local before assigning it")
else:
raise AssertionError("expected UnboundLocalError")
assert a == 0
def next_value(value):
return value + 1
a = next_value(a)
assert a == 1

a += 1 同时需要读取与赋回名字。优先用参数和返回值表达状态变化,通常比隐式修改外部变量更易测试。若设计上确实要改变模块状态或闭包状态,再使用下面两种声明。Python 官方 FAQ:UnboundLocalError

global 指向模块,nonlocal 指向最近的外层函数绑定

跳转到“global 指向模块,nonlocal 指向最近的外层函数绑定”
counter = 0
def increment_global():
global counter
counter += 1
return counter
def make_counter(start):
count = start
def middle():
def increment():
nonlocal count
count += 1
return count
return increment
return middle()
assert increment_global() == 1 and counter == 1
first = make_counter(10)
second = make_counter(100)
assert [first(), first(), second()] == [11, 12, 101]
assert counter == 1
print("global and two independent closure states passed")

middle() 没有自己的 count,但 nonlocal count 仍然有效,因为它能找到更外层 make_counter() 中的绑定。因此原说法“只找上一级函数”过窄;准确理解是寻找最近的、已有同名绑定的外层函数作用域,不把模块全局作为 nonlocal 目标。

global 是名字解析声明,不是变量值初始化。声明后第一次读取一个从未绑定的模块名字仍可得到 NameError;如果随后先对它赋值,才会创建该模块绑定。声明还须符合语法顺序约束,不能在同一代码块先使用名字、再声明该名字。nonlocal 没有可用的外层函数绑定时,是编译阶段的 SyntaxError,而非调用后才发现。global 与 nonlocal

修改对象不等于重新绑定名字

跳转到“修改对象不等于重新绑定名字”
values = []
def append_value():
values.append(1)
def replace_local():
values = [2]
return values
append_value()
local_values = replace_local()
assert values == [1] and local_values == [2]
assert values is not local_values
late = [lambda: item for item in range(3)]
captured = [lambda item=item: item for item in range(3)]
assert [function() for function in late] == [2, 2, 2]
assert [function() for function in captured] == [0, 1, 2]
print("object mutation, rebinding and closure lookup passed")

append 修改列表对象,函数读取外层的 values 就足够;values = [...] 则创建当前函数的局部绑定。这个区别也解释了为什么修改字典条目通常不需要 global,而对名字做增量赋值可能需要。关于对象共享和复制,参见 copy 与对象身份。

闭包保留的是可在调用时查找的绑定关系,并不自动把每轮循环值拍成快照。第一组 lambda 在循环结束后读到同一个最终值;第二组利用函数默认参数在函数创建时求值,分别保留 0、1、2。若默认值本身是可变对象,还要另行处理共享修改,不能把该技巧理解为自动深复制。

类属性查找不是方法内部的普通 LEGB 查找

跳转到“类属性查找不是方法内部的普通 LEGB 查找”
label = "module"
class Example:
label = "class"
def compare(self):
return label, self.label, type(self).label
instance = Example()
assert instance.compare() == ("module", "class", "class")
instance.label = "instance"
assert instance.compare() == ("module", "instance", "class")
assert Example.label == "class"
print(instance.compare())

方法里的裸名字 label 不把类属性表当作普通外层函数作用域;self.label 才使用属性查找,type(self).label 从实例的类型取属性。这里没有继承和自定义描述符,复杂属性查找还要遵守相应规则。类体中的推导式也有特殊边界,因此 LEGB 口诀不能替代完整的语法上下文判断。类定义与属性

连号变量通常应改为一个集合

跳转到“连号变量通常应改为一个集合”

原例通过 globals()[f"a{i}"] = i 创建 a1、a2、a3。这确实是在写当前模块的名字表,但名字数量变动后,遍历、传参和清理都会更难处理。对于同一组数据,字典或列表通常更直接。

series = {f"a{index}": index for index in range(1, 4)}
assert series == {"a1": 1, "a2": 2, "a3": 3}
assert sum(series.values()) == 6
print(series["a2"])

如果任务确实涉及动态模块导出,再明确说明所写的命名空间、名称冲突与调用者约定。名字的可见范围也不等于对象的生存时间:闭包、容器或其他引用仍可能使对象继续存在。需要与 C 的块、链接属性和存储期作对照时,参见 Python、C 与 C++ 的作用域比较。