Vanson's Eternal Blog

Python中的Closure闭包

Python closure.png
Published on
/12 mins read/---

概述

它允许函数记住并访问其创建时所在的作用域链中的变量,即使该函数在其创建上下文之外执行。

闭包的核心在于,它可以捕获并保存外部函数的局部变量,使其在外部函数执行完毕后仍然可以被访问和使用。

自由变量

在一个函数中,如果使用了在该函数外部定义的变量(但不是全局变量),那么这个变量被称为自由变量。

闭包的核心就是自由变量的捕获和使用。

x = 10
def func():
    print(x)  # x 是自由变量
 

闭包的定义

如果一个嵌套函数引用了外部函数的自由变量,那么这个嵌套函数就是一个闭包。

闭包可以“记住”其创建时所在的作用域中的变量,即使外部函数已经执行完毕。

def outer():
    x = 10
    def inner():
        print(x)  # inner 函数引用了外部函数 outer 的局部变量 x
    return inner
 

闭包形成的条件

  • ‌嵌套函数结构‌:内部函数定义在外部函数体内。
  • ‌变量引用‌:内部函数引用外部函数的局部变量(自由变量)。
  • ‌返回闭包‌:外部函数将内部函数作为返回值或传递给其他作用域。

闭包机制

函数对象的 __closure__ 属性

在 Python 中,闭包函数对象有一个 __closure__ 属性,它是一个元组,包含了闭包捕获的所有自由变量的单元格对象(cell)。

每个单元格对象有一个 cell_contents 属性,用于存储被捕获的变量的值。

def outer():
    x = 10
    def inner():
        print(x)
    return inner
 
closure_func = outer()
print(closure_func.__closure__)  # 输出:(<cell at 0x...: int object at 0x...>,)
print(closure_func.__closure__[0].cell_contents)  # 输出:10
 

自由变量的捕获

当一个嵌套函数引用了外部函数的局部变量时,Python 会自动将这些变量捕获到闭包中。

这些变量的值会被保存在闭包的 __closure__ 属性中,即使外部函数已经执行完毕。

def outer():
    x = 10
    y = 20
    def inner():
        print(x, y)
    return inner
 
closure_func = outer()
closure_func()  # 输出:10 20
 

应用

创建私有变量

闭包可以用来创建私有变量,这些变量只能通过闭包函数访问。

def counter():
    count = 0  # 私有变量
    def increment():
        nonlocal count
        count += 1
        print(count)
    return increment
 
# 创建闭包
my_counter = counter()
my_counter()  # 输出:1
my_counter()  # 输出:2
my_counter()  # 输出:3
 
 
  • count 是一个局部变量,定义在 counter 函数中。
  • increment 函数捕获了 count 变量,并通过 nonlocal 关键字修改它。
  • 每次调用 my_counter() 时,count 的值都会增加,并且这个值被保存在闭包中。

实现函数的“记忆”功能

闭包可以用来实现函数的“记忆”功能,即保存函数调用时的状态。

def adder(x):
    def inner(y):
        return x + y
    return inner
 
# 创建闭包
add_five = adder(5)
print(add_five(3))  # 输出:8
print(add_five(10))  # 输出:15
 
  • adder 函数接收一个参数 x,并返回一个闭包 inner。
  • inner 函数捕获了 x 的值,并将其与传入的参数 y 相加。
  • 每次调用 add_five(y) 时,x 的值始终为 5,而 y 的值根据传入的参数变化。

创建可配置的函数

闭包可以用来创建可配置的函数,这些函数的行为可以根据传入的参数动态调整。

def multiplier(factor):
    def multiply(x):
        return x * factor
    return multiply
 
# 创建闭包
double = multiplier(2)
triple = multiplier(3)
 
print(double(5))  # 输出:10
print(triple(5))  # 输出:15
 
  • multiplier 函数接收一个参数 factor,并返回一个闭包 multiply。
  • multiply 函数捕获了 factor 的值,并将其与传入的参数 x 相乘。
  • 每次调用 double(x) 或 triple(x) 时,factor 的值分别为 2 和 3,而 x 的值根据传入的参数变化。

实现简单的缓存

闭包可以用来实现简单的缓存功能,保存函数的计算结果以避免重复计算。

def memoize(func):
    cache = {}
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return wrapper
 
@memoize
def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)
 
print(fibonacci(10))  # 输出:55
print(fibonacci(10))  # 输出:55(直接从缓存中获取结果)
 
  • memoize 是一个装饰器函数,它接收一个函数 func,并返回一个闭包 wrapper。
  • wrapper 函数使用一个字典 cache 来保存函数的计算结果。
  • 如果某个参数的计算结果已经存在于 cache 中,则直接返回缓存的结果,否则调用 func 并将结果保存到 cache 中。

最佳实践

闭包中的变量是不可变的

如果闭包捕获的变量是不可变类型(如整数、字符串等),则无法在闭包中修改其值。如果需要修改,可以使用 nonlocal 关键字。

def outer():
    x = 10
    def inner():
        nonlocal x
        x += 1
        print(x)
    return inner
 
closure_func = outer()
closure_func()  # 输出:11
closure_func()  # 输出:12
 

闭包中的变量是延迟绑定的

闭包捕获的是变量的引用,而不是变量的值。因此,如果在闭包外部修改了变量的值,闭包中捕获的变量也会相应地改变。

