文章目录
在日常的 Python 开发中,我们经常会谈到装饰器、回调函数、函数式编程等概念,而它们背后都离不开一个核心的特性—— 闭包(Closure)。起初,闭包可能听起来有些抽象,但一旦理解了它的本质,你会发现它是一个非常强大且优雅的工具,能让你的代码更加简洁和高效。
这篇文章是我在工作中对闭包的一些理解和应用总结,希望能帮助你彻底搞懂它。
一、什么是闭包?一句话说清
抛开复杂的定义,我们可以这样理解:
闭包就是一个“记住”了自己创建时所在环境的函数。
即便创建它的外部函数已经执行完毕,闭包依然可以访问和操作那些外部函数中的变量。
要构成一个闭包,必须满足以下三个条件:
- 函数嵌套:存在一个外部函数,内部定义了另一个函数。
- 内部函数引用外部变量:内部函数必须引用了外部函数作用域中的变量(非全局变量)。
- 外部函数返回内部函数:外部函数最终返回的是内部函数对象,而不是调用它。
让我们来看一个最经典的例子:
def outer_func(x):
# x 是外部函数的局部变量,也叫“自由变量”
def inner_func(y):
# inner_func 引用了外部的变量 x
return x + y
# 返回内部函数对象
return inner_func
# 调用外部函数,得到一个闭包
add_5 = outer_func(5)
add_10 = outer_func(10)
# 执行闭包
print(f"add_5(1) 的结果是: {add_5(1)}") # 输出: 6
print(f"add_10(2) 的结果是: {add_10(2)}") # 输出: 12
在这个例子中,add_5
和 add_10
就是闭包。当 outer_func(5)
执行完毕后,变量 x=5
并没有消失,而是被“绑定”到了 add_5
这个函数上。
我们可以通过 __closure__
这个特殊属性来验证,它会告诉你闭包捕获了哪些自由变量:
print(add_5.__closure__)
# (<cell at 0x...: int object at 0x...>,)
print(add_5.__closure__[0].cell_contents)
# 5
二、闭包的作用和使用场景
理解了概念,那么闭包在实际工作中有什么用呢?
1. 数据封装与状态保持(替代简单的类)
闭包提供了一种轻量级的方式来封装数据和行为,避免使用全局变量,同时又不像定义一个完整的类那么“重”。
想象一下,你需要一个计数器:
def make_counter():
count = 0 # 这个状态被封装在闭包中
def counter():
nonlocal count # 声明 count 不是局部变量
count += 1
return count
return counter
counter1 = make_counter()
print(counter1()) # 1
print(counter1()) # 2
counter2 = make_counter() # 创建一个新的、独立状态的计数器
print(counter2()) # 1
count
变量被安全地隐藏在 counter
函数的作用域内,外部无法直接访问,只能通过调用 counter1()
来修改。这实现了一种优雅的状态保持。
2. 实现装饰器
这是闭包最广为人知的应用。Python 中的装饰器本质上就是一个接受函数并返回一个新函数的闭包。
import time
def timer_decorator(func):
# 这是一个典型的闭包结构
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"函数 '{func.__name__}' 执行耗时: {end_time - start_time:.4f} 秒")
return result
return wrapper
@timer_decorator
def some_task(n):
time.sleep(n)
print("任务完成!")
some_task(2)
@timer_decorator
只是 some_task = timer_decorator(some_task)
的语法糖。wrapper
函数就是一个闭包,它“记住”了 func
(也就是原始的 some_task
函数),并在其基础上添加了计时功能。
3. 函数工厂
闭包非常适合用来创建一系列功能相似但配置不同的函数。
def make_multiplier(n):
"""创建一个乘以n的函数"""
def multiplier(x):
return x * n
return multiplier
# 函数工厂 "生产" 出不同的函数
times_3 = make_multiplier(3)
times_5 = make_multiplier(5)
print(times_3(10)) # 30
print(times_5(10)) # 50
三、nonlocal
关键字的重要性
在 make_counter
的例子中,我们用到了 nonlocal
。这是一个关键点。
如果我们在闭包内部尝试修改一个捕获的变量,Python 默认会认为我们正在创建一个新的局部变量。
def make_counter_wrong():
count = 0
def counter():
# 如果没有 nonlocal,下面这行会报错 UnboundLocalError
# 因为 Python 认为你在给一个不存在的局部变量 count 赋值
count += 1
return count
return counter
nonlocal
关键字就是用来告诉 Python:“嘿,我不是要创建新变量,我是要修改外层(但非全局)作用域里的那个 count
变量。”
global
:修改全局作用域的变量。nonlocal
:修改外层非全局作用域的变量。
四、一个常见的“坑”:循环中的闭包
这是一个经典的面试题,也是实际开发中可能遇到的问题。
funcs = []
for i in range(3):
def create_func():
return i * i
funcs.append(create_func)
# 你期望的输出可能是 0, 1, 4
# 但实际输出是...
for f in funcs:
print(f())
# 输出:
# 4
# 4
# 4
为什么会这样?
因为闭包捕获的是变量 i
本身,而不是它在循环中某一刻的值。当循环结束后,i
的值最终变成了 2
。所以,后续调用所有闭包时,它们访问到的 i
都是 2
,结果自然都是 2*2=4
。这被称为**“延迟绑定”或“后期绑定”**。
如何修复?
思路是让闭包在创建时就“固定”住当时的值。最简单的方法是利用函数默认参数:
funcs_fixed = []
for i in range(3):
# 利用默认参数在定义时就绑定 i 的值
def create_func_fixed(val=i):
return val * val
funcs_fixed.append(create_func_fixed)
for f in funcs_fixed:
print(f())
# 输出:
# 0
# 1
# 4
总结
闭包是 Python 中一个强大而基础的概念。它不仅是理解装饰器等高级功能的基石,本身也是编写干净、模块化代码的利器。
- 核心:函数和其创建时环境的结合体。
- 优点:数据封装、状态保持、避免污染全局命名空间。
- 应用:实现装饰器、函数工厂、轻量级状态机。
- 注意:修改捕获变量时使用
nonlocal
,警惕循环中的延迟绑定问题。
掌握了闭包,你对 Python 函数式编程的理解会更上一层楼。