命名空间与作用域 (Namespaces and Scopes)
在 Python 编程中,理解变量的查找规则至关重要。为什么有时候在一个函数里访问不到外面的变量?为什么有时候会报 UnboundLocalError?这一切都与命名空间和作用域有关。
1. 命名空间 (Namespace)
命名空间本质上是一个从名字到对象的映射(就像一个字典)。Python 中主要有三种命名空间:
- 内置命名空间 (Built-in):包含 Python 内置的函数(如
print(),len())和异常。程序启动时创建,退出时销毁。 - 全局命名空间 (Global):包含模块定义的名称(函数、类、变量)。模块被导入时创建。
- 局部命名空间 (Local):包含函数内部定义的名称。函数调用时创建,返回时销毁。
2. 作用域与 LEGB 规则
作用域 (Scope) 是指在程序中可以直接访问某个命名空间的文本区域。
当你在 Python 中访问一个变量时,解释器会按照 LEGB 的顺序查找:
- L (Local):局部作用域。当前函数或类的方法内部。
- E (Enclosing):嵌套作用域(闭包)。外部嵌套函数的命名空间(从最近的一层往外找)。
- G (Global):全局作用域。当前模块的文件级别。
- B (Built-in):内置作用域。Python 内置模块。
如果这四层都找不到,就会抛出 NameError。
LEGB 示例
x = "Global" # 全局
def outer():
x = "Enclosing" # 嵌套
def inner():
x = "Local" # 局部
print(f"Inner: {x}")
inner()
print(f"Outer: {x}")
outer()
print(f"Global: {x}")
# 输出:
# Inner: Local
# Outer: Enclosing
# Global: Global
inner 中注释掉 x = "Local",它就会打印 "Enclosing"。
3. 修改外部变量:global 关键字
默认情况下,在函数内部对一个变量赋值,会创建一个新的局部变量,而不会修改全局变量。
如果你想在函数内部修改全局变量,必须使用 global 关键字声明。
count = 0
def add():
# count += 1 # ❌ 报错:UnboundLocalError,因为 Python 认为 count 是局部变量但未初始化
global count # ✅ 声明我们要使用全局变量 count
count += 1
add()
print(count) # 1
4. 修改嵌套变量:nonlocal 关键字
如果你在嵌套函数(闭包)中,想要修改外部函数(Enclosing 作用域)的变量,需要使用 nonlocal。
def make_counter():
count = 0
def counter():
nonlocal count # 声明我们要引用上一层函数的变量
count += 1
return count
return counter
c = make_counter()
print(c()) # 1
print(c()) # 2
print(c()) # 3
5. 常见陷阱:UnboundLocalError
这是一个非常容易犯的错误。
x = 10
def func():
print(x) # ❌ 这里会报错!
x = 20 # 因为这里进行了赋值,Python 编译时认定 x 是局部变量
func()
x = 20 赋值语句,因此将 x 标记为局部变量。但是执行到 print(x) 时,局部变量 x 还没有被赋值,所以报错。
解决:要么把 x 声明为 global,要么不要在函数内给 x 赋值(如果只是想读取全局 x)。
总结
- LEGB 规则决定了变量的查找顺序:局部 -> 嵌套 -> 全局 -> 内置。
- 在函数内部读取全局变量不需要特殊关键字。
- 在函数内部修改全局变量需要
global。 - 在嵌套函数内部修改外部函数的变量需要
nonlocal。
下一步
解决了“变量在哪里”的问题,我们再来聊聊“变量是什么类型”的问题。虽然 Python 是动态类型的,但在大型项目中,明确的类型信息能显著减少错误。