03-变量作用域
Python 版本要求:Python 3.11+ 贯穿项目:functions_demo/ 代码位置:
app/core/scope.py测试验证:cd functions_demo && uv run pytest -k TestScope -v理解变量作用域,避免变量命名冲突和意外的数据修改。
概念铺垫
为什么需要理解作用域?一个真实的变量冲突场景
问题场景: 你在开发一个计数器程序,想要在函数内部修改外部变量。
变量冲突的困惑:
count = 0 # 全局变量
def increment():
count = count + 1 # 期望修改全局变量
return count
result = increment() # UnboundLocalError!问题:
- 为什么会报错?
- 如何在函数内修改全局变量?
- 嵌套函数中的变量如何处理?
理解作用域后的解决方案:
count = 0 # 全局变量
def increment() -> int:
global count # 声明使用全局变量
count += 1
return count
print(increment()) # 1
print(increment()) # 2
print(count) # 2这就是作用域的价值:明确变量的可见范围,避免意外冲突。
作用域解决了什么问题?
作用域的本质是:定义变量在哪些地方可以被访问。
就像不同房间的人:
- 房间里的人(局部变量):只能在本房间活动
- 楼层的人(嵌套作用域):能在本楼层活动
- 整栋楼的人(全局变量):能在整栋楼活动
作用域的优势:
- 避免冲突:不同作用域可以有同名变量
- 数据隔离:函数内部的数据不会被外部意外修改
- 内存管理:局部变量用完即销毁
- 代码清晰:明确变量的使用范围
L1 理解层:会用
作用域的最简用法
核心规则
函数内部读取全局变量是允许的,但修改全局变量必须使用 global 关键字声明。这是 Python 作用域最基础的规则。
为什么这样设计?
- 读取全局变量不会产生副作用,所以直接允许
- 修改全局变量会影响外部状态,必须显式声明以避免意外修改
- 不使用
global时,赋值操作会创建同名局部变量,而非修改全局变量
示例代码
# 全局变量
total = 0
def add(value: int) -> int:
# 局部变量
result = total + value # 可以读取全局变量
return result
print(add(10)) # 10
print(total) # 0(全局变量未被修改)
# 修改全局变量需要 global 声明
def increment() -> None:
global total
total += 1
increment()
print(total) # 1第一部分:局部变量和全局变量
概念说明
变量有"生命范围"(作用域)。函数内部定义的变量只在函数内有效(局部变量);函数外部定义的变量在整个文件都有效(全局变量)。
┌─────────────────────────────────────────────────────────────┐
│ 全局作用域(整个文件) │
│ │
│ global_var = "全局" │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 函数作用域(仅函数内部) │ │
│ │ │ │
│ │ local_var = "局部" │ │
│ │ 可以读取 global_var ✅ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ print(local_var) ← ❌ NameError │
│ │
└─────────────────────────────────────────────────────────────┘第二部分:LEGB 作用域规则
查找顺序
┌─────────────────────────────────────────────────────────────┐
│ LEGB 作用域查找规则 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 当使用一个变量时,Python 按以下顺序查找: │
│ │
│ L - Local(局部作用域) │
│ 当前函数内部的变量 │
│ │
│ E - Enclosing(封闭作用域) │
│ 外层嵌套函数的变量 │
│ │
│ G - Global(全局作用域) │
│ 模块级别的变量 │
│ │
│ B - Built-in(内置作用域) │
│ Python 内置的变量和函数 │
│ │
│ ───────────────────────────────────────────────────── │
│ │
│ 查找过程: │
│ Local → Enclosing → Global → Built-in │
│ │
│ 找到即停,找不到则抛 NameError │
│ │
└─────────────────────────────────────────────────────────────┘示例
下面的代码展示了 LEGB 四层作用域的实际查找过程:
x: str = "全局变量" # Global
def outer() -> None:
y: str = "外层变量" # Enclosing
def inner() -> None:
z: str = "内层变量" # Local
print(f"inner: {z}") # Local
print(f"inner: {y}") # Enclosing
print(f"inner: {x}") # Global
inner()
outer()执行过程分析:
print(z):在 Local 作用域找到z = "内层变量"print(y):Local 没有y,向上查 Enclosing,找到y = "外层变量"print(x):Local、Enclosing 都没有x,向上查 Global,找到x = "全局变量"- 如果查找 Built-in(如
print函数本身),则从 Python 内置命名空间获取
第三部分:global 关键字
修改全局变量
核心概念: 默认情况下,在函数内部赋值会创建新的局部变量,不会影响全局变量。要修改全局变量,需要用 global 声明。
为什么会报 UnboundLocalError? Python 在编译函数时,会扫描所有赋值语句。如果发现某个变量被赋值(如 count = count + 1),就将其标记为局部变量。但赋值右边的 count 还未定义,导致运行时报错。
global 的作用: 明确告诉 Python:"这个变量是全局的,不要创建局部副本"。
# ❌ 不使用 global(创建新的局部变量)
count = 0
def increment_wrong():
count = count + 1 # UnboundLocalError!
# ✅ 使用 global(修改全局变量)
count = 0
def increment() -> None:
global count # 声明使用全局的 count
count += 1
increment()
increment()
print(count) # 2global 使用场景
何时需要 global:
- 程序级别的配置开关(如调试模式)
- 全局计数器或统计器
- 单例模式的实现
何时不需要 global:
- 只是读取全局变量值
- 修改可变对象的内容(如列表的
append、字典的键值修改)
注意事项:
- 过多使用
global会降低代码的可维护性和可测试性 - 全局变量状态难以追踪,容易产生副作用
- 优先考虑使用类属性或闭包来替代全局变量
# 配置变量
config: dict[str, bool] = {"debug": False}
def enable_debug() -> None:
global config
config["debug"] = True
# 计数器
call_count: int = 0
def track_call() -> None:
global call_count
call_count += 1
print(f"第 {call_count} 次调用")
track_call() # 第 1 次调用
track_call() # 第 2 次调用第四部分:nonlocal 关键字
修改外层函数变量
核心概念:nonlocal 用于在嵌套函数中修改外层函数的变量(不是全局变量)。它让内层函数能够"跨越"一层作用域去操作外层的局部变量。
使用规则:
nonlocal只能用于 Enclosing 作用域的变量- 不能用于全局变量(会报 SyntaxError)
- 必须在赋值前声明,否则产生 UnboundLocalError
与 global 的区别:
global:跨越到模块级别nonlocal:只跨越到外层函数级别
# ❌ 不使用 nonlocal(会出错)
def counter_wrong():
count = 0
def increment():
count += 1 # UnboundLocalError!
return count
return increment
# ✅ 使用 nonlocal(正确)
def counter():
count = 0
def increment() -> int:
nonlocal count # 声明使用外层变量
count += 1
return count
return increment
my_counter = counter()
print(my_counter()) # 1
print(my_counter()) # 2
print(my_counter()) # 3global vs nonlocal
┌─────────────────────────────────────────────────────────────┐
│ global vs nonlocal │
├─────────────────────────────────────────────────────────────┤
│ │
│ global: │
│ • 引用/修改全局变量 │
│ • 作用于模块级别 │
│ │
│ nonlocal: │
│ • 引用/修改外层嵌套函数的变量 │
│ • 作用于嵌套函数之间 │
│ • 不能用于全局变量 │
│ │
│ ───────────────────────────────────────────────────── │
│ │
│ 选择指南: │
│ • 修改全局变量 → global │
│ • 修改外层函数变量 → nonlocal │
│ • 只读取变量 → 不需要声明 │
│ │
└─────────────────────────────────────────────────────────────┘渐进复杂:从简单到复杂的作用域
本节从最简单的作用域用法开始,逐步增加复杂度,帮助读者循序渐进地理解作用域机制。
层级 1:只读取全局变量
场景说明: 函数内部只需要访问全局变量的值,不进行任何修改。这是最简单、最安全的作用域用法。
核心要点:
- 函数内直接使用全局变量名即可读取其值
- 不需要任何声明关键字
- 全局变量的值不会被改变
message = "Hello"
def show() -> None:
print(message) # 读取全局变量
show() # Hello层级 2:局部变量覆盖
场景说明: 函数内部创建同名变量,此时局部变量"覆盖"了全局变量名,但全局变量本身并未被修改。
核心要点:
- 赋值操作默认创建局部变量
- 同名局部变量优先被使用(LEGB 规则的 L 优先)
- 函数执行完毕后,局部变量销毁,全局变量保持原值
message = "Global"
def show() -> None:
message = "Local" # 创建局部变量
print(message) # Local
show()
print(message) # Global(全局变量未改变)层级 3:使用 global 修改全局变量
场景说明: 当需要在函数内部真正修改全局变量的值时,必须显式声明 global。
核心要点:
- 使用
global 变量名声明后,赋值操作会修改全局变量而非创建局部变量 - 多次调用函数会累积修改效果
- 修改全局变量会影响整个程序状态,需谨慎使用
counter = 0
def increment() -> None:
global counter
counter += 1
increment()
increment()
print(counter) # 2层级 4:嵌套函数中的变量
场景说明: 嵌套函数(函数内定义函数)形成多层作用域,内层函数可以访问外层函数的变量。
核心要点:
- 内层函数可以读取外层函数的变量(Enclosing 作用域)
- 外层函数无法访问内层函数的局部变量
- 嵌套层级越深,作用域链条越长
def outer() -> None:
x = "outer"
def inner() -> None:
y = "inner"
print(x) # 可以访问外层变量
inner()
# print(y) # NameError
outer()层级 5:使用 nonlocal 修改外层变量
场景说明: 在嵌套函数中,如果内层函数需要修改外层函数的变量(不是全局变量),使用 nonlocal 关键字。
核心要点:
nonlocal只作用于 Enclosing 作用域,不涉及全局变量- 允许内层函数持久化修改外层函数的状态
- 常用于闭包(Closure)模式,实现状态保持
def make_multiplier(factor: int):
"""创建一个乘法器"""
def multiply(value: int) -> int:
nonlocal factor # 使用外层变量
result = value * factor
return result
# 可以修改 factor
def set_factor(new_factor: int) -> None:
nonlocal factor
factor = new_factor
multiply.set_factor = set_factor # 将内层函数挂载为属性(函数是对象,可附加属性)
return multiply
multiplier = make_multiplier(2)
print(multiplier(5)) # 10
multiplier.set_factor(3)
print(multiplier(5)) # 15关键代码说明:
| 代码 | 含义 | 为什么这样写 |
|---|---|---|
nonlocal factor | 声明在内层函数中修改外层变量 | 不加 nonlocal 赋值时会创建新的局部变量而不是修改外层的 factor |
def set_factor(new_factor: int) -> None | 定义一个修改 factor 的内层函数 | 将修改操作封装为函数,外部无法直接访问 factor |
multiply.set_factor = set_factor | 将 set_factor 挂载到 multiply 函数上 | 函数也是对象,可以附加属性,让调用者通过 multiply.set_factor(3) 修改内部状态 |
实际应用:累加器闭包
应用场景
闭包(Closure)是作用域的高级应用:内层函数"捕获"外层函数的变量,即使外层函数已经执行完毕,内层函数仍然可以访问和修改这些变量。
闭包的价值:
- 状态保持:不使用全局变量也能保持状态
- 数据封装:变量隐藏在函数内部,外部无法直接访问
- 函数工厂:根据参数生成定制化的函数
典型应用:
- 累加器、计分器
- 缓存装饰器
- 配置管理器
示例代码
def make_score_accumulator(initial: float = 0.0):
"""工厂函数:返回三个共享同一 total/count 状态的闭包"""
total: float = initial
count: int = 0
def add(score: float) -> float:
nonlocal total, count
total += score
count += 1
return total
def average() -> float:
return total / count if count > 0 else 0.0
def reset() -> None:
nonlocal total, count
total = initial
count = 0
return add, average, reset
# 使用
add, avg, reset = make_score_accumulator()
add(80)
add(90)
print(avg()) # 85.0工作原理解析:
total和count是外层函数make_score_accumulator的局部变量add、average、reset三个内层函数通过nonlocal访问和修改这些变量- 当
make_score_accumulator返回后,total和count仍然存在(被闭包捕获) - 三个闭包共享同一组状态,互相协作完成累加、求平均、重置功能
关键代码说明:
| 代码 | 含义 | 为什么这样写 |
|---|---|---|
nonlocal total, count | 声明修改外层变量 | 不加 nonlocal 赋值时会创建新的局部变量而不是修改外层的 total 和 count |
return add, average, reset | 以元组形式返回多个闭包 | 将多个操作封装成函数集合,无需定义类即可实现状态封装 |
total = initial(reset 内) | 重置到初始值而非 0 | 使用闭包捕获的 initial 而非硬编码 0,让累加器可从任意起点重置 |
average 不使用 nonlocal | 只读取不修改 | average 仅读取 total 和 count,无需 nonlocal 声明 |
L2 实践层:最佳实践
推荐做法
| 做法 | 原因 | 示例 |
|---|---|---|
| 避免使用 global | 全局变量难以追踪,副作用不可控 | 用类属性或闭包代替 |
| 优先使用局部变量 | 自动销毁,内存效率高,无命名冲突 | 函数内部定义变量 |
| 闭包替代 global | 状态封装,数据隐藏 | create_counter() 返回闭包 |
| nonlocal 用于闭包 | 嵌套函数间共享状态的标准方式 | 计数器、累加器 |
| 用类替代复杂作用域 | 更清晰的状态管理 | class Counter: |
| 明确变量来源 | 让代码自解释,减少隐式依赖 | 函数参数传递而非读取全局 |
反模式:不要这样做
# ❌ 滥用 global 变量
total = 0
count = 0
average = 0
def add_value(value: int) -> None:
global total, count
total += value
count += 1
def calculate_average() -> None:
global average
average = total / count
# 问题:
# 1. 三个全局变量分散在不同函数中修改
# 2. 调用顺序依赖:必须先调用 add_value 再 calculate_average
# 3. 无法并发使用:多个调用者会互相干扰
# 4. 测试困难:每次测试需要重置全局变量# ✅ 正确做法:用类封装状态
class Calculator:
"""计算器类"""
def __init__(self) -> None:
self.total: int = 0
self.count: int = 0
def add_value(self, value: int) -> None:
self.total += value
self.count += 1
def get_average(self) -> float:
return self.total / self.count if self.count > 0 else 0.0
# 使用
calc = Calculator()
calc.add_value(10)
calc.add_value(20)
print(calc.get_average()) # 15.0
# 可以创建多个独立实例
calc2 = Calculator()
calc2.add_value(5)
print(calc2.get_average()) # 5.0(不影响 calc)# ✅ 正确做法:用闭包封装状态
def create_calculator() -> dict:
"""创建计算器闭包"""
total = 0
count = 0
def add_value(value: int) -> None:
nonlocal total, count
total += value
count += 1
def get_average() -> float:
return total / count if count > 0 else 0.0
def reset() -> None:
nonlocal total, count
total = 0
count = 0
return {
"add": add_value,
"average": get_average,
"reset": reset
}
# 使用
calc = create_calculator()
calc["add"](10)
calc["add"](20)
print(calc["average"]()) # 15.0# ❌ 不必要的 global(只读取)
config = {"debug": True}
def check_config() -> bool:
global config # 不必要!读取不需要 global
return config["debug"]
# 问题:global 增加了代码复杂度,但实际不需要# ✅ 正确做法:读取全局变量不需要 global
config = {"debug": True}
def check_config() -> bool:
return config["debug"] # 直接读取即可
# 只有修改(赋值)时才需要 global
def set_debug(value: bool) -> None:
global config
config["debug"] = value# ❌ global 导致的隐蔽 bug
counter = 0
def increment() -> int:
global counter
counter += 1
return counter
# 问题:如果多次导入这个模块,counter 状态会混乱
# 模块级全局变量在 import 时只初始化一次# ✅ 正确做法:使用类或函数工厂
def create_counter() -> Callable[[], int]:
"""创建计数器"""
count = 0
def increment() -> int:
nonlocal count
count += 1
return count
return increment
# 每次调用 create_counter 都是独立的状态
counter1 = create_counter()
counter2 = create_counter()
print(counter1()) # 1
print(counter1()) # 2
print(counter2()) # 1(独立计数)# ❌ nonlocal 用于非闭包场景
def outer():
x = 10
def inner():
nonlocal x # 如果 outer 不是闭包,用类更清晰
x += 1
return x
return inner
# 问题:复杂嵌套难以理解,不如直接用类# ✅ 正确做法:复杂状态管理用类
class Counter:
"""计数器类"""
def __init__(self, start: int = 0) -> None:
self._count = start
def increment(self) -> int:
self._count += 1
return self._count
def get(self) -> int:
return self._count
# 类比闭包更易理解和扩展
counter = Counter(10)
print(counter.increment()) # 11
print(counter.get()) # 11# ❌ 同名变量混淆
x = "global"
def func():
x = "local" # 创建局部变量,不是修改全局
def inner():
x = "inner" # 又是新的局部变量
print(x)
inner()
print(x)
func()
print(x)
# 输出:inner, local, global
# 三层同名变量,容易混淆# ✅ 正确做法:避免同名,用不同名字
global_status = "global"
def func():
local_status = "local"
def inner():
inner_status = "inner"
print(inner_status)
inner()
print(local_status)
func()
print(global_status)
# 输出:inner, local, global
# 名字不同,清晰明了适用场景
| 场景 | 是否推荐 | 原因 |
|---|---|---|
| 函数内部临时变量 | ✅ 推荐 | 局部作用域,自动管理 |
| 闭包状态保持 | ✅ 推荐 | nonlocal + 闭包 |
| 配置参数传递 | ✅ 推荐 | 函数参数而非全局变量 |
| 单例模式 | ❓ 看情况 | 全局变量可实现,但模块更规范 |
| 跨模块共享状态 | ❌ 不推荐 | 用模块级变量或单例类 |
| 简单计数器 | ❓ 看情况 | 闭包或类,看复杂度 |
| 多线程共享 | ❌ 不推荐 | 全局变量线程不安全 |
global/nonlocal 使用决策
┌──────────────────────────────────────────────────────────────┐
│ global / nonlocal 使用决策 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 需要修改外层变量吗? │
│ ├─ 否 → 直接读取,不需要任何声明 │
│ └─ 是 │
│ │ │
│ ▼ │
│ 变量在哪一层? │
│ ├─ 模块级别 → 用 global │
│ ├─ 外层函数 → 用 nonlocal │
│ │ │
│ ───────────────────────────────────────────────────── │
│ │
│ 优先考虑替代方案: │
│ ├─ 状态复杂 → 用类封装 │
│ ├─ 需要隔离 → 用闭包工厂 │
│ ├─ 配置数据 → 用参数传递 │
│ │
└──────────────────────────────────────────────────────────────┘L3 专家层:底层原理
Python 如何实现 LEGB 规则
LEGB 规则的核心实现依赖于命名空间字典和帧对象的链式查找。
LEGB 底层实现:
┌─────────────────────────────────────────────────────────────┐
│ LEGB 命名空间查找机制 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 每一层作用域对应一个字典(命名空间): │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ L - Local(局部命名空间) │ │
│ │ frame.f_locals │ │
│ │ 函数内部定义的变量 │ │
│ │ 最快查找,数组索引实现 │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ 未找到 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ E - Enclosing(封闭命名空间) │ │
│ │ 闭包的 __closure__ 属性 │ │
│ │ 外层嵌套函数的变量 │ │
│ │ 需要遍历闭包单元格 │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ 未找到 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ G - Global(全局命名空间) │ │
│ │ frame.f_globals │ │
│ │ 模块级别的变量 │ │
│ │ 字典查找,O(1) 平均 │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ 未找到 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ B - Built-in(内置命名空间) │ │
│ │ __builtins__ │ │
│ │ print, len, int 等内置函数 │ │
│ │ 字典查找 │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ 未找到 │
│ NameError! │
│ │
└─────────────────────────────────────────────────────────────┘# 查看 LEGB 各层命名空间
import sys
x = "global" # Global 层
def outer():
y = "enclosing" # Enclosing 层
def inner():
z = "local" # Local 层
frame = sys._getframe()
print(f"Local: {frame.f_locals}") # {'z': 'local'}
print(f"Global: {frame.f_globals.get('x')}") # 'global'
print(f"Built-in: {'print' in frame.f_builtins}") # True
inner()
return inner
outer()闭包的底层实现:closure 属性
闭包通过 __closure__ 属性存储外层变量的引用,这就是为什么闭包可以"记住"外层函数的状态。
闭包内部结构:
┌─────────────────────────────────────────────────────────────┐
│ 闭包的 __closure__ 属性 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 闭包函数有一个特殊的 __closure__ 属性: │
│ │
│ __closure__ 是一个元组,包含多个"单元格"对象 │
│ 每个单元格存储一个被捕获的外层变量 │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Cell 对象结构 │ │
│ │ │ │
│ │ cell.cell_contents → 存储的实际值 │ │
│ │ │ │
│ │ 例如: │ │
│ │ __closure__[0].cell_contents = count 的值 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 为什么是"引用"而不是"值"? │
│ • 多个闭包共享同一个单元格 │
│ • 修改一个闭包的值,其他闭包也能看到 │
│ • 这就是 nonlocal 能修改的原因 │
│ │
└─────────────────────────────────────────────────────────────┘# 查看闭包的 __closure__ 属性
def create_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
def get():
return count
return increment, get
inc, get = create_counter()
# 查看闭包属性
print(f"increment.__closure__: {inc.__closure__}")
print(f"单元格内容: {inc.__closure__[0].cell_contents}") # 0
inc()
print(f"调用后单元格内容: {inc.__closure__[0].cell_contents}") # 1
# 两个闭包共享同一个单元格
print(f"get.__closure__: {get.__closure__}")
print(f"get 单元格内容: {get.__closure__[0].cell_contents}") # 1(同步更新)编译时的变量分类
Python 在编译函数时会分析变量的"绑定"行为,决定它是局部变量还是自由变量。
变量分类规则:
┌─────────────────────────────────────────────────────────────┐
│ Python 编译时的变量分类 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Python 编译函数时会扫描代码,判断每个变量: │
│ │
│ 1. 如果变量在函数内被"绑定"(赋值) │
│ → 标记为局部变量(Local) │
│ → 包括:赋值、函数参数、for 循环变量 │
│ │
│ 2. 如果变量在函数内使用但未被绑定 │
│ → 标记为自由变量(Free) │
│ → 会向上查找(Enclosing → Global → Built-in) │
│ │
│ 3. 如果使用 global/nonlocal 声明 │
│ → 强制改变绑定规则 │
│ → global:绑定到 Global │
│ → nonlocal:绑定到 Enclosing │
│ │
│ ───────────────────────────────────────────────────── │
│ │
│ UnboundLocalError 的原因: │
│ │
│ count = 0 │
│ def increment(): │
│ count = count + 1 ← 这里的 count 被 │
│ 编译器标记为局部变量 │
│ 但赋值右边还未定义 │
│ 所以报 UnboundLocalError │
│ │
└─────────────────────────────────────────────────────────────┘# 演示编译时的变量分析
import dis
# 没有 global 声明
def wrong():
count = count + 1 # UnboundLocalError
# 查看 bytecode
dis.dis(wrong)
# 注意:count 被标记为 STORE_FAST(局部变量)
# 有 global 声明
def correct():
global count
count = count + 1
dis.dis(correct)
# 注意:count 使用 LOAD_GLOBAL 和 STORE_GLOBAL性能考量
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| Local 变量查找 | O(1) | 数组索引,最快 |
| Enclosing 变量查找 | O(n) | 需遍历闭包单元格 |
| Global 变量查找 | O(1) | 字典查找 |
| Built-in 变量查找 | O(1) | 字典查找 |
性能测试:
import timeit
# 局部变量
def local_access():
x = 1
return x
# 全局变量
x_global = 1
def global_access():
return x_global
# 闭包变量
def make_closure():
x = 1
def closure_access():
return x
return closure_access
closure_access = make_closure()
# 测试
local_time = timeit.timeit("local_access()", globals=globals(), number=1000000)
global_time = timeit.timeit("global_access()", globals=globals(), number=1000000)
closure_time = timeit.timeit("closure_access()", globals=globals(), number=1000000)
print(f"局部变量: {local_time:.4f}s")
print(f"全局变量: {global_time:.4f}s")
print(f"闭包变量: {closure_time:.4f}s")
# 结果示例:
# 局部变量: 0.06s ← 最快
# 全局变量: 0.08s ← 稍慢(字典查找)
# 闭包变量: 0.10s ← 最慢(遍历单元格)优化建议:
# ❌ 循环内频繁访问全局变量
config = {"value": 100}
def process():
result = []
for i in range(1000000):
result.append(i + config["value"]) # 每次都查全局字典
return result
# ✅ 将全局变量缓存到局部
def process_optimized():
local_value = config["value"] # 一次查找
result = []
for i in range(1000000):
result.append(i + local_value) # 局部变量访问
return result设计动机
Python 为什么这样设计作用域?
| 设计选择 | 原因 | 替代方案对比 |
|---|---|---|
| LEGB 查找顺序 | 从近到远,符合直觉 | JavaScript 函数作用域更复杂 |
| 读取无需声明 | 减少语法负担,读取无副作用 | Rust 强制所有权声明 |
| 修改需声明 | 防止意外修改,显式更安全 | Perl 默认全局,混乱 |
| nonlocal 关键字 | 明确区分 global 和 enclosing | Python 2 只有 global |
| 闭包单元格共享 | 多闭包协作,状态同步 | JavaScript 独立闭包 |
为什么 Python 2 没有 nonlocal?
# Python 2 只能用迂回方式修改外层变量
def counter():
count = [0] # 用列表包装(可变对象)
def increment():
count[0] += 1 # 修改列表内容,不是赋值
return count[0]
return increment
# Python 3 的 nonlocal 更清晰
def counter():
count = 0
def increment():
nonlocal count # 显式声明
count += 1
return count
return increment知识关联
作用域知识关联图:
┌───────────────┐
│ __closure__ │
│ 闭包单元格 │
└───────────────┘
│
↓
┌─────────────┐ ┌───────────────┐ ┌───────────────┐
│ 帧对象 │────→│ LEGB │────→│ 命名空间 │
│ f_locals │ │ 查找规则 │ │ 字典结构 │
│ f_globals │ └───────────────┘ └───────────────┘
└─────────────┘ │
↓
┌───────────────┐
│ 编译分析 │
│ 变量绑定 │
└───────────────┘
│
↓
┌───────────────┐
│ global/nonlocal│
│ 强制绑定 │
└───────────────┘
│
↓
┌───────────────┐
│ 装饰器 │
│ 函数包装 │
└───────────────┘本章小结
┌─────────────────────────────────────────────────────────────┐
│ 变量作用域 知识要点 │
├─────────────────────────────────────────────────────────────┤
│ │
│ L1 理解层: │
│ ✓ LEGB 规则:Local → Enclosing → Global → Built-in │
│ ✓ 读取全局变量无需声明 │
│ ✓ 修改全局变量用 global │
│ ✓ 修改外层变量用 nonlocal │
│ │
│ L2 实践层: │
│ ✓ 避免滥用 global,用类或闭包代替 │
│ ✓ 优先使用局部变量 │
│ ✓ 避免同名变量,减少混淆 │
│ ✓ 复杂状态管理用类 │
│ │
│ L3 专家层: │
│ ✓ LEGB 通过帧对象和命名空间字典实现 │
│ ✓ 闭包通过 __closure__ 属性存储外层变量 │
│ ✓ 编译时分析变量绑定决定作用域 │
│ ✓ 局部变量访问最快(数组索引) │
│ ✓ UnboundLocalError 是编译分析的结果 │
│ │
└─────────────────────────────────────────────────────────────┘交互演示
运行项目 CLI 查看本章代码的实际执行效果:
cd functions_demo && uv run python -m app # 选 3