Python 变量到底从哪来?——LEGB 作用域规则详解与实战

2026-05-12 27 预计阅读时间:1 分钟
来源:realpython.com AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:8 分钟

写 Python 时,你一定遇到过这种困惑:明明在函数外定义了变量,函数内却读不到;或者想在函数里修改外部变量,结果要么静默失败,要么抛出 UnboundLocalError。这些问题的根源都在一条规则——LEGB。理解它,作用域相关的 bug 就不再是玄学。

LEGB:四个字母,四层查找顺序

Python 遇到一个变量名时,按 Local → Enclosing → Global → Built-in 的顺序逐层查找,找到就停,找不到就报 NameError

  • Local(L):当前函数体内的局部命名空间。
  • Enclosing(E):外层嵌套函数的命名空间——只有闭包/嵌套函数才会产生这一层。
  • Global(G):模块顶层(即 .py 文件)的命名空间。
  • Built-in(B):Python 内置命名空间,如 lenprintint

下面用一个可运行的例子,把四层全部展示出来:

# global_scope.py
from builtins import len as builtin_len  # 演示 B 层被覆盖

total = 100  # G 层:模块全局变量

def outer():
    subtotal = 50  # E 层:outer 的局部变量,对 inner 来说是 enclosing

    def inner(count):
        # L 层:inner 的局部变量
        discount = 10
        # 逐层查找演示
        print("discount (L):", discount)       # Local
        print("subtotal (E):", subtotal)        # Enclosing
        print("total    (G):", total)           # Global
        print("len      (B):", builtin_len)     # Built-in(被我们 import 后改名)
        return subtotal - discount + count

    return inner

fn = outer()
print("result:", fn(5))

运行:

python global_scope.py

输出:

discount (L): 10
subtotal (E): 50
total    (G): 100
len      (B): <built-in function len>
result: 45

每一层都找到了对应的变量,顺序清晰可见。

两个最容易踩的坑

坑一:赋值即局部,哪怕还没执行到那行

Python 在编译函数时,只要发现函数体内有对某变量名的 赋值(包括 =+=defimport 等),就把该变量标记为 Local。这意味着——即使赋值语句在引用之后,引用也会直接找 Local 层,而此时 Local 层还没绑定值,于是抛出 UnboundLocalError

x = 42

def broken():
    print(x)   # 想读全局 x,但下一行有赋值,x 已被标记为 Local
    x = 99     # 赋值导致 x 变成局部变量

broken()       # UnboundLocalError: cannot access local variable 'x'

修复方式:用 global 声明告诉 Python "这个 x 我要的是全局层"。

x = 42

def fixed():
    global x
    print(x)   # 现在明确指向 Global 层的 x
    x = 99     # 修改的是全局 x

fixed()
print("x after fixed():", x)  # 99

坑二:嵌套函数里想改外层变量,global 不够用

global 只能跳到模块顶层。如果你想修改的是 Enclosing 层(外层函数的变量),需要用 nonlocal

def make_counter():
    count = 0          # E 层变量

    def increment():
        nonlocal count  # 声明 count 来自 Enclosing 层
        count += 1
        return count

    return increment

c = make_counter()
print(c())  # 1
print(c())  # 2
print(c())  # 3

如果去掉 nonlocalcount += 1 会把 count 标记为 Local,同样触发 UnboundLocalError

实战:用 LEGB 知识排查真实问题

下面模拟一个常见场景——配置字典在多层函数间传递时,不小心被局部赋值覆盖。

# config_bug_demo.py
CONFIG = {"timeout": 30, "retries": 3}

def process_request(url):
    # 想根据 url 调整 timeout,但写成了赋值而非修改
    if "slow" in url:
        CONFIG = {"timeout": 120, "retries": 5}  # 整个 CONFIG 变成局部变量!

    print("timeout:", CONFIG["timeout"])
    return CONFIG

result = process_request("https://slow-api.example.com")
print("global CONFIG unchanged?", CONFIG)  # 全局 CONFIG 还是原值

运行后你会发现:函数内的 CONFIG 是一个全新的局部字典,全局 CONFIG 完全没被影响。这往往不是你想要的。

正确做法——修改字典内容而非重新赋值:

# config_fix_demo.py
CONFIG = {"timeout": 30, "retries": 3}

def process_request(url):
    if "slow" in url:
        CONFIG["timeout"] = 120   # 修改已有键,不触发"赋值即局部"
        CONFIG["retries"] = 5

    print("timeout:", CONFIG["timeout"])
    return CONFIG

process_request("https://slow-api.example.com")
print("global CONFIG now:", CONFIG)  # 全局 CONFIG 已被修改

对可变对象(dict、list)做 原地修改CONFIG["key"] = ...items.append(...))不会触发"赋值即局部"规则,因为这不是对变量名本身的赋值,而是对对象的方法/索引操作。这是 Python 作用域里一个关键的区别。

闭包与延迟绑定:LEGB 的查找时机

LEGB 查找发生在 运行时,不是定义时。这意味着闭包中的变量引用是"延迟绑定"的——它拿到的是最终值,而非定义时的值。

funcs = []
for i in range(3):
    funcs.append(lambda: i)   # 所有 lambda 共享同一个 i

print([f() for f in funcs])  # [2, 2, 2],不是 [0, 1, 2]

循环结束时 i 的值是 2,三个 lambda 在被调用时才去查找 i,都找到同一个 Global 层的 2

修复:用默认参数在定义时捕获值(默认参数在函数定义时求值,属于 Local 层)。

funcs = []
for i in range(3):
    funcs.append(lambda i=i: i)  # i 的默认值在定义时绑定

print([f() for f in funcs])  # [0, 1, 2]

作用域排查清单

遇到变量找不到或行为异常时,按这个顺序检查:

检查项 怎么看
函数体内是否有对该变量名的赋值? 有赋值 → 变量被标记为 Local,即使赋值在引用之后
想修改全局变量? global 声明
想修改外层函数变量? nonlocal 声明
对 dict/list 做原地修改? 不需要声明,直接 obj[key] = ...obj.append(...)
闭包里循环变量延迟绑定? 用默认参数 lambda x=x: x 在定义时捕获
确认查找层级? 在可疑位置加 print(locals()) / print(globals()) 观察命名空间

最后一条是最直接的调试手段——打印命名空间,看变量到底在哪一层、绑定的是什么值。比猜来猜去高效得多。

LEGB 规则本身不复杂,但"赋值即局部"和"运行时查找"这两个机制组合起来,会产生不少反直觉的行为。记住这两点,再配合上面的排查清单,作用域问题就能快速定位,不再靠运气写代码。


相关推荐