知识点思维导图
25 个知识节点
参考资料
Python(10) - 装饰器与高阶函数
读完后,你应能完成以下任务:
- 绘制“Python(10) - 装饰器与高阶函数 / 先建立前端锚点”的关键对象与数据流,解释“但"长得像"只是第一层。”,并用源码位置、日志或 Trace 标注证据。
- 为“Python(10) - 装饰器与高阶函数 / 地基:函数是"一等公民"”设计正常与异常输入,验证“关键:greet 不带括号是"函数这个对象本身",greet() 带括号才是"调用它拿返回值"。”,输出首个偏差位置与回归测试结果。
- 实现“Python(10) - 装饰器与高阶函数 / 高阶函数:吃函数 / 吐函数”的最小代码或配置,检验“"高阶函数"就是参数或返回值里有函数的函数。”,输出命令、结果与 Diff,并说明不适用边界。
你在 Angular/TS 里写过
@Component,在 React 里写过withRouter(Comp)这种高阶组件——那你已经会装饰器了,只是换了门语言。本篇解决三个问题:Python 的@xxx到底是什么(先用你眼熟的@Component锚一下)、它背后"包一层函数"的机制怎么运转、以及它和 JS 装饰器哪里不一样(别踩"装饰器在定义时就执行了"这个坑)。
一、先建立前端锚点
第一眼,Python 装饰器和 TS/Angular 的装饰器长得一模一样:
// TS / Angular:@ 开头,贴在类/方法上面
@Component({ selector: 'app-user' })
export class UserComponent {}
但"长得像"只是第一层。装饰器的机制更接近你写过的 React 高阶组件(HOC):
// React HOC:传入一个组件,返回一个被"增强过"的新组件
const EnhancedComp = withLogger(MyComponent)
一句话切入点:装饰器 = 一个"函数进、函数出"的高阶函数 + @ 语法糖。先讲清"函数能当值传"这件事,装饰器就顺理成章了。
| JS / TS | Python | 说明 |
|---|---|---|
const f = () => {} 然后 g(f) |
def f(): ... 然后 g(f) |
函数当值传 |
withLogger(Comp) |
with_logger(func) |
函数进函数出 |
@Component({...}) |
@app.get("/x") |
@ 语法糖 |
TS 装饰器收 (target, key, descriptor) |
装饰器收"被装饰的函数本身" | 入参不同(见第五节) |
二、地基:函数是"一等公民"
装饰器能成立,全靠 Python(和 JS 一样)把函数当成普通值——能赋值、能传参、能从函数里返回。这点你在 JS 里早就习惯了:
function greet(name) { return `hi ${name}` }
const fn = greet // 函数赋值给变量
;[1, 2].map(greet) // 函数当参数传
Python 逐字对应:
关键:greet 不带括号是"函数这个对象本身",greet() 带括号才是"调用它拿返回值"。这一点是后面所有装饰器代码的阅读基础。
2.1 高阶函数:吃函数 / 吐函数
"高阶函数"就是参数或返回值里有函数的函数。返回函数这点最关键,因为装饰器就靠它:
// JS 完全同款闭包
const makeMultiplier = (factor) => (x) => x * factor
const double = makeMultiplier(2)
double(10) // 20
闭包(内层函数记住外层变量)你在 JS 里天天用,Python 的规则一致。装饰器 = 高阶函数 + 闭包 + @ 语法糖,三者你都见过。
三、第一个装饰器:手写一遍就懂
需求:给任意函数加一层"执行前后打日志",但不改函数本身代码。这正是 HOC / 中间件的经典场景。
并排看 JS 里你会怎么手写同样的"包一层":
// 没有 @ 语法糖时,HOC 就是手动包
function logCalls(func) {
return function (...args) { // ...args ≈ Python 的 *args
console.log(`调用 ${func.name}`, args)
const result = func(...args)
console.log(`${func.name} 返回`, result)
return result
}
}
const add = logCalls((a, b) => a + b) // 手动赋值回去
看懂这段,装饰器你就过关了 80%。@log_calls 这行所做的,只是把 add = log_calls(add) 写得更好看。
3.1 args 和 kwargs 一定要讲清
装饰器要能套在"任意函数"上,所以 wrapper 必须接住任意参数再透传。这两个语法对标 JS 的剩余/展开:
| JS | Python | 含义 |
|---|---|---|
function f(...args) |
def f(*args) |
收集所有位置参数成一个序列 |
f(...arr) |
f(*arr) |
把序列展开成多个位置参数 |
| (无直接对应) | def f(**kwargs) |
收集所有 key=value 关键字参数成 dict |
f({...obj}) 近似 |
f(**d) |
把 dict 展开成关键字参数 |
所以 def wrapper(*args, **kwargs): func(*args, **kwargs) 的意思就是:不管你怎么调,我都原样接住、原样转发。
四、带参数的装饰器:再包一层
你见过 @app.get("/users")、@retry(times=3) 这种"装饰器自己还带括号传参"的写法。它比普通装饰器多嵌套一层:最外层先吃配置参数,返回真正的装饰器。
记忆法:带括号的装饰器 = 三层函数(配置层 → 装饰器层 → wrapper 层)。和 React 里 connect(mapState)(Comp) 那种"先配置、再返回 HOC、再包组件"的两步调用一模一样:
// react-redux 的 connect:connect(配置) 先返回一个 HOC,再用它包组件
const EnhancedComp = connect(mapStateToProps)(MyComponent)
// └─ 第一层吃配置 ─┘└─ 第二层吃组件 ─┘
五、边界:和 JS 装饰器哪里不一样
类比建立了直觉,但必须立刻划清差异,否则会用 JS/TS 心智踩坑:
-
执行时机:Python 装饰器在"函数定义时"就运行,不是调用时。 这是最大的坑。
@log_calls那行在模块加载、def add被定义的瞬间就执行了一次(add = log_calls(add)),而不是等你add(2,3)才执行。所以装饰器里写有副作用的代码(比如注册路由、打印)会在 import 阶段就发生。 -
入参不同。 TS 的方法装饰器签名是
(target, propertyKey, descriptor)这套"反射式三件套";Python 装饰器朴素得多——收到的就是被装饰的函数(或类)本身,你自己决定怎么包。没有 descriptor 那层抽象。 -
TS 装饰器曾长期是实验特性、依赖编译;Python 装饰器是语言原生、运行时直接生效。 不用配
experimentalDecorators,不用 Babel 插件,写了就能跑。 -
别忘了
functools.wraps。 JS 里函数name丢了通常无所谓,但 Python 框架(FastAPI/pytest 等)大量靠__name__、签名做反射,漏写@functools.wraps(func)会导致名字变成wrapper、文档丢失,甚至路由/测试发现失败。写装饰器就顺手加上,别问,加就对了。 -
装饰的是方法时,第一个参数是
self不是this。 wrapper 的*args会把self一并接住透传,无需特殊处理,但你得知道args[0]可能就是实例本身。
六、标准库里你天天会遇到的装饰器
不用全自己写,Python 内置和标准库给了一批高频装饰器,先认脸:
| 装饰器 | 作用 | JS / TS 类比 |
|---|---|---|
@property |
方法伪装成只读属性 | get xxx() |
@staticmethod |
静态方法,无 self |
static method() |
@classmethod |
类方法,首参是 cls |
static + 工厂方法 |
@functools.wraps |
保留被包函数的元信息 | 手动拷 fn.name |
@functools.lru_cache |
自动缓存函数结果(记忆化) | 手写 memoize / useMemo 思路 |
@functools.lru_cache 很实用,等于免费给你一个记忆化缓存:
七、一个贴近实战的例子:计时装饰器
把本篇串起来,写一个能套在任意函数上的"执行耗时统计",这是后端排查性能时的常用小工具:
注意它的通用性:因为用了 *args/**kwargs 透传 + @functools.wraps,这个 @timeit 可以原样套到项目里任何函数上,完全不碰被测函数的代码——这正是装饰器(以及它对标的 HOC / 中间件)的核心价值:横切关注点(日志、计时、缓存、鉴权)和业务逻辑分离。
八、总结
- 先建立前端锚点:但"长得像"只是第一层。
- 地基:函数是"一等公民":关键:greet 不带括号是"函数这个对象本身",greet() 带括号才是"调用它拿返回值"。
- 第一个装饰器:手写一遍就懂:这正是 HOC / 中间件的经典场景。
- 带参数的装饰器:再包一层:你见过 @app.get("/users")、@retry(times=3) 这种"装饰器自己还带括号传参"的写法。
- 边界:和 JS 装饰器哪里不一样:执行时机:Python 装饰器在"函数定义时"就运行,不是调用时。 -> 入参不同。 TS 的方法装饰器签名是 (target, propertyKey, descriptor) 这套"反射式三件套";Python 装饰器朴素得多——收到的就是被装饰的函数(或类)本身,你自己决定怎么包。没有 descriptor 那层抽象。 -> TS 装饰器曾长期是实验特性、依赖编译; -> 别忘了 functools.wraps。
- 标准库里你天天会遇到的装饰器:| @classmethod | 类方法,首参是 cls | static + 工厂方法 |
学完自测
选择所有正确答案;提交后逐项核对判断依据。