type
status
date
slug
summary
tags
category
icon
password
📝 嵌套函数(nested function / inner function)
1、定义
在一个 def 语句的内部再写一个 def 语句,里面的函数就叫嵌套函数(inner / nested function),外面的函数叫外层函数(outer / enclosing function)。
2、语法规则速览
嵌套深度不限,可以
f1
里套f2
,f2
里再套 f3
…内层函数名仅在外层函数局部作用域可见;外层无法直接调用 inner()。
内层函数可以被返回、塞进容器、当作参数传递,从而在外层函数返回后仍然存活——这就是闭包。
3、生命周期(LEGB 视角)
3.1、编译阶段
Python 看到
def inner
时,会为 inner
生成一个代码对象 inner.__code__
,并记录:co_freevars
:在内层用到、却定义在外层的自由变量名(free variables)。
co_cellvars
:外层定义、被内层引用的被捕获变量名。
3.2、运行阶段
外层函数被调用 → 创建局部变量 → 遇到
return inner
→ 返回一个函数对象,其 __closure__
属性指向若干 cell 对象,每个 cell 保存一个自由变量的当前值。3.3、外层函数返回后
栈帧销毁,但 cell 仍被内层函数引用 → 变量继续存活 → 闭包诞生。
4、作用域与名字查找(LEGB)
Local → Enclosing → Global → Builtin
- 读外层变量:直接可读。
- 写外层变量:默认会创建同名局部变量,除非显式声明
nonlocal
。
- 写全局变量:需
global
声明。
5、闭包(closure)——嵌套函数 + 自由变量
满足三个条件:1. 函数嵌套;2. 内层引用外层变量;3. 外层返回内层。
底层结构:
📝 函数闭包(function closure)
1、定义
闭包 = 函数对象 + 外层作用域的“现场快照”。
即使外层函数已经执行完毕,闭包仍然能访问那些被捕获的变量。
2、产生的三个硬条件(面试常问)
- 必须有函数嵌套;
- 内层函数引用了外层非全局变量(free variable);
- 外层函数把内层函数返回(或作为参数、塞进容器)。
3、底层原理
场景 | 结果 |
可变对象内容修改 | cell 仍指向同一对象,内容可变 |
重新赋值(无 nonlocal ) | 创建新的局部变量,不再走 cell |
循环变量陷阱 | 所有 lambda 共享同一 cell,需用默认参数冻结 |
3.1、编译阶段
inner.__code__.co_freevars
→ ('x', 'y')
声明需要外层的哪些变量。outer.__code__.co_cellvars
→ ('x', 'y')
声明要提供给内层做闭包。3.2、运行阶段
每产生一个自由变量,就创建一个 cell 对象,位于
frame.f_localsplus
。外层返回后,栈帧销毁,但 cell 仍被
inner.__closure__
引用,值继续存活。3.3、查看现场
4、常见陷阱与对策
4.1、循环变量闭包陷阱
原因:所有回调捕获同一个 cell,循环结束后变量定格在最终值。
修复:用默认参数把值“冻”在定义时
4.2、延迟绑定(late binding)
类似 4.1,常见于 GUI、回调注册。
解决:再加一层闭包或用
functools.partial
。4.3、nonlocal 忘记写
在内层赋值会默认创建局部变量,导致
UnboundLocalError
。5、实用模式
5.1、函数工厂(配置固化)
5.2、私有状态(轻量级对象)
5.3、装饰器骨架(高阶 + 闭包)
📝 函数装饰器(function decorator)
1、定义
“用一个可调用对象(函数或类)去 包装(wrap) 另一个可调用对象,从而 无侵入地增强其行为”的设计模式。
2、语法糖与手工展开
执行时机:模块导入时立即执行一次,返回的新函数被绑定到原名字。
装饰器本身可以是:函数、类、lambda、partial 对象,只要最终返回一个 callable。
3、无参装饰器模板(最常用)
关键点
嵌套函数形成闭包,保存原函数引用。
@wraps
复制 __name__/__doc__/__annotations__
,否则调试信息会丢失。4、带参装饰器(三层嵌套)
执行顺序:
repeat(3)
→ 返回 装饰器 → @decorator
再装饰 hello
。4.1、先忘掉 @
,只看纯函数调用
目标:写一个装饰器,它自己先接收参数,再去装饰真正的函数。
注意最后的调用链:
repeat(3)
先执行一次,返回一个新的“真正的装饰器”,再把这个装饰器应用到 hello
。4.2、把三层拆开起名,便于跟踪
第 1 层:
repeat(n)
只是一个普通函数,它返回下一层函数 decorator
。第 2 层:
decorator(func)
才是经典意义上的“装饰器”,它返回 wrapper
。第 3 层:
wrapper(*args, **kw)
最终替代原函数被调用。4.3、逐帧执行流程(按时间线)
- 模块导入时
@repeat(3)
立即把decorator
应用到hello
- 之后每次
hello()
实际执行
4.4、变量作用域示意
名称 | 定义位置 | 被谁引用 | 生命周期 |
n | repeat 局部 | decorator & wrapper 闭包 | 直到 wrapper 被回收 |
func | decorator 局部 | wrapper | 同上 |
*args, **kw | wrapper 形参 | 无 | 每次调用新建 |
4.5、如果只有两层会怎样?
- 错误示范:
- 使用:
缺少第 1 层 ⇒ 无法先“喂”参数再装饰,因此必须拆成三层。
5、叠加装饰器(顺序从近到远)
6、类装饰器(可维护状态)
优点:可保存状态(如计数、缓存);缺点:需要实现
__call__
。7、典型实战套路
7.1、计时器
7.2、重试(backoff)
7.3、缓存(LRU)
7.4、Flask 路由
8、常见陷阱与调试技巧
坑 | 描述 | 解决 |
元数据丢失 | wrapper.__name__ 变成 'wrapper' | 用 @functools.wraps(func) |
执行时机 | 装饰器在 import 时执行,可能过早 | 用工厂函数或 if __name__ == '__main__': 控制 |
位置参数/关键字参数维护 | 忘记 *args, **kw 导致签名不匹配 | 用 inspect.signature 检查 |
叠加顺序 | 先 @log 再 @cache 会每次打印 | 按需调换顺序 |