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的值。这就是闭包的特性——它记住了外部函数的变量。

要形成一个闭包,需要满足三个条件:

  1. 有一个外部函数(比如outer
  2. 外部函数里定义了变量(比如msg
  3. 外部函数里定义了一个内部函数,并且这个内部函数引用了外部函数的变量
  4. 外部函数把内部函数返回了
关于变量捕获

闭包捕获的变量可以是任何类型。来看个例子:

>>> 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被捕获到了闭包里面。doubletriple是两个不同的闭包实例,各自记住了自己的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

每次调用acctotal都会累加,这个状态就保存在闭包里面。

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变量被闭包捕获了,外部代码无法直接访问和修改,只能通过提供的pushpop等方法操作。这在一定程度上实现了数据封装。

六、不想用闭包的话还有什么选择

闭包写起来简洁,但有时候可读性不够好,尤其是逻辑复杂的时候。用类可以替代闭包,代码结构更清晰。

把上面的计数器用类重写一下:

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代码会有帮助。

回顾一下:

  • 闭包是什么,怎么创建一个闭包
  • 闭包捕获变量的机制和修改方法
  • 闭包的常见应用场景
  • 用类替代闭包的做法
Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