← 返回题目列表

Python 的 LEGB 作用域规则是什么?

高频 中等 第 9 / 22 题 更新于 2026/07/25
Python作用域LEGBglobalnonlocal

简化版

LEGB 是 Python 查找名字的顺序:Local 局部作用域、Enclosing 外层嵌套函数作用域、Global 模块全局作用域、Built-in 内置作用域。找变量时按 L → E → G → B 查,赋值时默认会在当前局部作用域创建或绑定名字,除非使用 globalnonlocal

详细版

例子:

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 的查找顺序:

  1. Local:当前函数内部。
  2. Enclosing:外层嵌套函数内部。
  3. Global:当前模块全局变量。
  4. Built-in:Python 内置名字,例如 lenprintdict

例如:

print(len([1, 2, 3]))

当前作用域没有 len,模块全局没有 len,最后从 built-in 里找到内置函数 len。LEGB 只描述“名字查找”,不是对象生命周期,也不是包导入顺序。一个名字在哪一层被找到,取决于代码所在位置和编译时对赋值语句的分析。

inner 函数读取 name
   |
   v
Local(inner) -> Enclosing(outer) -> Global(module) -> Built-in

二、读取变量和赋值变量的规则不同

读取变量时会按 LEGB 查找;赋值变量时,如果没有声明 globalnonlocal,默认在当前局部作用域绑定名字。

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 还没有绑定任何对象,所以报错;它不是“找不到全局”,而是“已经判定要用局部,但局部尚未赋值”。

四、globalnonlocal 的区别

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 countcount += 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 已经被绑定成列表对象

常见不要用作变量名的名字:

  • list
  • dict
  • str
  • set
  • id
  • type
  • sum

这类覆盖不会立即报错,但会让后续代码变得非常怪。比如你写了 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.xClassName.x

八、加强记忆

把 LEGB 记成“读名字走楼梯,写名字先落地”:读变量从 Local、Enclosing、Global、Built-in 一层层找;写变量默认在当前函数局部落地。遇到闭包计数器、全局配置、内置名被覆盖时,先判断这行代码是在读取名字、重新绑定名字,还是通过引用修改对象内容,答案就很清楚。