码农高天
Python装饰器在底层完全等价于将函数对象传参后重新赋值给原变量名,带参数版本也仅是在此基础上增加了一层中间函数调用。
AI-assisted summary of 【python】装饰器超详细教学,用尽毕生所学给你解释清楚,以后再也不迷茫了! — original source. Check the original for full context.
Source published:
Key ideas
- Python 中的函数本质上是普通的函数对象(function object),定义函数仅是新建变量保存可调用的代码对象,因此函数能像普通变量一样作为参数传入或作为返回值返回。 (0:00)
- 装饰器在底层完全等价于语法糖 `func = dec(func)`,其核心是接收一个可调用对象并返回一个被重新赋值给原函数名的新对象。 (5:00)
- 带参数的装饰器结构并未引入新魔法,仅是通过三层嵌套函数增加了一次前置调用,等价逻辑展开为 `func = dec(arg)(func)`。 (10:00)
Detailed notes
## Background 该视频由“码农高天”于 2023 年 9 月 29 日发布在 YouTube 平台上。视频简介表明这是一期针对 Python 中较易令人混淆的核心知识点——装饰器(decorator)的深度教学,旨在通过尽可能覆盖所有细节的讲解,帮助观众彻底消除使用装饰器时的困惑与迷茫。
## TL;DR 视频首先从 Python 底层机制切入,强调“函数即对象”的概念,阐明函数本质上是保存在变量中的可调用对象,因此可以自然地作为参数传递或作为返回值返回。随后,作者通过分析字节码和演示“装饰器直接返回数字 1”的极端反例,证明装饰器在本质上不过是 `double = dec(double)` 这一形式的语法糖,并结合 `*args` 与 `**kwargs` 演示了标准计时包装器的实现。最后,视频拆解了进阶的带参装饰器,指出其核心只是多了一层接收配置参数的函数调用,通过三层嵌套结构即可清晰理清其执行脉络。
## Atmosphere & style 整期视频节奏明快且逻辑极其严密,采用“破除玄学”的硬核技术流风格,通过直接展示底层字节码和刻意编写极端反例,迅速拉近了高阶抽象语法与直观代码逻辑之间的距离。主讲人的语调沉稳笃定且循循善诱,既有刨根问底的技术纯粹感,又保持着对初学者友好的亲和力,给观众带来一种复杂知识点被层层剥开、豁然开朗的透彻与踏实感。
## Bulletin - [00:00](https://www.youtube.com/watch?v=GSdEK-JTKiw&t=0s) Python 中的函数本质上是普通的函数对象(function object),定义函数仅是新建变量保存可调用的代码对象,因此函数能像普通变量一样作为参数传入或作为返回值返回。 - [05:00](https://www.youtube.com/watch?v=GSdEK-JTKiw&t=300s) 装饰器在底层完全等价于语法糖 `func = dec(func)`,其核心是接收一个可调用对象并返回一个被重新赋值给原函数名的新对象。 - [10:00](https://www.youtube.com/watch?v=GSdEK-JTKiw&t=600s) 带参数的装饰器结构并未引入新魔法,仅是通过三层嵌套函数增加了一次前置调用,等价逻辑展开为 `func = dec(arg)(func)`。
## Key takeaways - [00:00](https://www.youtube.com/watch?v=GSdEK-JTKiw&t=0s) 在 Python 中一切皆为对象,函数也是普通对象,在底层通过 `code object` 经由 `MAKE_FUNCTION` 构造成 `function object` 并赋值给变量名。 - [00:45](https://www.youtube.com/watch?v=GSdEK-JTKiw&t=45s) 函数对象具备 callable(可调用)特性,语法上支持在名称后添加小括号 `()` 执行,对非 callable 对象发起调用会在运行时报错。 - [01:30](https://www.youtube.com/watch?v=GSdEK-JTKiw&t=90s) 函数可以作为高阶函数的参数传递,例如将具体算子函数注入到通用计算函数中执行。 - [02:30](https://www.youtube.com/watch?v=GSdEK-JTKiw&t=150s) 函数可以作为另一个函数的返回值被传出,返回出的内部闭包函数依然保留可调用特性。 - [05:00](https://www.youtube.com/watch?v=GSdEK-JTKiw&t=300s) 装饰器的本质就是语法糖,在语法层面上 `@dec` 修饰的函数定义完全等价于 `func = dec(func)`。 - [05:30](https://www.youtube.com/watch?v=GSdEK-JTKiw&t=330s) 字节码层面证实,装饰器先完成目标函数的标准定义与构造,再紧接着加载装饰器并执行 `CALL_FUNCTION`,最后将结果存回原函数变量名。 - [06:00](https://www.youtube.com/watch?v=GSdEK-JTKiw&t=360s) 装饰器甚至可以直接返回非函数对象(如直接返回数字 1),被修饰后的函数名就会变成纯数字,证明其并无特殊黑魔法限制。 - [07:00](https://www.youtube.com/watch?v=GSdEK-JTKiw&t=420s) 常规装饰器通常返回一个内部 `wrapper` 包装函数,在内部执行附加逻辑(如统计耗时)后再执行原函数并返回结果。 - [08:00](https://www.youtube.com/watch?v=GSdEK-JTKiw&t=480s) 在 `wrapper` 内部使用 `*args` 和 `**kwargs` 可以确保装饰器兼容被修饰函数的任意形参列表和签名。 - [10:00](https://www.youtube.com/watch?v=GSdEK-JTKiw&t=600s) 带参数装饰器在语法糖展开时等价于两步调用 `func = timing(1000)(func)`,即先带参调用生成常规装饰器,再修饰目标函数。 - [10:30](https://www.youtube.com/watch?v=GSdEK-JTKiw&t=630s) 带参数装饰器由三层嵌套构成:最外层接收配置参数、中间层接收被装饰函数、最内层接管实际执行逻辑。
## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=GSdEK-JTKiw&t=0s) 理解装饰器的前置知识:Python 中的函数即对象与高阶函数
很多初学 Python 的人对装饰器(Decorator)感到十分迷茫,主要原因是许多其他编程语言并没有原生提供这类语法,而且网络上的教程和示例代码往往非常复杂,让人望而却步。要把装饰器彻底掰开揉碎讲明白,首先必须理清 Python 的基础知识:在 Python 中,所有东西都是对象(object),函数也不例外。
从 Python 字节码的角度来看,定义一个函数并没有任何神秘之处。函数体本身会被编译为一个 `code object`(代码对象),用于保存底层的运行逻辑。随后的字节码会把该 `code object` 与函数名 `double` 加载(LOAD)进来,并通过 `MAKE_FUNCTION` 将其构造成一个 `function object`(函数对象),最后将这个函数对象存储(STORE)到变量名 `double` 中。
“所以说当我们在python中所谓定义一个函数的时候我们只是新建了一个变量然后这个变量里面保存了一个函数对象也就是function object”——码农高天。
函数对象的一个关键特征在于它是“可调用的”(callable)。在 Python 语法层面,callable 意味着名字后面可以跟一对小括号 `()` 来发起调用。只要对象本身不是 callable(例如尝试调用数字 `1()`),虽然能编译通过且会给出 `SyntaxWarning`,但在运行时(runtime)依然会报错。
理解了函数只是普通对象后,自然就能理解函数可以像普通变量一样被传递: 1. **函数作为参数**:定义一个 `calc_number(func, num)`,该函数接收一个函数作为第一个参数、一个数字作为第二个参数,然后在内部执行 `func(num)`。将 `double`(乘 2)和 `triple`(乘 3)作为参数传入并给入数字 3,就会分别打印出 6 和 9。 2. **函数作为返回值**:函数不仅能被传进去,还可以作为返回值被传出来。例如设计一个 `get_multiple_func(n)`,它接收一个参数 `n`,并在内部定义并返回一个内部函数 `multiple(x)`(返回 `n * x`)。当调用 `get_multiple_func(2)` 时,返回的就是倍增函数;传入 3 时,返回的就是三倍函数。返回出来的函数依然是 callable,可以像普通函数一样接收参数执行。
```mermaid flowchart LR accTitle: 函数作为返回值的数据流向 accDescr: 展示外部函数接收参数后返回内部闭包函数并被调用的全流程
subgraph Outer["外部高阶函数"] A["get_multiple_func(n=2)"] --> B["生成并返回 multiple(x)"] end subgraph Execution["外部调用过程"] B --> C["变量 double 获取内部函数"] C --> D["double(3) 触发调用"] D --> E["输出结果: 6"] end ```
---
[05:00](https://www.youtube.com/watch?v=GSdEK-JTKiw&t=300s) 装饰器的本质:语法糖与极端反例
一旦理解了“函数可以作为参数传给另一个函数”以及“函数的返回值也可以是一个函数”,装饰器就变得异常清晰。装饰器本身就是一个普通的 callable,在绝大多数情况下,`@` 后面跟着的名称就是一个函数名。
在函数 `double` 的定义上方添加 `@dec`,查看其底层的字节码可以发现:上半部分依然是正常的函数定义与 `MAKE_FUNCTION`,而下半部分则是装饰器真正生效的逻辑。底层的操作是先加载装饰器函数,紧接着执行 `CALL_FUNCTION`,再把结果 `STORE_NAME` 回到 `double`。
“当我们对一个函数做一个decorator的操作的时候它完全等价于这个变量名等于Decorator调用这个变量名也就是说Decorator不过是一个输入和输出都是函数的函数”——码农高天。
为了消除大家对装饰器的敬畏感,码农高天举了一个极端的反例:如果写一个装饰器函数 `dec(f)`,里面直接 `return 1`,并在 `double` 上标注 `@dec`,最后直接打印该变量 `print(double)`,输出结果就是一个纯粹的数字 `1`。这证明了装饰器在语法层面根本不具有任何神秘性,它完完全全等价于 `double = dec(double)`。
在正常的开发场景中,装饰器通常是接收一个函数作为参数,并返回一个新的包装函数(wrapper)。以性能计时器 `timing` 为例: - `timing(f)` 接收被修饰的函数 `f`。 - 其内部定义了一个 `wrapper(*args, **kwargs)` 函数。在 wrapper 内部先记录起始时间,调用被装饰的原函数 `f(*args, **kwargs)` 并保存返回值,记录结束时间并打印耗时,最后将原函数的返回值返回。 - 借助 `*args` 和 `**kwargs`(可变位置参数和关键字参数),可以让装饰器兼容任意签名、任意参数数量的函数,使 `timing` 可以无缝复用到各种不同的业务函数上。
---
[10:00](https://www.youtube.com/watch?v=GSdEK-JTKiw&t=600s) 进阶:带参数的装饰器实现逻辑
很多人在阅读进阶代码时,常被“带参数的装饰器”绕晕。其实带参数的装饰器并没有任何奇特之处,它只是在等价转换的过程中**多了一次函数调用**。
对比两者在代码展开时的等价形式: - **无参数装饰器**(`@timing`): ```python double = timing(double) ``` - **带参数装饰器**(`@timing(1000)`): ```python double = timing(1000)(double) ```
“原来这个Decorator是参数是函数返回值也是函数现在这个decorator是参数是可以你任意指定的东西然后他要返回一个函数A 这个函数A的参数是函数 返回值也是函数”——码农高天。
带参数装饰器的具体嵌套结构可拆解为三层: 1. **最外层**(如 `timeit(iterations)`):接收用户自定义的配置参数(如执行循环测试的次数 `iterations`)。它的返回值是一个函数,即 `inner`。 2. **中间层**(即 `inner(f)`):它充当了原本没有参数时的那个经典装饰器,接收待装饰的目标函数 `f` 作为参数,返回实际替换原函数的 `wrapper`。 3. **最内层**(即 `wrapper(*args, **kwargs)`):实际接管执行逻辑。在内部循环运行 `iterations` 次函数 `f`,统计总耗时并返回结果。
```mermaid flowchart TD accTitle: 带参数装饰器的三层执行流程 accDescr: 展示带参装饰器从接收配置到返回执行包装器的三层嵌套过程
subgraph Level1["第一层: 接收配置参数"] A["调用 timing(1000)"] --> B["返回第二层函数 inner"] end subgraph Level2["第二层: 接收被修饰函数"] B --> C["inner(double) 接收原函数"] C --> D["返回第三层函数 wrapper"] end subgraph Level3["第三层: 实际执行调用"] D --> E["赋回原函数名: double = wrapper"] E --> F["double(x) 触发 wrapper 内部循环执行 1000 次"] end ```
通过这一层层展开,原本复杂的装饰器逻辑就完全还原成了 Python 基础的函数调用链条。只要牢记核心等价公式与函数对象的本质,面对多层嵌套或带参数的装饰器时便不再会感到困惑。
## Notable quotes > [05:00](https://www.youtube.com/watch?v=GSdEK-JTKiw&t=300s) 当我们对一个函数做一个decorator的操作的时候它完全等价于这个变量名等于Decorator调用这个变量名也就是说Decorator不过是一个输入和输出都是函数的函数 — 码农高天
> [00:00](https://www.youtube.com/watch?v=GSdEK-JTKiw&t=0s) 当我们在python中所谓定义一个函数的时候我们只是新建了一个变量然后这个变量里面保存了一个函数对象也就是function object — 码农高天
## Mentioned Python, 码农高天
## Video description 这期做一个教学向的视频,之前有人想让我讲一下装饰器decorator,这也确实是python里比较让人迷惑的一个知识点。这次有个机会把里面的所有细节都尽可能地触及到,希望大家看完视频就再也不为怎么用装饰器发愁了!
[Source](https://www.youtube.com/watch?v=GSdEK-JTKiw)