Skip to content

03-变量作用域

Python 版本要求:Python 3.11+ 贯穿项目:functions_demo/ 代码位置app/core/scope.py测试验证cd functions_demo && uv run pytest -k TestScope -v

理解变量作用域,避免变量命名冲突和意外的数据修改。


概念铺垫

为什么需要理解作用域?一个真实的变量冲突场景

问题场景: 你在开发一个计数器程序,想要在函数内部修改外部变量。

变量冲突的困惑:

python
count = 0  # 全局变量

def increment():
    count = count + 1  # 期望修改全局变量
    return count

result = increment()  # UnboundLocalError!

问题:

  • 为什么会报错?
  • 如何在函数内修改全局变量?
  • 嵌套函数中的变量如何处理?

理解作用域后的解决方案:

python
count = 0  # 全局变量

def increment() -> int:
    global count  # 声明使用全局变量
    count += 1
    return count

print(increment())  # 1
print(increment())  # 2
print(count)        # 2

这就是作用域的价值:明确变量的可见范围,避免意外冲突


作用域解决了什么问题?

作用域的本质是:定义变量在哪些地方可以被访问

就像不同房间的人:

  • 房间里的人(局部变量):只能在本房间活动
  • 楼层的人(嵌套作用域):能在本楼层活动
  • 整栋楼的人(全局变量):能在整栋楼活动

作用域的优势:

  1. 避免冲突:不同作用域可以有同名变量
  2. 数据隔离:函数内部的数据不会被外部意外修改
  3. 内存管理:局部变量用完即销毁
  4. 代码清晰:明确变量的使用范围

L1 理解层:会用

作用域的最简用法

核心规则

函数内部读取全局变量是允许的,但修改全局变量必须使用 global 关键字声明。这是 Python 作用域最基础的规则。

为什么这样设计?

  • 读取全局变量不会产生副作用,所以直接允许
  • 修改全局变量会影响外部状态,必须显式声明以避免意外修改
  • 不使用 global 时,赋值操作会创建同名局部变量,而非修改全局变量

示例代码

python
# 全局变量
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 四层作用域的实际查找过程:

python
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:"这个变量是全局的,不要创建局部副本"。

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)  # 2

global 使用场景

何时需要 global:

  • 程序级别的配置开关(如调试模式)
  • 全局计数器或统计器
  • 单例模式的实现

何时不需要 global:

  • 只是读取全局变量值
  • 修改可变对象的内容(如列表的 append、字典的键值修改)

注意事项:

  • 过多使用 global 会降低代码的可维护性和可测试性
  • 全局变量状态难以追踪,容易产生副作用
  • 优先考虑使用类属性或闭包来替代全局变量
python
# 配置变量
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:只跨越到外层函数级别
python
# ❌ 不使用 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())  # 3

global vs nonlocal

┌─────────────────────────────────────────────────────────────┐
│              global vs nonlocal                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   global:                                                   │
│   • 引用/修改全局变量                                       │
│   • 作用于模块级别                                          │
│                                                             │
│   nonlocal:                                                 │
│   • 引用/修改外层嵌套函数的变量                             │
│   • 作用于嵌套函数之间                                      │
│   • 不能用于全局变量                                        │
│                                                             │
│   ─────────────────────────────────────────────────────     │
│                                                             │
│   选择指南:                                                 │
│   • 修改全局变量 → global                                   │
│   • 修改外层函数变量 → nonlocal                             │
│   • 只读取变量 → 不需要声明                                 │
│                                                             │
└─────────────────────────────────────────────────────────────┘

渐进复杂:从简单到复杂的作用域

本节从最简单的作用域用法开始,逐步增加复杂度,帮助读者循序渐进地理解作用域机制。

层级 1:只读取全局变量

场景说明: 函数内部只需要访问全局变量的值,不进行任何修改。这是最简单、最安全的作用域用法。

核心要点:

  • 函数内直接使用全局变量名即可读取其值
  • 不需要任何声明关键字
  • 全局变量的值不会被改变
python
message = "Hello"

def show() -> None:
    print(message)  # 读取全局变量

show()  # Hello

层级 2:局部变量覆盖

场景说明: 函数内部创建同名变量,此时局部变量"覆盖"了全局变量名,但全局变量本身并未被修改。

核心要点:

  • 赋值操作默认创建局部变量
  • 同名局部变量优先被使用(LEGB 规则的 L 优先)
  • 函数执行完毕后,局部变量销毁,全局变量保持原值
python
message = "Global"

def show() -> None:
    message = "Local"  # 创建局部变量
    print(message)     # Local

show()
print(message)  # Global(全局变量未改变)

层级 3:使用 global 修改全局变量

场景说明: 当需要在函数内部真正修改全局变量的值时,必须显式声明 global

核心要点:

  • 使用 global 变量名 声明后,赋值操作会修改全局变量而非创建局部变量
  • 多次调用函数会累积修改效果
  • 修改全局变量会影响整个程序状态,需谨慎使用
python
counter = 0

def increment() -> None:
    global counter
    counter += 1

increment()
increment()
print(counter)  # 2

层级 4:嵌套函数中的变量

场景说明: 嵌套函数(函数内定义函数)形成多层作用域,内层函数可以访问外层函数的变量。

核心要点:

  • 内层函数可以读取外层函数的变量(Enclosing 作用域)
  • 外层函数无法访问内层函数的局部变量
  • 嵌套层级越深,作用域链条越长
