在 Python 里,一个变量名到底指向哪个对象,并不是靠声明决定的,而是由命名空间和查找顺序共同决定。理解这两点,你就掌握了变量作用域的核心原理,也能一眼看出“变量找不到”或“被意外覆盖”这类问题的根源。
什么是命名空间
- 命名空间就是名字到对象的映射,你可以把它想象成一个字典:
{‘x’: 10, ‘func’: <function>}。 - Python 中有多种命名空间,它们独立存在,互不干扰。同一个名字在不同命名空间里可以指向完全不同的对象。
- 每个模块、每个函数调用、每个类定义都会创建自己的命名空间。
三层命名空间
按照生命周期的长短和可见范围,命名空间可以分为三类:
1. 局部命名空间 (Local)
- 在哪里:函数或方法内部。
- 何时创建/销毁:函数被调用时创建,函数返回或抛出异常时销毁。
- 包含什么:函数内部定义的变量、参数。
- 特点:只在当前函数内可见,函数外无法访问。
def foo():
x = 10 # x 在 foo 的局部命名空间里
print(x)
foo()
# print(x) # 报错 NameError,外部看不到局部变量
2. 全局命名空间 (Global)
- 在哪里:模块级别(.py 文件顶层)。
- 何时创建/销毁:模块被导入时创建,解释器退出时销毁。
- 包含什么:模块顶层定义的变量、函数、类,以及通过
import引入的名字。 - 特点:该模块内的所有函数、类都可以读取全局变量,但要修改它需要用
global声明。
x = 10 # 全局变量
def bar():
print(x) # OK,读取全局变量
def baz():
global x # 声明要修改全局的 x
x = 20
3. 内置命名空间 (Built-in)
- 在哪里:
builtins模块里。 - 何时创建/销毁:Python 解释器启动时创建,一直存在到退出。
- 包含什么:内置函数(
print、len、range)、内置异常(Exception)、内置常量(True、None)。 - 特点:任何模块、任何函数都可以直接使用,不需要导入。
print(len('hello')) # print 和 len 都来自内置命名空间
另外,闭包还有一层“外层函数”的命名空间(Enclosing),我们放到作用域查找顺序里一起解释。
查找顺序:LEGB 规则
当你在代码里写一个变量名 name 时,Python 按 L → E → G → B 的顺序依次查找:
- L (Local):当前函数的局部命名空间
- E (Enclosing):外层函数的局部命名空间(如果有嵌套函数)
- G (Global):当前模块的全局命名空间
- B (Built-in):内置命名空间
一旦在某个层次找到了这个名字,就停止搜索,直接使用该名字对应的对象。如果四个层次都找不到,就抛出 NameError。
实操示例
x = 'global' # G 命名空间
def outer():
x = 'enclosing' # E 命名空间(对于 inner 来说)
def inner():
x = 'local' # L 命名空间(inner 自己的局部)
print(x) # 打印 'local',先在 L 找到
inner()
outer()
如果把 inner() 里的赋值注释掉:
def outer():
x = 'enclosing'
def inner():
print(x) # 打印 'enclosing',L 没有,找 E
inner()
如果 outer() 里的赋值也去掉,就会打印全局的 'global',最后才轮到内置命名空间(比如 print 函数自身的名字)。
赋值的默认行为
- 在函数内部直接写
x = ...,一定是在当前函数的局部命名空间创建或修改变量,不会自动去改全局变量。这正是许多新手犯错的根源:以为改的是全局,实际是新建局部。 - 要修改全局变量,必须在函数内用
global x声明;要修改外层函数变量(闭包场景),用nonlocal x声明。
两个实用的查看工具
locals():返回当前局部命名空间的字典(在模块级别调用和globals()相同)。globals():返回当前全局命名空间的字典。
在调试时,你可以在函数内打印 locals() 看看当前有哪些变量及其值,非常直观。
命名空间是 Python 变量作用域机制的底层基础,把握住 LEGB 查找规则和赋值默认行为,就能快速定位和避免作用域相关的坑,写出的代码逻辑也更清晰可控。