def outer():
    x = [10]
    def inner():
        print(x[0])
    return inner
 
closure_func = outer()
closure_func()  # 输出:10
x = [20]
closure_func()  # 输出:20
 
 
def create_functions():
    funcs = []
    for i in range(3):
        def inner():
            return i
        funcs.append(inner)
    return funcs
 
for f in create_functions():
    print(f())  # 输出: 2, 2, 2
 
 

解决方案‌:通过参数绑定或立即求值

# 修正方案1:默认参数捕获
def create_functions_fixed():
    funcs = []
    for i in range(3):
        def inner(i=i):  # 创建局部变量i
            return i
        funcs.append(inner)
    return funcs
 
# 修正方案2:使用lambda立即绑定
funcs = [lambda i=i: i for i in range(3)]
 
 

框架中的闭包

Django Middleware

它在内部使用闭包来实现中间件(Middleware)机制。中间件可以拦截请求和响应,从而实现诸如身份验证、请求日志记录等功能。

 
def middleware_decorator(get_response):
    def middleware(request):
        # 在视图函数调用之前执行的代码
        print("Middleware: Before view")
        response = get_response(request)
        # 在视图函数调用之后执行的代码
        print("Middleware: After view")
        return response
    return middleware
 
 
  • middleware_decorator 是一个闭包,它接收一个 get_response 函数作为参数。
  • 内部的 middleware 函数捕获了 get_response,并在调用 get_response(request) 之前和之后分别执行了一些代码。
  • 这种机制允许中间件在请求处理的前后插入自定义逻辑。
# django/utils/deprecation.py
class MiddlewareMixin:
    def __init__(self, get_response=None):
        self.get_response = get_response
        super().__init__()
 
    def __call__(self, request):
        response = None
        if hasattr(self, 'process_request'):
            response = self.process_request(request)
        response = response or self.get_response(request)
        if hasattr(self, 'process_response'):
            response = self.process_response(request, response)
        return response
 
 

‌闭包特性‌:__call__方法实现闭包链式调用,保留中间件的处理状态‌

Django 视图装饰器

# django/views/decorators/http.py
def require_http_methods(request_method_list):
    def decorator(func):
        @wraps(func)
        def inner(request, *args, **kwargs):
            if request.method not in request_method_list:
                return HttpResponseNotAllowed(request_method_list)
            return func(request, *args, **kwargs)
        return inner
    return decorator
 
# 使用示例:闭包捕获request_method_list
@require_http_methods(["GET"])
def my_view(request):
    return HttpResponse("OK")
 

闭包作用‌:decorator函数捕获外层request_method_list参数,inner函数保留对func的引用。

Flask 路由

它使用闭包来实现装饰器,从而简化路由的定义。

# flask/app.py
class Flask:
    def route(self, rule, **options):
        def decorator(f):
            endpoint = options.pop('endpoint', None)
            self.add_url_rule(rule, endpoint, f, **options)
            return f
        return decorator
 
# 使用闭包捕获rule参数
@app.route('/users')
def get_users():
    return jsonify(users)
 
 

闭包机制‌:decorator函数捕获外层rule和options参数,实现路由绑定‌。

FastAPI 依赖注入系统

# fastapi/dependencies/utils.py
def Depends(dependency=None, *, use_cache=True):
    def wrapper():
        return dependency
 
    # 闭包捕获dependency参数
    return DependsParamWrapper(
        dependency=wrapper(),
        use_cache=use_cache
    )
 
# 使用示例
async def get_db():
    with Session() as session:
        yield session
 
@app.get("/items")
async def read_items(db: Session = Depends(get_db)):
    return db.query(Item).all()
 
 

‌闭包应用‌:Depends通过闭包保留依赖函数引用,实现请求级上下文管理‌。

asyncio 异步回调

import asyncio
 
def async_callback():
    def callback():
        print("Callback executed")
    return callback
 
async def main():
    loop = asyncio.get_event_loop()
    loop.call_soon(async_callback())
    await asyncio.sleep(1)
 
asyncio.run(main())
 
  • async_callback 是一个闭包,它返回一个回调函数 callback。
  • 在 main 函数中,使用 loop.call_soon 将回调函数注册到事件循环中。
  • 当事件循环运行时,callback 函数被调用,打印一条消息。
  • 这种机制允许在异步操作中插入自定义回调逻辑。

闭包的优缺点

优点

  • 封装性:闭包可以封装外部函数的局部变量,使其对外部不可见,从而实现私有化。
  • 灵活性:闭包可以捕获并保存外部函数的局部变量,使其在外部函数执行完毕后仍然可以被访问和使用,这为动态编程提供了很大的灵活性。
  • 可配置性:闭包可以用来创建可配置的函数,这些函数的行为可以根据传入的参数动态调整。

缺点

  • 复杂性:闭包的实现机制较为复杂,对于不熟悉闭包的开发者来说,可能会难以理解和维护。
  • 性能开销:闭包的使用可能会导致一些性能开销,尤其是在闭包捕获大量数据或频繁调用时。
  • 内存管理:闭包会捕获外部函数的局部变量,这可能会导致内存泄漏。如果闭包捕获了大量数据或长时间不释放,可能会占用过多内存。
← Previous postPython中的Typing模块
Next post →React夯实基础