Python中的命名空间和作用域
在 Python 中,命名空间(Namespace)和作用域(Scope)是两个紧密相关但概念不同的核心机制。理解它们对于编写无 Bug 的代码、理解变量的生命周期以及解决变量名冲突至关重要。
简单来说:
- 命名空间 决定了变量名和对象之间的绑定关系(变量存在哪里)。
- 作用域 决定了在代码的哪个位置可以访问到这些变量(在哪里能找到变量)。
一、命名空间 (Namespace)
命名空间本质上是一个从名称到对象的映射。在 Python 内部,命名空间通常是用字典(Dictionary)来实现的。
1. 三种主要的命名空间
Python 中主要有三种命名空间,它们的生命周期各不相同:
内置命名空间 (Built-in Namespace)
- 内容:包含 Python 内置的函数和异常(如
print(),len(),Exception等)。 - 生命周期:Python 解释器启动时创建,退出时销毁。
- 查看方式:
dir(__builtins__)
- 内容:包含 Python 内置的函数和异常(如
全局命名空间 (Global Namespace)
- 内容:包含模块(Module)级别的变量、函数定义、类定义等。
- 生命周期:在模块被读取(import 或运行)时创建,解释器退出或模块被卸载时销毁。
- 查看方式:
globals()
局部命名空间 (Local Namespace)
- 内容:包含函数内部定义的变量、参数等。
- 生命周期:函数被调用时创建,函数返回或抛出异常时销毁。每次调用函数都会创建一个新的局部命名空间。
- 查看方式:
locals()
2. 命名空间的查找顺序
虽然有不同的命名空间,但它们是隔离的。例如,两个不同的函数中都可以有一个名为 x 的变量,它们互不干扰,因为它们属于不同的局部命名空间。
二、作用域 (Scope) 与 LEGB 规则
作用域是指在代码的特定区域,你可以直接通过名称访问变量的范围。
Python 使用 LEGB 规则 来决定变量的查找顺序。当你引用一个变量时,Python 会按照以下顺序搜索命名空间:
- L (Local) - 局部作用域:
- 包含在当前函数或类方法内部定义的变量。
- E (Enclosing) - 嵌套(闭包)作用域:
- 包含在外部嵌套函数中的变量(例如:函数 A 里面定义了函数 B,B 访问 A 的变量)。
- G (Global) - 全局作用域:
- 当前模块(脚本)最外层定义的变量。
- B (Built-in) - 内置作用域:
- Python 预定义的变量(如
open,range)。
- Python 预定义的变量(如
查找原则:一旦找到第一个匹配的名称,搜索就会停止。如果四个层级都找不到,就会抛出 NameError。
代码示例:LEGB 演示
x = "Global x" # Global
def outer():
x = "Enclosing x" # Enclosing
def inner():
x = "Local x" # Local
print(x)
inner()
outer()
# 输出: Local x
# 如果注释掉 inner 中的 x,输出: Enclosing x
# 如果再注释掉 outer 中的 x,输出: Global x
三、修改变量:global 和 nonlocal 关键字
默认情况下,在函数内部赋值一个变量,Python 会将其视为在该函数的局部作用域中创建一个新变量,而不是修改外部变量。如果你想修改外部作用域的变量,需要使用关键字。
1. global 关键字
用于在局部作用域中修改全局变量。
count = 0 # 全局变量
def add():
global count # 声明我们要使用的是全局变量 count
count += 1 # 修改它
add()
print(count) # 输出: 1
如果不加 global,count += 1 会报错(UnboundLocalError),因为 Python 认为你要修改一个未初始化的局部变量。
2. nonlocal 关键字
用于在嵌套函数(内部函数)中修改嵌套作用域(Enclosing scope)的变量。它不能用于全局变量。
def outer():
x = 10
def inner():
nonlocal x # 声明我们要使用的是 outer 函数中的 x
x = 20
inner()
print(x)
outer() # 输出: 20
如果不加 nonlocal,inner 中的 x = 20 只会创建一个新的局部变量,outer 中的 x 仍然是 10。
四、常见陷阱 (The "Gotcha")
UnboundLocalError
这是一个非常经典的错误。请看下面的代码:
x = 10
def func():
print(x) # 试图打印全局的 x
x = 20 # 试图定义局部的 x
func()
结果:UnboundLocalError: local variable 'x' referenced before assignment
原因:
Python 在编译函数时,发现函数内部有赋值语句 x = 20。因此,它将 x 标记为局部变量。
当执行到 print(x) 时,Python 试图访问局部的 x,但此时赋值语句还没执行,局部变量 x 还没有绑定值,所以报错。
解决方法:
- 如果只是想读取全局
x,不要在函数内对x赋值。 - 如果想修改全局
x,在函数开头加上global x。
五、总结
| 概念 | 描述 | 关键点 |
|---|---|---|
| 命名空间 | 名字到对象的映射(字典)。 | 内置、全局、局部。 |
| 作用域 | 变量可见的代码区域。 | 决定了你在哪里能访问变量。 |
| LEGB 规则 | 变量查找顺序。 | Local -> Enclosing -> Global -> Built-in |
| global | 关键字。 | 允许在局部修改全局变量。 |
| nonlocal | 关键字。 | 允许在内部函数修改外部函数的变量。 |
理解这些规则,你就能明白为什么有时候变量“变了”,有时候又“没变”,以及为什么有时候会报“变量未定义”的错误。