资讯动态

python中的闭包函数

发布时间:2026/10/9 13:19:40 来源:尧图企业网站定制
前言上一类把「闭包是什么」讲清楚的问题落到代码里往往会卡在一个具体写法上内层函数里想改外层的变量为什么一赋值就报UnboundLocalError两个闭包为什么互相串了状态什么时候该写闭包、什么时候该写类这些是「写闭包函数」时真正会遇到的障碍。一个常见的误解是只要内层函数用到了外层的变量就必须写nonlocal。事实不是——只读不写不需要任何声明nonlocal只在你要给这个名字重新绑定赋值、、for循环变量、with ... as等时才需要。另一个误解是闭包必须依赖某种「黑魔法」保持状态实际上它靠的就是函数对象持有的 cell 引用是语言规范里写明的一等机制。本文按「怎么写」来组织先给出闭包函数的语法骨架再讲nonlocal的判定规则然后用几个可以跳出去直接用的例子计数器、累加器、带缓存的函数、配置工厂把用法串起来最后说明闭包与类各自的适用边界。所有示例以 Python 3 为基准nonlocal自 Python 3.0 起可用无需额外版本说明。Python 2.7 已于 2020 年 1 月 1 日停止维护本文不使用任何 Python 2 专有写法。一、闭包函数的语法骨架闭包函数的标准形态是三层外层函数接收参数并定义局部状态内层函数引用这些状态外层函数返回内层函数不调用它。# 适用于 Python 3.0def outer(参数):局部状态 初始值def inner(自己的参数):return 用到局部状态的表达式return inner # 注意返回函数对象不是 inner()最容易写错的一行是最后那句。写return inner()是把内层函数调用结果返回出去闭包就没了。# 适用于 Python 3.0def make_multiplier(n):def multiply(x):return x * nreturn multiply # 返回函数本身double make_multiplier(2)triple make_multiplier(3)print(double(10), triple(10)) # 20 30double和triple各自持有一份独立的n互不影响。每调用一次make_multiplier就产生一个新的 cell 绑定。二、nonlocal的判定规则Python 判断一个名字是局部还是自由靠的是编译期扫描到的赋值动作与运行时顺序无关。# 适用于 Python 3.0def f():x 1def g():print(x) # 只读合法x 是自由变量g()def h():x 1def g():x 2 # 这里有赋值 → x 变成 g 自己的局部变量print(x) # 打印 2g()print(x) # 打印 1外层的 x 根本没被改def k():x 1def g():x 1 # 既是读又是写 → 判定为局部但读的时候还没绑定print(x) # UnboundLocalErrorg()第三种情况才是报错的那种x 1里x先被读而它已被判定为g的局部变量且尚未赋值于是UnboundLocalError。加nonlocal就解决了# 适用于 Python 3.0def make_counter(start0):count startdef counter(step1):nonlocal countcount stepreturn countreturn counterc make_counter(10)print(c()) # 11print(c(5)) # 16nonlocal的一个硬性规则必须能在某个外层函数作用域里找到同名绑定否则编译期就抛SyntaxError不会拖到运行时。所以下面这段连文件都跑不起来# 适用于 Python 3.0def f():def g():nonlocal missing # SyntaxError: no binding for nonlocal missing foundmissing 1return g另外nonlocal只往上找函数作用域不会找到模块级变量要改模块级变量得用global。两者不能同时声明同一个名字。三、闭包与类怎么选闭包和类都能「保存状态 提供操作」区别在复杂度维度闭包类状态数量一两个变量多个字段方法数量通常一个多个需要继承 / 多态不合适合适需要序列化pickle不可直接序列化通常可以需要被反射、调试时看名字只有外层函数名有类名和方法名代码量少多判断标准可以简化成一句话只有一个行为、状态不超过两三个变量用闭包否则用类。闭包还有一个明确的短板——函数对象默认无法被pickle序列化而模块级具名函数和普通类实例可以。实战一累加器与只能读一次的资源# 适用于 Python 3.0def make_accumulator():total 0def add(x):nonlocal totaltotal xreturn totalreturn addacc make_accumulator()print(acc(1), acc(2), acc(3)) # 1 3 6「只加载一次」是闭包很自然的用法用None哨兵表示尚未初始化。# 适用于 Python 3.0def lazy_reader(path):cache Nonedef read():nonlocal cacheif cache is None:with open(path, encodingutf-8) as fp:cache fp.read()return cachereturn readget_text lazy_reader(notes.txt)# 第一次调用才真正读盘之后命中内存print(len(get_text()))print(len(get_text()))注意nonlocal cache不能省cache fp.read()是赋值动作。另外这里没有处理并发多线程同时首次调用会各读一次要严格只读一次用functools.lru_cache3.2或functools.cache3.9更稳。实战二手写一个缓存装饰器装饰器本质上就是「接收函数、返回闭包」的函数这是闭包最主流的落地场景。# 适用于 Python 3.0from functools import wrapsdef memoize(func):cache {}wraps(func) # 保留原函数的 __name__、__doc__ 等元信息def wrapper(*args, **kwargs):key (args, tuple(sorted(kwargs.items())))if key not in cache:cache[key] func(*args, **kwargs)return cache[key]return wrappermemoizedef fib(n):斐波那契数列朴素递归。return n if n 2 else fib(n - 1) fib(n - 2)print(fib(30)) # 832040print(fib.__name__) # fib没有 wraps 会是 wrapperprint(fib.__doc__) # 斐波那契数列朴素递归。cache是wrapper的自由变量所以它会一直活到fib这个函数对象被回收为止。这也是闭包的一个副作用缓存没有上限长期运行会持续占内存。生产代码里应优先用functools.lru_cache(maxsize128)它自带淘汰策略。常见坑点1. 返回时多写了一对括号❌ 错误写法def make_multiplier(n):def multiply(x):return x * nreturn multiply() # TypeError: multiply() missing 1 required argument: x✅ 正确写法def make_multiplier(n):def multiply(x):return x * nreturn multiply # 返回函数对象2. 循环里创建闭包全都读到最后一个值❌ 错误写法handlers []for i in range(3):handlers.append(lambda: i)print([h() for h in handlers]) # [2, 2, 2]✅ 正确写法默认参数在定义时求值或用工厂函数再包一层handlers []for i in range(3):handlers.append(lambda ii: i)print([h() for h in handlers]) # [0, 1, 2]3. 该写nonlocal却只写了global❌ 错误写法def make_counter():count 0def counter():global count # 指向模块级名字外层那个 count 依然没被改count 1return count # NameError: name count is not definedreturn counter✅ 正确写法def make_counter():count 0def counter():nonlocal countcount 1return countreturn counter4. 以为nonlocal能找到模块级变量❌ 错误写法total 0def f():def g():nonlocal total # SyntaxError外层不是函数作用域total 1return g✅ 正确写法模块级变量用globaltotal 0def f():def g():global totaltotal 1return g5. 闭包持有大对象不释放❌ 错误写法def load():big list(range(10_000_000))return lambda: big[0] # big 被 cell 引用无法回收✅ 正确写法只把需要的那点数据捕获进来def load():big list(range(10_000_000))first big[0]return lambda: first # 只捕获 firstbig 可以被回收6. 用闭包做状态还指望能 pickle❌ 错误写法把make_counter()的返回值直接pickle.dumps会抛AttributeError: Cant pickle local object。✅ 正确写法需要序列化就用类把状态放到实例属性里或者只序列化数据、运行时再重建闭包。7. 装饰器里忘了functools.wraps❌ 错误写法def deco(func):def wrapper(*a, **kw):return func(*a, **kw)return wrapper # 被装饰后 __name__ 变成 wrapper__doc__ 丢失✅ 正确写法from functools import wrapsdef deco(func):wraps(func)def wrapper(*a, **kw):return func(*a, **kw)return wrapper8. 闭包与默认参数混用把可变对象当状态❌ 错误写法def make_adder(state[]): # 默认值在函数定义时创建只此一份def add(x):state.append(x)return statereturn add✅ 正确写法状态在外层函数体内创建每次调用外层都拿到新的一份def make_adder():state []def add(x):state.append(x)return statereturn add总结写法结果内层函数只读外层变量合法变量自动成为自由变量内层函数给外层变量赋值但不声明UnboundLocalError先读后写时加nonlocal绑定到最近的外层函数作用域外层不存在同名绑定编译期SyntaxError用于模块级变量用global不能用nonlocalreturn inner()返回调用结果闭包丢失写闭包函数记住三句话返回值不要带括号只读不用声明、要写必须nonlocal循环里要拿当前值就用默认参数或工厂函数。状态超过两三个变量、或者需要多个方法时直接换成类可读性和可调试性都会明显更好。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