Python 中的闭包
Python闭包:核心概念与实战应用
闭包在Python中是一个挺有意思的概念。简单来说,就是在一个函数内部再定义一个函数,这个内部函数还能记住外部函数的变量。即使外部函数执行完了,内部函数依然可以访问那些变量。
闭包在函数式编程里用得比较多。在Python中,它有个很重要的用途——写装饰器。搞懂闭包,对理解装饰器会有很大帮助。
本文主要讲这几个内容:
- 闭包是什么,Python底层是怎么实现的
- 实际开发中哪些场景会用到闭包
- 不用闭包的话,还有什么替代方案
一、闭包到底是什么
先看一段代码:
>>> def outer():
... msg = "Hello"
... def inner():
... print(msg)
... return inner
...
>>> func = outer()
>>> func()
Hello
outer()函数里面定义了一个inner()函数,最后把inner返回了。这时候func拿到的就是一个闭包对象。有意思的是,outer()执行完之后,按理说msg这个局部变量应该被销毁了,但调用func()时还能正确打印出msg的值。这就是闭包的特性——它记住了外部函数的变量。
要形成一个闭包,需要满足三个条件:
- 有一个外部函数(比如
outer) - 外部函数里定义了变量(比如
msg) - 外部函数里定义了一个内部函数,并且这个内部函数引用了外部函数的变量
- 外部函数把内部函数返回了
关于变量捕获
闭包捕获的变量可以是任何类型。来看个例子:
>>> def create_multiplier(factor):
... def multiplier(x):
... return x * factor
... return multiplier
...
>>> double = create_multiplier(2)
>>> triple = create_multiplier(3)
>>> double(5)
10
>>> triple(5)
15
这里factor被捕获到了闭包里面。double和triple是两个不同的闭包实例,各自记住了自己的factor值。
修改捕获的变量
如果闭包要修改捕获的变量,需要根据变量类型区分处理:
情况一:变量是不可变类型(数字、字符串、元组等)
需要用nonlocal关键字声明:
>>> def create_counter():
... count = 0
... def counter():
... nonlocal count
... count += 1
... return count
... return counter
...
>>> c1 = create_counter()
>>> c1()
1
>>> c1()
2
>>> c2 = create_counter()
>>> c2()
1
nonlocal告诉Python,这个count不是当前函数的局部变量,要去上一层作用域找。每个闭包实例都有自己的count副本,互不影响。
情况二:变量是可变类型(列表、字典等)
可以直接修改,不需要nonlocal:
>>> def create_collector():
... items = []
... def collector(item):
... items.append(item)
... return items
... return collector
...
>>> collect = create_collector()
>>> collect("a")
['a']
>>> collect("b")
['a', 'b']
items是列表,直接调用append修改内容就行。
二、闭包能干哪些事
1. 做个函数工厂
有时候我们需要批量生产功能相似但参数不同的函数,闭包很适合干这个活。
举个例子,写个计算不同幂次的函数:
>>> def make_power(exponent):
... def power(base):
... return base ** exponent
... return power
...
>>> square = make_power(2)
>>> cube = make_power(3)
>>> square(5)
25
>>> cube(5)
125
make_power就是个工厂,传入指数,返回对应的幂函数。
2. 做个有状态的函数
普通函数每次调用完状态就丢了,闭包可以把状态保留下来。
比如实现一个累计求和的函数:
>>> def create_accumulator():
... total = 0
... def accumulator(value):
... nonlocal total
... total += value
... return total
... return accumulator
...
>>> acc = create_accumulator()
>>> acc(10)
10
>>> acc(20)
30
>>> acc(5)
35
每次调用acc,total都会累加,这个状态就保存在闭包里面。
3. 做回调函数
在GUI编程或者异步编程中,经常需要给按钮绑定一个回调函数,这个回调函数可能需要携带一些额外的数据。闭包正好可以解决这个问题。
下面是个简单的tkinter例子:
import tkinter as tk
win = tk.Tk()
win.title("Demo")
win.geometry("300x200")
label = tk.Label(win, text="", font=("Arial", 14))
label.pack(pady=20)
def make_greeting(text):
def greet():
label.config(text=text)
return greet
btn = tk.Button(
win,
text="Click Me",
command=make_greeting("Hello, World!")
)
btn.pack()
win.mainloop()
make_greeting接收要显示的文字,返回一个无参数的闭包,正好满足command的要求。
三、闭包和装饰器
装饰器本质上就是闭包的一种应用。它接收一个函数,返回一个增强过的函数。
看个例子,写个计时装饰器:
import time
def timer(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} took {end - start:.4f} seconds")
return result
return wrapper
@timer
def slow_function(n):
time.sleep(n)
return n
slow_function(2)
输出类似:
slow_function took 2.0023 seconds
timer就是个闭包,wrapper捕获了func这个变量,在执行前后加了计时逻辑。
四、用闭包做缓存
缓存是提升性能的常用手段。闭包可以用来实现一个简单的缓存装饰器:
def cache(func):
store = {}
def wrapper(n):
if n not in store:
print(f"computing {n}...")
store[n] = func(n)
else:
print(f"using cached result for {n}")
return store[n]
return wrapper
@cache
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)
print(fibonacci(10))
print(fibonacci(10))
运行结果:
computing 10...
55
using cached result for 10
55
第一次计算fibonacci(10)会递归计算所有值并缓存,第二次直接从缓存取,效率大大提升。
五、用闭包做数据封装
Python的类没有真正的私有变量,用闭包可以模拟出私有数据的效果:
def create_stack():
data = [] # 这个变量外部访问不到
def push(item):
data.append(item)
def pop():
if data:
return data.pop()
raise IndexError("pop from empty stack")
def size():
return len(data)
# 返回一个包含方法的字典,也可以返回一个函数对象并绑定方法
return {"push": push, "pop": pop, "size": size}
stack = create_stack()
stack["push"](1)
stack["push"](2)
print(stack["size"]()) # 2
print(stack["pop"]()) # 2
print(stack["size"]()) # 1
# stack["data"] # 报错,访问不到
data变量被闭包捕获了,外部代码无法直接访问和修改,只能通过提供的push、pop等方法操作。这在一定程度上实现了数据封装。
六、不想用闭包的话还有什么选择
闭包写起来简洁,但有时候可读性不够好,尤其是逻辑复杂的时候。用类可以替代闭包,代码结构更清晰。
把上面的计数器用类重写一下:
class Counter:
def __init__(self):
self._count = 0
def __call__(self):
self._count += 1
return self._count
def reset(self):
self._count = 0
c = Counter()
print(c()) # 1
print(c()) # 2
c.reset()
print(c()) # 1
实现了__call__方法后,类的实例就可以像函数一样被调用。相比闭包,类的优点是可以有多个方法(比如reset),属性也更容易查看和调试。
七、总结
闭包的核心就是函数可以记住定义时的环境变量。这个特性在很多场景下都很有用:
- 工厂模式:批量生成配置不同的函数
- 状态保持:跨多次调用保存数据
- 装饰器:在不修改原函数的情况下增强功能
- 缓存:避免重复计算
- 数据封装:模拟私有变量
闭包写起来简洁,但如果逻辑比较复杂,用类来实现可能是更好的选择。理解闭包对写出更优雅的Python代码会有帮助。
回顾一下:
- 闭包是什么,怎么创建一个闭包
- 闭包捕获变量的机制和修改方法
- 闭包的常见应用场景
- 用类替代闭包的做法
更多推荐



所有评论(0)