Python 的 LEGB 作用域规则是什么?
简化版
LEGB 是 Python 查找名字的顺序:Local 局部作用域、Enclosing 外层嵌套函数作用域、Global 模块全局作用域、Built-in 内置作用域。找变量时按 L → E → G → B 查,赋值时默认会在当前局部作用域创建或绑定名字,除非使用 global 或 nonlocal。
详细版
例子:
x = "global"
def outer():
x = "enclosing"
def inner():
x = "local"
print(x)
inner()
outer() # local
如果 inner 里没有 x,会向外找:
x = "global"
def outer():
x = "enclosing"
def inner():
print(x)
inner()
outer() # enclosing
常见坑:
x = 10
def f():
print(x)
x = 20
这会报 UnboundLocalError,因为函数体里出现了 x = 20,Python 会把 x 当成本地变量;但 print(x) 时本地变量还没赋值。
需要修改全局变量用 global,修改外层嵌套函数变量用 nonlocal。
完整版教学
一、LEGB 分别是什么
LEGB 的查找顺序:
- Local:当前函数内部。
- Enclosing:外层嵌套函数内部。
- Global:当前模块全局变量。
- Built-in:Python 内置名字,例如
len、print、dict。
例如:
print(len([1, 2, 3]))
当前作用域没有 len,模块全局没有 len,最后从 built-in 里找到内置函数 len。LEGB 只描述“名字查找”,不是对象生命周期,也不是包导入顺序。一个名字在哪一层被找到,取决于代码所在位置和编译时对赋值语句的分析。
inner 函数读取 name
|
v
Local(inner) -> Enclosing(outer) -> Global(module) -> Built-in
二、读取变量和赋值变量的规则不同
读取变量时会按 LEGB 查找;赋值变量时,如果没有声明 global 或 nonlocal,默认在当前局部作用域绑定名字。
x = 1
def f():
x = 2
print(x)
f() # 2
print(x) # 1
函数内的 x = 2 创建了局部变量,不会修改模块全局的 x。这条规则非常重要:Python 不是执行到赋值那一行才临时决定作用域,而是在编译函数体时看到赋值,就把这个名字标记为局部变量。x += 1 也算赋值,因为它等价于读取 x、计算新值、再绑定 x。
| 操作 | 没有声明时的含义 |
|---|---|
print(x) | 按 LEGB 查找 |
x = 2 | 在当前局部作用域绑定 |
x += 1 | 读取局部 x 后再绑定,可能触发未绑定错误 |
global x | 后续 x 指向模块全局 |
nonlocal x | 后续 x 指向外层函数作用域 |
三、UnboundLocalError 为什么出现
经典例子:
x = 1
def f():
print(x)
x = 2
Python 在编译函数体时看到 x = 2,就判定 x 是局部变量。于是 print(x) 不是去读全局 x,而是读局部 x。但执行到 print(x) 时,局部 x 还没有赋值,所以报 UnboundLocalError,它是 NameError 的子类。
修复方式之一:
x = 1
def f():
global x
print(x)
x = 2
但不要滥用全局变量,业务代码里更推荐通过参数和返回值传递状态。数字例子可以这样看:全局 x = 1 明明存在,但函数局部符号表里也有一个 x,执行 print(x) 时局部 x 还没有绑定任何对象,所以报错;它不是“找不到全局”,而是“已经判定要用局部,但局部尚未赋值”。
四、global 和 nonlocal 的区别
global 表示这个名字来自模块全局作用域:
count = 0
def inc():
global count
count += 1
nonlocal 表示这个名字来自外层嵌套函数作用域:
def make_counter():
count = 0
def inc():
nonlocal count
count += 1
return count
return inc
如果没有 nonlocal count,count += 1 会被视为在 inc 内部给局部变量赋值,从而报错。nonlocal 不能绑定模块全局,也不能凭空创建一个外层变量;外层嵌套函数里必须已经存在这个名字。global 则直接指向模块命名空间,常用于脚本级计数或配置,但在复杂业务中会增加状态耦合。
global x -> module.x
nonlocal x -> nearest enclosing function scope that already has x
易错点:
global不是“全项目全局”,只是当前模块全局;nonlocal也不是上一层任意对象属性,而是外层函数作用域里的名字绑定。
五、不要覆盖内置名字
因为 Built-in 是最后一层作用域,所以你可以不小心覆盖内置名字:
list = [1, 2, 3]
# list("abc") # TypeError,因为 list 已经被绑定成列表对象
常见不要用作变量名的名字:
listdictstrsetidtypesum
这类覆盖不会立即报错,但会让后续代码变得非常怪。比如你写了 list = [1, 2, 3],后面再调用 list("abc"),Python 会先在当前作用域找到那个列表对象,不会继续去 Built-in 找真正的 list 类型,于是出现 'list' object is not callable。排查这类问题时,先看有没有把内置名当变量名。
六、闭包为什么能“记住”外层变量
LEGB 的 E 层就是闭包能工作的基础。外层函数调用结束后,如果内层函数还引用着外层变量,那么这些变量会被保存在闭包单元里,而不是简单销毁。计数器例子中,inc 每次调用都能访问同一个 count 绑定;加上 nonlocal 后,才能把 count 重新绑定为新整数。
def make_counter():
count = 0
def inc():
nonlocal count
count += 1
return count
return inc
c = make_counter()
print(c(), c(), c()) # 1 2 3
这里 3 次调用共享同一个闭包状态。如果 count 是列表,inc 里只做 count.append(1),不需要 nonlocal,因为那是修改列表对象内容;如果写 count = count + [1],就是重新绑定名字,需要 nonlocal。
七、常见误区与追问
- 误区:函数里能读全局变量,就一定能直接改全局变量。 读取按 LEGB,赋值默认局部;修改全局绑定必须声明
global。 - 误区:
UnboundLocalError是因为全局变量不存在。 全局变量可能存在,错误原因是函数内赋值让该名字被判定为局部,但读取时还没绑定。 - 误区:
nonlocal可以声明任何外部名字。 它只能指向外层嵌套函数中已经存在的名字,不能指向模块全局,也不能凭空创建。 - 追问:为什么
count.append(1)不需要nonlocal? 因为没有重新绑定count,只是通过已有引用修改列表对象。 - 追问:为什么覆盖
list后内置list()不能用了? 因为名字查找先在当前作用域命中你的变量,不会再去 Built-in 找内置类型。 - 追问:类作用域算不算 Enclosing? 函数嵌套的 E 层主要指外层函数作用域;类体有自己的命名空间,方法内普通名字查找不会把类属性当作 enclosing 局部变量,访问类属性通常用
self.x或ClassName.x。
八、加强记忆
把 LEGB 记成“读名字走楼梯,写名字先落地”:读变量从 Local、Enclosing、Global、Built-in 一层层找;写变量默认在当前函数局部落地。遇到闭包计数器、全局配置、内置名被覆盖时,先判断这行代码是在读取名字、重新绑定名字,还是通过引用修改对象内容,答案就很清楚。