Python函数-闭包拓展
一、闭包中外层变量的内存变化详解
1、核心概念
在闭包中,内层函数捕获外层变量时:
- 可变对象(列表/字典等):修改内容时内存地址不变
- 不可变对象(整数/字符串等):重新赋值时会创建新对象
- 闭包通过
__closure__属性保存捕获的变量
2、示例代码
def outer(): lst = [] # 可变变量 count = 0 # 不可变变量 name = "init" # 不可变变量
def inner(val): nonlocal count # 必需声明才能修改 lst.append(val) # 修改可变对象 count += 1 # 创建新的整数对象 name = "updated" # 创建新的字符串对象(局部变量)
print(f"操作: 添加 {val}") print(f"列表: id={id(lst)} 内容={lst}") print(f"计数: id={id(count)} 值={count}") print(f"名称: id={id(name)} 值={name}\n")
print(f"闭包创建时:") print(f"列表初始: id={id(lst)}") print(f"计数初始: id={id(count)}") print(f"名称初始: id={id(name)}") return inner
# 创建闭包closure = outer()print("\n首次调用:")closure("A")
print("第二次调用:")closure("B")3、执行结果分析
闭包创建时:列表初始: id=0x7f9a5c015b40计数初始: id=0x7f9a6421b5d0名称初始: id=0x7f9a5c0a4b70
首次调用:操作: 添加 A列表: id=0x7f9a5c015b40 内容=['A']计数: id=0x7f9a6421b5f0 值=1名称: id=0x7f9a5c0a4c30 值=updated
第二次调用:操作: 添加 B列表: id=0x7f9a5c015b40 内容=['A', 'B']计数: id=0x7f9a6421b610 值=2名称: id=0x7f9a5c0a4c30 值=updated4、内存变化图示
闭包创建时:┌─────────────────────┐ ┌────────────┐│ 外层函数栈帧 │ │ 堆内存 ││ │ │ ││ lst ──────────────┼─────▶ │ 列表对象 ││ [0x7f9a...] │ │ id: 0x7f9a ││ │ │ content: []││ count: 0 [0x7f9a..]├─────▶ │ 整数对象 0 ││ │ │ ││ name: "init" [0x7f]├─────▶ │ 字符串对象 │└─────────────────────┘ └────────────┘
首次调用 closure("A"):┌─────────────────────┐ ┌────────────┐│ 闭包(__closure__) │ │ 堆内存 ││ │ │ ││ lst ──────────────┼─────▶ │ 列表对象 ││ [0x7f9a...] │ │ id: 0x7f9a ││ │ │ content: ["A"]│ count: 1 [0x7f9a..]├─────▶ │ 整数对象 1 ││ │ │ ││ name: "init" [0x7f]├─────▶ │ 字符串对象 ││ │ │ ││ 局部变量 name ───────┼─────▶ │ "updated" │└─────────────────────┘ └────────────┘
第二次调用 closure("B"):┌─────────────────────┐ ┌────────────┐│ 闭包(__closure__) │ │ 堆内存 ││ │ │ ││ lst ──────────────┼─────▶ │ 列表对象 ││ [0x7f9a...] │ │ id: 0x7f9a ││ │ │ content: ["A","B"]│ count: 2 [0x7f9a..]├─────▶ │ 整数对象 2 ││ │ │ ││ name: "init" [0x7f]├─────▶ │ 字符串对象 ││ │ │ ││ 局部变量 name ───────┼─────▶ │ "updated" │└─────────────────────┘ └────────────┘5、关键变化分析
5.1、可变变量(列表)的内存行为
- 内存地址始终不变(示例中
0x7f9a5c015b40) - 内容修改在原始内存位置进行
- 闭包通过
__closure__[0]持续引用同一对象
5.2、不可变变量(整数)的内存行为
- 每次重新赋值(count += 1)创建新对象
- 内存地址变化(0x7f9a6421b5d0 → 0x7f9a6421b5f0 → 0x7f9a6421b610)
- 闭包通过
__closure__[1]引用最新对象 - 旧整数对象被垃圾回收(如果没有其他引用)
5.3、未声明的不可变变量(name)
- 内层函数中的赋值
name = "updated"创建局部变量 - 外层闭包中的name保持不变
- 每次调用都创建新字符串对象(但Python会重用短字符串)
6、闭包内部结构验证
# 验证闭包捕获的变量print("闭包捕获的变量:")for i, cell in enumerate(closure.__closure__): print(f"cell{i}: id={id(cell.cell_contents)} value={cell.cell_contents}")
# 输出示例:# cell0: id=0x7f9a5c015b40 value=['A', 'B']# cell1: id=0x7f9a6421b610 value=2# cell2: id=0x7f9a5c0a4b70 value=init7、内存变化总结表
| 变量类型 | 操作 | 内存地址变化 | 对象数量变化 | 闭包引用更新 |
|---|---|---|---|---|
| 可变对象 | append() | 不变 | 原对象修改 | 引用不变 |
| 不可变对象 | 重新赋值 | 每次变化 | 创建新对象 | 引用更新 |
| 未声明变量 | 内层赋值 | 每次变化 | 创建新对象 | 不更新闭包 |
8、内存管理注意事项
8.1、内存泄漏风险:闭包会使所有捕获变量保持活跃状态
def create_huge_closure(): data = [0] * 10**6 # 大列表 return lambda: data[0] # 闭包持有整个列表
holder = create_huge_closure() # 即使不再使用,data仍存在8.2、意外共享:多个闭包共享外层变量
def factory(): shared = [] return (lambda x: shared.append(x), lambda: shared)
adder, getter = factory()adder(1)print(getter()) # 输出[1] - 共享状态8.3、解决方案:需要独立状态时,使用参数传递
def safe_factory(): return (lambda x, s=[]: s.append(x), lambda s=[]: s.copy())二、闭包引用
1、定义
- 闭包引用 = 内层函数持续持有外层函数局部变量的 cell 指针,即使外层栈帧已销毁。
2、底层结构(图示)
outer 栈帧(已返回) ├─ x = 10 ──┐ ┌──────────┐ └-----------┼─────────▶│ cell │ ← 地址固定 │ └──────────┘inner 函数对象 ├─ __closure__ -> (cell,) └─ 每次执行 LOAD_DEREF x → 从 cell 取值cell是一个 C-level 对象,内部存 PyObject*。- 变量值改变 ⇒ cell 内容变,但 cell 地址不变,因此闭包永远拿到“同一口袋”。
3、四种引用场景对照表
| 场景 | 代码片段 | 是否形成闭包引用 | 备注 |
|---|---|---|---|
| 只读 | lambda: x | ✅ | 读 cell |
| 修改 | nonlocal x; x += 1 | ✅ | 改 cell |
| 重新绑定 | x = 20(无 nonlocal) | ❌ | 创建同名局部,遮蔽外层 |
| 可变对象 | lst.append(1) | ✅ | cell 指向同一 list,内容可变 |