Python在执行一行代码时,解释器需要先确定代码中每个名字到底指向哪个对象,这个过程就是变量查找。很多初学者写的代码报NameError或者UnboundLocalError,根源都是没弄清楚查找顺序。Python采用一套被称为LEGB的规则来决定名字的解析顺序,理解这套规则是掌握闭包、装饰器以及模块化设计的基础。

从命名空间到LEGB:作用域的本质
要理解LEGB,首先要明白Python中的命名空间(namespace)。命名空间本质上是一个从名字到对象的映射,你可以把它想象成一本字典,键是变量名,值是对象的引用。Python在运行时同时存在多个命名空间,比如内置函数所在的内置命名空间、每个模块自己的全局命名空间、每次函数调用时创建的局部命名空间。
LEGB是四层作用域的缩写,按照查找优先级从高到低排列:L代表Local(局部作用域),E代表Enclosing(外层函数的嵌套作用域),G代表Global(模块级全局作用域),B代表Builtin(内置作用域)。当Python遇到一个名字时,会严格按照L到E到G再到B的顺序查找,找到第一个匹配就停止,如果四层都没有找到,才会抛出NameError异常。
这个顺序是单向的、只读向外的。也就是说,内层代码可以读取外层的名字,但外层代码无法看到内层的名字。函数一旦执行结束,它的局部命名空间就会被销毁,除非有闭包引用把它留住。这种设计保证了模块之间的隔离性,也解释了为什么函数可以访问全局变量,而全局代码却访问不到函数内部的临时变量。
逐层拆解四层作用域的查找行为
Local是当前函数内部的作用域。每次函数被调用,Python都会创建一个新的局部命名空间,函数参数也放在这一层。需要特别注意,Python在编译阶段就会决定一个名字是不是局部变量:只要函数体内任何位置出现了对某个名字的赋值语句,这个名字在整个函数内都会被视为局部变量。这就是著名的UnboundLocalError的来源。
x = 100
def demo():
print(x) # 报错:UnboundLocalError
x = 200 # 因为这行赋值,x 在整个函数内被判定为局部变量
demo()</code>上面这段代码中,虽然全局有x,但函数内的x = 200让Python在编译时把x标记为局部名,于是在赋值之前访问就触发了未绑定错误。解决办法是使用global x声明,或者尽量避免在函数内修改全局变量。
Enclosing是嵌套函数场景下外层函数的作用域。当内层函数引用一个名字,而这个名字既不在内层局部,也不在全局时,Python会到包裹它的外层函数中查找。这一层是闭包能够工作的基石。下面的例子展示了内层函数如何捕获外层变量:
def outer():
msg = 'hello from outer'
def inner():
print(msg) # 依次查找 L -> E,在 Enclosing 层找到 msg
return inner
func = outer()
func() # 输出:hello from outer即使outer已经执行完毕,inner依然能访问msg,因为inner的__closure__属性保存了对msg的引用,这就是闭包。需要注意,嵌套可能有多层,Enclosing的查找会从最近的内层向外逐层进行。
Global是模块级别的作用域,一个.py文件内的顶层变量都生活在这里。无论函数嵌套多少层,只要名字不在局部和Enclosing中,最终都会到模块全局找。Builtin则是最外层,存放着print、len、range这些内置名字,由builtins模块提供。可以通过dir(builtins)查看全部内置名字。一个常见的坑是自己定义了名为list或len的变量,从而遮蔽了内置名字:
len = 10 # 全局作用域遮蔽了内置的 len 函数
print(len) # 输出 10,正常
print(len('abc')) # 报错:TypeError,int 不可调用global与nonlocal:主动修改外层变量
LEGB默认只允许读取外层变量,如果想在函数内部修改全局变量或外层函数变量,就需要显式声明。global关键字告诉Python这个名字指向全局作用域,nonlocal则指向最近的Enclosing层。两者都不能用于声明还不存在的名字。
counter = 0
def increase():
global counter
counter += 1
def make_counter():
count = 0
def add():
nonlocal count
count += 1
return count
return add
increase()
print(counter) # 输出 1
add = make_counter()
add()
print(add()) # 输出 2,count 被闭包持久保存使用global通常被认为是不好的实践,因为它让函数产生了隐藏的副作用,破坏了模块化。相比之下,nonlocal配合闭包实现的状态封装更加优雅,很多装饰器内部就是靠它来维护计数器或缓存状态的。
还有一个容易混淆的点:类体本身是一个作用域,但类体内的名字对外部不可见,类的嵌套作用域不会参与函数的LEGB查找。另外,for循环和if分支不会创建新的作用域,循环变量会泄漏到所在函数或模块中,这点和许多其他语言不同,写代码时要特别留意变量遮蔽问题。
掌握LEGB规则之后,遇到NameError、UnboundLocalError这类错误时,你可以按照局部、嵌套、全局、内置四层顺序快速排查名字到底定义在哪里、是否被赋值语句意外遮蔽。作用域问题看似细节,却是读懂Python闭包与装饰器源码的必经之路。
Python变量查找LEGB规则Python作用域修改时间:2026-08-31 02:26:33