闭包原理
一、一句话定义
闭包(Closure):一个函数 + 它记住的外部环境变量,打包在一起。即使外层函数已经返回,内层函数依然能访问那些变量。
生活类比:你去常去的面馆,老板(outer)记住了你的口味(flavor="不要香菜")。某天老板回老家了(外层函数返回),但你再去,新来的徒弟(inner)居然也知道”你不要香菜”——因为那张口味单(闭包的环境)被留下来了。
二、闭包的三个必要条件
-
有一个嵌套函数(函数里定义函数)。
-
内层函数引用了外层函数的变量。
-
外层函数返回内层函数(不调用,只返回)。
def make_greeting(greeting): # 外层
def greet(name): # 内层
return f"{greeting}, {name}!" # 引用了外层的 greeting
return greet # 返回内层函数对象(不调用)
hello = make_greeting("Hello") # 外层已返回,但 greeting="Hello" 被记住
howdy = make_greeting("Howdy")
print(hello("Alice")) # Hello, Alice!
print(howdy("Bob")) # Howdy, Bob!
关键点:
make_greeting("Hello")执行完返回greet,理论上greeting是局部变量该被销毁——但greet还引用它,所以 Python 把greeting绑定进闭包一起保留(成为自由变量 free variable)。
graph LR subgraph SGts4m9["闭包 = 函数 + 环境"] F[greet 函数代码] ENV[环境: greeting=Hello] F --- ENV end hello[hello = 闭包实例1] --> 闭包 note["每个 make_greeting 调用生成一个独立闭包<br/>各自记住自己的 greeting"]
三、闭包的内存模型(加分)
-
闭包里的”被记住的变量”存在函数的
__closure__属性里。 -
用
cell_contents可以查看:
def counter():
n = 0
def inc():
nonlocal n
n += 1
return n
return inc
c = counter()
print(c()) # 1
print(c()) # 2
print(c.__closure__[0].cell_contents) # 2
n没被销毁,而是住在c.__closure__这个”小仓库”里。这就是闭包能保持状态的原因——比用全局变量优雅多了。
四、闭包的经典用途
| 用途 | 说明 |
|---|---|
| 工厂函数 | 批量生成带预设参数的函数(上例 make_greeting) |
| 装饰器 | 装饰器本质就是闭包(见 07-装饰器本质与手写) |
| 惰性求值 / 回调 | GUI、异步里常把上下文”封”进回调 |
| 状态保持 | 不用类也能保存状态(轻量对象) |
# 用闭包实现"带幂次的幂函器"
def power_factory(p):
def power(x):
return x ** p
return power
square = power_factory(2)
cube = power_factory(3)
print(square(5), cube(5)) # 25 125
五、一句话讲清
“闭包是函数加上它捕获的外部变量。条件是嵌套函数、内层引用外层变量、外层返回内层。它让函数’记住’了定义时的环境,常用于装饰器和工厂函数。内存上这些变量存在 __closure__ 里不会被回收。“
1. 函数定义与抽取
1.1 为什么需要函数
两个场景对比——没有函数 vs 有函数:
# ❌ 没有函数——代码重复
a1, b1 = 3, 5
result1 = a1 + b1
print(f"{a1}+{b1}={result1}")
a2, b2 = 10, 20
result2 = a2 + b2
print(f"{a2}+{b2}={result2}")
a3, b3 = 7, 8
result3 = a3 + b3
print(f"{a3}+{b3}={result3}")
# ✅ 使用函数——代码复用
def add_and_print(a, b):
"""计算并打印两个数的和"""
result = a + b
print(f"{a}+{b}={result}")
add_and_print(3, 5)
add_and_print(10, 20)
add_and_print(7, 8)
# 优势:改一处就全部生效,代码行数更少
1.2 函数的四大好处
| 好处 | 说明 |
|---|---|
| 代码复用 | 同样的逻辑只写一次,到处调用 |
| 模块化 | 大问题拆成小函数,每个函数只做一件事 |
| 可测试 | 函数可以独立测试,更容易发现 Bug |
| 可读性 | 好的函数名本身就是注释 |
1.3 函数文档字符串
def calculate_bmi(weight, height):
"""
计算 BMI 指数
参数:
weight: 体重(千克)
height: 身高(米)
返回:
BMI 值(浮点数)
示例:
>>> calculate_bmi(70, 1.75)
22.86
"""
return weight / (height ** 2)
# 查看文档
print(calculate_bmi.__doc__)
help(calculate_bmi)
2. 形参和实参
def greet(name, greeting="Hello"):
# name 和 greeting 是形参(形式参数)——函数定义时的占位符
return f"{greeting}, {name}!"
# "World" 和 "Hi" 是实参(实际参数)——函数调用时传入的实际值
print(greet("World", "Hi")) # "Hi, World!"
关键理解:
-
形参 = 定义时的占位变量
-
实参 = 调用时传入的真实数据
-
调用时实参的值被赋给形参
3. 函数执行内存分析
def swap(a, b):
print(f"swap 内部(交换前):a={a}, b={b}")
temp = a
a = b
b = temp
print(f"swap 内部(交换后):a={a}, b={b}")
x, y = 10, 20
print(f"调用前:x={x}, y={y}")
swap(x, y)
print(f"调用后:x={x}, y={y}")
# 输出:x 和 y 没有变!因为函数内的 a、b 是局部变量
函数调用过程:
-
调用
swap(x, y)→ 创建局部变量a = 10,b = 20 -
函数内部交换 a 和 b →
a = 20,b = 10 -
函数结束 → a 和 b 被销毁
-
x 和 y 从未被修改 → 仍然是 10 和 20
4. 传递不可变对象与可变对象 ⭐⭐
这是 Python 函数参数传递中最核心的概念:
4.1 不可变对象(int, str, tuple, float, bool)
def modify(n, s):
n = 100 # n 指向新对象 100,原参数不受影响
s = "changed" # s 指向新对象,原参数不受影响
num = 10
text = "original"
modify(num, text)
print(num) # 10(不变!)
print(text) # "original"(不变!)
4.2 可变对象(list, dict, set)
def append_item(lst):
lst.append(4) # ⚠️ 修改了 lst 指向的对象本身!
nums = [1, 2, 3]
append_item(nums)
print(nums) # [1, 2, 3, 4](被修改了!)
def reassign(lst):
lst = [4, 5, 6] # 重新赋值:让局部变量 lst 指向新列表
nums2 = [1, 2, 3]
reassign(nums2)
print(nums2) # [1, 2, 3](不变!因为只是改了局部变量的指向)
核心区别
obj.method()修改对象本身 → 影响外部
param = new_value改变变量指向 → 不影响外部
5. 参数传递形式
5.1 位置参数
def register(name, age, city):
print(f"{name}, {age}岁, 来自{city}")
register("张三", 20, "北京") # 按位置一一对应
# register(20, "张三", "北京") # ❌ 顺序错了,语义全乱
5.2 关键字参数
register(age=20, city="北京", name="张三") # 指定参数名,顺序无所谓
register("张三", city="北京", age=20) # 混合使用(位置在前)
5.3 默认参数
def register(name, age, city="北京"):
print(f"{name}, {age}岁, 来自{city}")
register("张三", 20) # 使用默认值:city="北京"
register("李四", 25, "上海") # 覆盖默认值
# ⚠️ 默认参数只初始化一次!可变对象是陷阱
def add_item(item, lst=[]): # ❌ 危险!
lst.append(item)
return lst
print(add_item(1)) # [1]
print(add_item(2)) # [1, 2](使用了同一个列表!)
print(add_item(3)) # [1, 2, 3](共享同一个默认列表)
# 正确做法
def add_item(item, lst=None):
if lst is None:
lst = [] # 每次调用时创建新列表
lst.append(item)
return lst
5.4 可变参数 *args
def sum_all(*args):
"""求和任意数量的参数"""
print(type(args)) # <class 'tuple'>
print(args) # (1, 2, 3, 4, 5)
return sum(args)
print(sum_all(1, 2)) # 3
print(sum_all(1, 2, 3, 4, 5)) # 15
# *args 把多余的**位置参数**打包为元组
5.5 关键字参数 **kwargs
def print_info(**kwargs):
"""打印任意数量的关键字参数"""
print(type(kwargs)) # <class 'dict'>
for key, value in kwargs.items():
print(f"{key}: {value}")
print_info(name="张三", age=20, city="北京")
# **kwargs 把多余的**关键字参数**打包为字典
5.6 组合使用
# 参数顺序:位置 → *args → 关键字 → **kwargs
def func(a, b, *args, c=10, **kwargs):
print(f"a={a}, b={b}")
print(f"*args={args}")
print(f"c={c}")
print(f"**kwargs={kwargs}")
func(1, 2, 3, 4, 5, c=20, x=100, y=200)
# **kwargs={'x': 100, 'y': 200}
5.7 解包传参
# * 解包列表/元组为位置参数
nums = [1, 2, 3]
print(*nums) # 等价于 print(1, 2, 3)
def add(a, b, c):
return a + b + c
print(add(*[1, 2, 3])) # 6
# ** 解包字典为关键字参数
info = {"name": "张三", "age": 20, "city": "北京"}
register(**info) # 等价于 register(name="张三", age=20, city="北京")
6. 浅拷贝与深拷贝 ⭐⭐
import copy
# 原始数据
original = [1, 2, [10, 20]]
print(f"原始:{original}")
# === 赋值(不是拷贝!)===
ref = original # 只是两个变量指向同一个对象
ref[0] = 999
print(f"赋值后原始:{original}") # [999, 2, [10, 20]] ← 变了!
# === 浅拷贝 ===
original = [1, 2, [10, 20]]
shallow = copy.copy(original)
shallow[0] = 100 # 修改第一层 → 不影响原始
shallow[2][0] = 999 # 修改嵌套层 → 影响原始!
print(f"浅拷贝后原始:{original}") # [1, 2, [999, 20]]
# === 深拷贝 ===
original = [1, 2, [10, 20]]
deep = copy.deepcopy(original)
deep[0] = 100 # 不影响原始
deep[2][0] = 999 # 也不影响原始!
print(f"深拷贝后原始:{original}") # [1, 2, [10, 20]] ← 完全不变
graph TB subgraph 赋值 A1[original] --> OBJ1[[1,2,内层列表]] A2[ref=original] --> OBJ1 end subgraph 浅拷贝 B1[original] --> OBJ2a[[1,2,→]] B2[shallow] --> OBJ2b[[1,2,→]] OBJ2a --> INNER[[10,20]] OBJ2b --> INNER end subgraph 深拷贝 C1[original] --> OBJ3a[[1,2,→]] C2[deep] --> OBJ3b[[1,2,→]] OBJ3a --> INNER1[[10,20]] OBJ3b --> INNER2[[10,20新]] end
选择指南
- 只有不可变数据(int, str 等) → 赋值就够了
- 只有一层嵌套的 list/dict → 浅拷贝
- 有多层嵌套的 list/dict → 深拷贝(或用 copy.deepcopy)
7. 方法的返回值
# 单个返回值
def square(x):
return x ** 2
# 多个返回值(实际返回一个元组)
def statistics(nums):
return min(nums), max(nums), sum(nums)/len(nums)
lo, hi, avg = statistics([1, 2, 3, 4, 5])
print(f"最小:{lo}, 最大:{hi}, 平均:{avg}")
# 没有 return → 返回 None
def no_return():
print("我什么都不返回")
# 隐式 return None
result = no_return()
print(result) # None
速记卡(面试闪卡)
Q1:一句话讲清「闭包(Closure)—— 函数”携带”的记忆」到底是什么?
A:闭包是函数+它记住的外部变量打包在一起,外层返回后内层仍能访问那些变量。
Q2:一、一句话定义与生活类比 —— 怎么理解?
A:闭包=一个函数+它记住的外部环境变量,打包在一起,即使外层已返回内层仍能访问。类比常去面馆:老板(outer)记住你口味(flavor=“不要香菜”),老板回老家(外层返回)后新徒弟(inner)也知道——因为口味单(闭包环境)被留下来了。
Q3:二、三个必要条件 —— 怎么理解?
A:闭包三件套:① 嵌套函数(函数里定义函数);② 内层引用外层变量;③ 外层返回内层(不调用只返回)。如 make_greeting(“Hello”) 返回 greet,greeting=“Hello” 被记住,hello(“Alice”) 仍能用。少任一件都不是闭包。
Q4:三、内存模型:为什么外层返回变量还没销毁 —— 怎么理解?
A:理论上 greeting 是局部变量该销毁,但 greet 还引用它,Python 把它绑进闭包成”自由变量(free variable)“。被记住的变量存在函数的 closure 属性里,用 cell_contents 查看。每个 make_greeting 调用生成独立闭包——这就是闭包”保持状态”的小仓库,比全局变量优雅。
Q5:四、经典用途与和类的取舍 —— 怎么理解?
A:用途:① 工厂函数批量生成带参函数;② 装饰器(本质就是闭包);③ 惰性求值/回调(把上下文封进回调);④ 状态保持(不用类也能存状态)。取舍:闭包更轻量适合简单一次性状态(计数器),类适合多方法复杂状态需继承。答出 closure 是加分项。
Q6:核心速记主线有哪些?
-
闭包=函数+环境,外层返回后内层仍能访问变量
-
三条件:嵌套函数/内层引用外层/外层返回内层
-
自由变量存 closure,不被回收,故能保持状态
-
用途:工厂函数、装饰器、回调、状态保持
-
与类取舍:闭包轻量 vs 类适合复杂状态
口诀
A:闭包函数带环境,外层返回记心间
三件套:嵌套引用返内不一般
自由变量住 closure,比全局优雅又平安
工厂装饰器都靠它,记住状态不靠类盘