名字绑定在哪一层,比缩进深浅更重要
跳转到“名字绑定在哪一层,比缩进深浅更重要”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) == 3try: 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 = 99squares = [index * index for index in range(3)]assert squares == [0, 1, 4] and index == 99print("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 == 1a += 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 == 1first = make_counter(10)second = make_counter(100)assert [first(), first(), second()] == [11, 12, 101]assert counter == 1print("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()) == 6print(series["a2"])如果任务确实涉及动态模块导出,再明确说明所写的命名空间、名称冲突与调用者约定。名字的可见范围也不等于对象的生存时间:闭包、容器或其他引用仍可能使对象继续存在。需要与 C 的块、链接属性和存储期作对照时,参见 Python、C 与 C++ 的作用域比较。