skip to content
Logo Lostman_Wang的小站

Python函数-闭包拓展

一、闭包中外层变量的内存变化详解

1、核心概念

在闭包中,内层函数捕获外层变量时:

  1. 可变对象(列表/字典等):修改内容时内存地址不变
  2. 不可变对象(整数/字符串等):重新赋值时会创建新对象
  3. 闭包通过__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 值=updated

4、内存变化图示

闭包创建时:
┌─────────────────────┐ ┌────────────┐
│ 外层函数栈帧 │ │ 堆内存 │
│ │ │ │
│ 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=init

7、内存变化总结表

变量类型操作内存地址变化对象数量变化闭包引用更新
可变对象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,内容可变