1. 认识
闭包(closure)不是一种新的数据类型,而是一种函数关系:一个函数定义在另一个函数内部,并且引用了外层函数的变量。即使外层函数已经执行结束,内部函数仍然可以使用那些变量。
可以把它理解成「函数和它需要的环境一起被保存下来」。下面的 make_greeting() 执行后返回了内部函数 greet(),返回的函数仍然记得当时传入的 language:
01def make_greeting(language):02def greet(name):03return f"{language},{name}!"0405return greet0607say_hello = make_greeting("你好")08say_morning = make_greeting("早上好")0910print(say_hello("小明")) # 你好,小明!11print(say_morning("小红")) # 早上好,小红!
make_greeting() 返回的不是问候语字符串,而是一个函数。每次调用 make_greeting() 都会创建一份独立的外层变量,因此 say_hello 和 say_morning 互不影响。
2. 捕获
内部函数读取外层函数的变量时,就形成了闭包。这个变量也叫「自由变量」:它不是内部函数的参数,也不是内部函数里新定义的局部变量,但内部函数仍然能够访问它。
01def make_multiplier(factor):02def multiply(number):03return number * factor0405return multiply0607double = make_multiplier(2)08triple = make_multiplier(3)0910print(double(5)) # 1011print(triple(5)) # 15
调用 make_multiplier(2) 时,factor 的值被 multiply() 捕获。之后即使 make_multiplier() 已经返回,double() 仍然可以使用 factor = 2。
闭包保存的是变量的绑定关系,而不是把代码复制一份。不同的外层调用会产生不同的绑定关系,这也是工厂函数能够创建多个定制函数的原因。
3. 修改
内部函数可以读取外层变量,但如果要给这个变量重新赋值,需要使用 nonlocal。它表示「修改最近一层外层函数中的变量」:
01def make_counter(start=0):02count = start0304def next_count():05nonlocal count06count += 107return count0809return next_count1011counter = make_counter(10)12print(counter()) # 1113print(counter()) # 1214print(counter()) # 13
如果没有 nonlocal,count += 1 会被当作在 next_count() 内创建局部变量。Python 会先读取这个尚未赋值的局部变量,于是抛出 UnboundLocalError。
nonlocal 只能用于嵌套函数中的外层局部变量,不能用来修改模块级变量。修改全局变量需要 global,但在实际项目中通常更推荐把状态放进对象,或通过参数和返回值显式传递。
4. 用途
闭包常用于「函数工厂」:先配置一次规则,再得到一个可以重复使用的函数。例如,为不同角色创建权限检查器:
01def require_permission(required):02def check(user_permissions):03return required in user_permissions0405return check0607can_edit = require_permission("edit")08can_delete = require_permission("delete")0910print(can_edit({"read", "edit"})) # True11print(can_delete({"read", "edit"})) # False
装饰器也是闭包的常见应用。装饰器通常返回一个内部函数,由它记住原函数,并在调用前后增加日志、鉴权或计时逻辑:
01from functools import wraps0203def log_call(function):04@wraps(function)05def wrapper(*args, **kwargs):06print(f"开始调用:{function.__name__}")07result = function(*args, **kwargs)08print("调用结束")09return result1011return wrapper1213@log_call14def add(left, right):15return left + right1617print(add(2, 3))
这里的 wrapper() 捕获了外层的 function。@log_call 语法会把 add 替换为 log_call(add) 返回的 wrapper,所以调用 add(2, 3) 时会先执行附加逻辑。
在 FastAPI 或 LangChain 项目中,也可以用闭包生成带有固定配置的处理函数,例如为不同模型预先绑定模型名称,再把生成的函数交给路由或工具调用。闭包适合保存少量、相对稳定的配置;配置较多或状态复杂时,使用类通常更清晰。
5. 陷阱
闭包捕获的是变量,而不是创建闭包那一刻的值。循环中创建函数时,所有函数可能会引用同一个循环变量,这称为「晚绑定」:
1functions = []23for number in range(3):4functions.append(lambda: number)56print([function() for function in functions]) # [2, 2, 2]
循环结束后,number 的值是 2,所以三个函数调用时都读取到 2。如果希望每次循环都保存当前值,可以使用默认参数在创建函数时绑定它:
1functions = []23for number in range(3):4functions.append(lambda number=number: number)56print([function() for function in functions]) # [0, 1, 2]
实际项目中,看到「循环里创建回调函数」时要特别注意这个问题。也可以把当前值传给另一个函数工厂,让每次调用都创建一个独立的闭包。
闭包还会延长被捕获对象的生命周期。如果闭包捕获了很大的列表、文件句柄或其他资源,长期保存这个闭包可能造成额外的内存或资源占用,因此需要及时释放不再使用的函数引用。