python
def outer() -> None:
    x = "outer"
    
    def inner() -> None:
        y = "inner"
        print(x)  # 可以访问外层变量
    
    inner()
    # print(y)  # NameError

outer()

层级 5:使用 nonlocal 修改外层变量

场景说明: 在嵌套函数中,如果内层函数需要修改外层函数的变量(不是全局变量),使用 nonlocal 关键字。

核心要点:

  • nonlocal 只作用于 Enclosing 作用域,不涉及全局变量
  • 允许内层函数持久化修改外层函数的状态
  • 常用于闭包(Closure)模式,实现状态保持
python
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_factorset_factor 挂载到 multiply 函数上函数也是对象,可以附加属性,让调用者通过 multiply.set_factor(3) 修改内部状态

实际应用:累加器闭包

应用场景

闭包(Closure)是作用域的高级应用:内层函数"捕获"外层函数的变量,即使外层函数已经执行完毕,内层函数仍然可以访问和修改这些变量。

闭包的价值:

  • 状态保持:不使用全局变量也能保持状态
  • 数据封装:变量隐藏在函数内部,外部无法直接访问
  • 函数工厂:根据参数生成定制化的函数

典型应用:

  • 累加器、计分器
  • 缓存装饰器
  • 配置管理器

示例代码

python
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

工作原理解析:

  • totalcount 是外层函数 make_score_accumulator 的局部变量
  • addaveragereset 三个内层函数通过 nonlocal 访问和修改这些变量
  • make_score_accumulator 返回后,totalcount 仍然存在(被闭包捕获)
  • 三个闭包共享同一组状态,互相协作完成累加、求平均、重置功能

关键代码说明:

代码含义为什么这样写
nonlocal total, count声明修改外层变量不加 nonlocal 赋值时会创建新的局部变量而不是修改外层的 totalcount
return add, average, reset以元组形式返回多个闭包将多个操作封装成函数集合,无需定义类即可实现状态封装
total = initial(reset 内)重置到初始值而非 0使用闭包捕获的 initial 而非硬编码 0,让累加器可从任意起点重置
average 不使用 nonlocal只读取不修改average 仅读取 totalcount,无需 nonlocal 声明

L2 实践层:最佳实践

推荐做法

做法原因示例
避免使用 global全局变量难以追踪,副作用不可控用类属性或闭包代替
优先使用局部变量自动销毁,内存效率高,无命名冲突函数内部定义变量
闭包替代 global状态封装,数据隐藏create_counter() 返回闭包
nonlocal 用于闭包嵌套函数间共享状态的标准方式计数器、累加器
用类替代复杂作用域更清晰的状态管理class Counter:
明确变量来源让代码自解释,减少隐式依赖函数参数传递而非读取全局

反模式:不要这样做

python
# ❌ 滥用 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. 测试困难:每次测试需要重置全局变量
python
# ✅ 正确做法:用类封装状态
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)
python
# ✅ 正确做法:用闭包封装状态
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
python
# ❌ 不必要的 global(只读取)
config = {"debug": True}

def check_config() -> bool:
    global config  # 不必要!读取不需要 global
    return config["debug"]

# 问题:global 增加了代码复杂度,但实际不需要
python
# ✅ 正确做法:读取全局变量不需要 global
config = {"debug": True}

def check_config() -> bool:
    return config["debug"]  # 直接读取即可

# 只有修改(赋值)时才需要 global
def set_debug(value: bool) -> None:
    global config
    config["debug"] = value
python
# ❌ global 导致的隐蔽 bug
counter = 0

def increment() -> int:
    global counter
    counter += 1
    return counter

# 问题:如果多次导入这个模块,counter 状态会混乱
# 模块级全局变量在 import 时只初始化一次
python
# ✅ 正确做法:使用类或函数工厂
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(独立计数)
python
# ❌ nonlocal 用于非闭包场景
def outer():
    x = 10

    def inner():
        nonlocal x  # 如果 outer 不是闭包,用类更清晰
        x += 1
        return x

    return inner

# 问题:复杂嵌套难以理解,不如直接用类
python
# ✅ 正确做法:复杂状态管理用类
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
python
# ❌ 同名变量混淆
x = "global"

def func():
    x = "local"  # 创建局部变量,不是修改全局

    def inner():
        x = "inner"  # 又是新的局部变量
        print(x)

    inner()
    print(x)

func()
print(x)
# 输出:inner, local, global
# 三层同名变量,容易混淆
python
# ✅ 正确做法:避免同名,用不同名字
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!                                │
│                                                             │
└─────────────────────────────────────────────────────────────┘
python
# 查看 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 能修改的原因                            │
│                                                             │
└─────────────────────────────────────────────────────────────┘
python
# 查看闭包的 __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        │
│                                                             │
└─────────────────────────────────────────────────────────────┘
python
# 演示编译时的变量分析
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)字典查找

性能测试:

python
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  ← 最慢(遍历单元格)

优化建议:

python
# ❌ 循环内频繁访问全局变量
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 和 enclosingPython 2 只有 global
闭包单元格共享多闭包协作,状态同步JavaScript 独立闭包

为什么 Python 2 没有 nonlocal?

python
# 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 查看本章代码的实际执行效果:

bash
cd functions_demo && uv run python -m app   # 选 3