码农高天
任何复杂装饰器结构均遵循被装饰对象等于装饰器调用的赋值逻辑,明确每层调用的输入输出即可彻底终结死记硬背。
AI-assisted summary of 【python】一个公式解决所有复杂的装饰器,理解了它以后任何装饰器都易如反掌! — original source. Check the original for full context.
Source published:
Key ideas
- “类装饰器”(class decorator)存在歧义,它实际上涵盖了两个正交维度:“可以作为装饰器的类”与“装饰类的装饰器”,两者与普通函数可以自由交叉组合。 (0:00)
- 理解复杂装饰器的核心在于还原语法糖的等价形式“被装饰对象 = 装饰器(被装饰对象)”,通过明确各层调用的输入与输出类型来推导代码结构,避免死记硬背。 (1:00)
- 用类实现带参数装饰器时,类的 __init__ 负责接收配置参数,而 __call__ 方法则作为工厂接收被装饰函数并返回闭包包装函数 wrapper。 (5:00)
- 在 Python 中类本身也是对象,装饰类的装饰器只需接收目标类对象,动态为其绑定或重写魔术方法(如 __str__)后再返回该类即可生效。 (7:30)
Detailed notes
## Background 该视频由技术博主“码农高天”于2023年10月16日发布在YouTube平台上。视频简介指出,本期内容在上一期装饰器讲解的基础上进一步拓展,引入了类作为装饰器以及装饰类的装饰器等多种复杂组合,旨在通过一个通用公式帮助观众轻松解决所有装饰器相关的疑问。
## TL;DR 高天首先澄清了“类装饰器”术语在语境上的歧义,指出装饰器本身的形式与被装饰对象是正交关系,并提炼出核心万能公式“被装饰对象 = 装饰器(被装饰对象)”。随后,他演示了如何利用类的 `__init__` 与 `__call__` 魔术方法实现普通计时器,并通过等价形式推导带参数的类装饰器内部结构改造。最后,视频讲解了如何编写直接装饰类的装饰器(如动态重载 `__str__`),并结合其开源项目 `object-print` 证明了该模式在实际开发与调试中的巨大实用价值。
## Atmosphere & style 视频整体呈现出清晰明快、干货满满且极具逻辑美感的技术拆解氛围。高天以轻松平实的口吻与观众对话,不依赖死记硬背的代码模板,而是手把手带着大家在代码编辑器中从底层原理推导,层层剥离语法糖的迷雾。整个讲解节奏沉稳自信、直击痛点,让原本令人头疼的复杂嵌套概念在公式还原下瞬间变得通透易懂。
## Bulletin - [00:00](https://www.youtube.com/watch?v=g_KBIEpljzY&t=0s) “类装饰器”(class decorator)存在歧义,它实际上涵盖了两个正交维度:“可以作为装饰器的类”与“装饰类的装饰器”,两者与普通函数可以自由交叉组合。 - [01:00](https://www.youtube.com/watch?v=g_KBIEpljzY&t=60s) 理解复杂装饰器的核心在于还原语法糖的等价形式“被装饰对象 = 装饰器(被装饰对象)”,通过明确各层调用的输入与输出类型来推导代码结构,避免死记硬背。 - [05:00](https://www.youtube.com/watch?v=g_KBIEpljzY&t=300s) 用类实现带参数装饰器时,类的 __init__ 负责接收配置参数,而 __call__ 方法则作为工厂接收被装饰函数并返回闭包包装函数 wrapper。 - [07:30](https://www.youtube.com/watch?v=g_KBIEpljzY&t=450s) 在 Python 中类本身也是对象,装饰类的装饰器只需接收目标类对象,动态为其绑定或重写魔术方法(如 __str__)后再返回该类即可生效。
## Key takeaways - [00:00](https://www.youtube.com/watch?v=g_KBIEpljzY&t=0s) 初学者常将装饰器简单定义为“输入是函数、输出也是函数的函数”,但这并不完全准确,无法覆盖更复杂的类相关场景。 - [00:30](https://www.youtube.com/watch?v=g_KBIEpljzY&t=30s) “类装饰器”术语有双重歧义:既可以指作为装饰器本身存在的类,也可以指修饰目标为类的装饰器,两者在概念上正交。 - [01:00](https://www.youtube.com/watch?v=g_KBIEpljzY&t=60s) 所有装饰器语法糖都遵循统一的等价赋值公式:被装饰对象 = 装饰器(被装饰对象)。 - [01:30](https://www.youtube.com/watch?v=g_KBIEpljzY&t=90s) 通过在类中实现 __call__ 魔术方法,其实例对象会变为 callable,能够像普通函数一样接收圆括号调用。 - [02:00](https://www.youtube.com/watch?v=g_KBIEpljzY&t=120s) 不带参数的类作为装饰器时,其 __init__ 接收被装饰函数,__call__ 则负责执行包装逻辑并调用原函数。 - [05:00](https://www.youtube.com/watch?v=g_KBIEpljzY&t=300s) 当类作为装饰器需要接收参数时(例如 @Timer(prefix='...')),其等价形式为 add = Timer(prefix='...')(add)。 - [05:30](https://www.youtube.com/watch?v=g_KBIEpljzY&t=330s) 在带参类装饰器中,__init__ 负责保存配置参数,__call__ 则转为接收原函数并返回内部 wrapper 闭包。 - [07:00](https://www.youtube.com/watch?v=g_KBIEpljzY&t=420s) 中文语境下将“可以作为装饰器的类”表述为“装饰器类”,在概念上比直接使用“类装饰器”更为精确清晰。 - [07:30](https://www.youtube.com/watch?v=g_KBIEpljzY&t=450s) 由于 Python 中的类本质也是对象,因此装饰类的装饰器其本质就是输入一个 class 对象、输出修改后 class 对象的函数。 - [08:30](https://www.youtube.com/watch?v=g_KBIEpljzY&t=510s) 装饰类的典型应用包括动态重载类的魔术方法,例如替换 cls.__str__ 以免去在各个类中重复手动编写字符串序列化逻辑。 - [10:00](https://www.youtube.com/watch?v=g_KBIEpljzY&t=600s) 面对任意复杂的嵌套装饰器,只需写出等价调用赋值链,就能立即推断出各层级所需的输入类型与输出类型。 - [10:20](https://www.youtube.com/watch?v=g_KBIEpljzY&t=620s) 类装饰器在工程中极具实用价值,如开源工具 object-print 即通过装饰类来自动实现类成员属性的友好打印与调试。
## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=g_KBIEpljzY&t=0s) 概念澄清与类作为装饰器的核心机制
在上一期视频中,高天介绍了 Python 装饰器(decorator),并给出了一个简化版的定义:“这个装饰器就是一个输入是函数,输出也是函数的这么一个函数”。高天坦言,这个定义其实并不完全准确,当时只是为了让刚入门装饰器的人能够快速理解核心概念。随后许多观众通过留言或私信的方式,希望他深入讲解“类装饰器”。针对这个术语,高天明确指出“类装饰器”及其英文“class decorator”在理解上存在歧义:在某些语境下,它指的是“可以当作装饰器的类”;而在另一些语境下,它指的是“可以装饰类的装饰器”。这两个其实是正交的概念——一个谈论的是装饰器本身的形式,另一个谈论的是装饰器所修饰的对象。
```mermaid flowchart TD accTitle: 装饰器与被装饰对象的正交组合关系 accDescr: 装饰器形式分为函数与类,被修饰对象同样分为函数与类,彼此可两两正交组合。 subgraph S1["装饰器本身 (Decorator)"] D1["函数 (Function)"] D2["类 (Class)"] end subgraph S2["被装饰的对象 (Decorated Target)"] T1["函数 (Function)"] T2["类 (Class)"] end D1 -->|"可装饰"| T1 D1 -->|"可装饰"| T2 D2 -->|"可装饰"| T1 D2 -->|"可装饰"| T2 ```
为了厘清这些关系,高天强调:“所以你要弄懂的是它背后的核心机制和理念,而不是一个一个的组合去硬背。”许多教程往往要求初学者生搬硬套各种规则,但实际上只需要掌握一条核心的“万能公式”,即装饰器语法糖背后的真正等价形式:
$$\text{被装饰对象} = \text{装饰器}(\text{被装饰对象})$$
高天在代码编辑器中展示了一个名为 `Timer` 的计时器类,类中定义了接收一个 `func` 参数的 `__init__` 初始化方法,并将其保存在 `self.func` 中;同时定义了魔术方法 `__call__`。这个魔术方法的作用就是让该类的所有实例都变成一个 callable(可调用对象),能够像函数一样在后面加上圆括号进行调用。
当在普通的加法函数 `add(a, b)` 上方写上 `@Timer` 时,其语法糖背后的等价形式正是 `add = Timer(add)`。这意味着: 1. `Timer(add)` 实例化了一个 `Timer` 类的对象,并将原来的 `add` 函数作为参数传递给了 `__init__`,保存在 `self.func` 里。 2. 原来的函数名 `add` 现在指向了这个新生成的 `Timer` 对象。 3. 后续执行 `add(2, 3)` 时,本质上是调用了该对象的 `__call__(2, 3)` 方法。在 `__call__` 内部,记录开始时间,通过 `self.func(a, b)` 调用原函数并获取返回值,打印消耗的时间,最后返回计算结果。
打印 `type(add)` 也可以证实其类型就是一个 `Timer` 对象。高天总结道,原先的函数装饰器是“函数调用一个函数返回一个 callable”,而类作为装饰器则是“类调用一个函数返回一个 object,它依然是一个 callable”。
- `Timer.__init__(self, func)`:将原函数保存在 `self.func` 实例变量中。 - `Timer.__call__(self, *args, **kwargs)`:使实例成为 callable,负责包裹计时逻辑与原函数调用。 - 语法糖等价变换:`@Timer` 标注在 `def add():...` 之上,完全等价于 `add = Timer(add)`。
---
[05:00](https://www.youtube.com/watch?v=g_KBIEpljzY&t=300s) 带参数的类作为装饰器与装饰类的装饰器
当装饰器本身需要接收参数时,使用类来实现又该如何推导?高天提出了一个新需求:在打印耗时前输出一个可配置的前缀字符串 `prefix`,调用方式变为 `@Timer(prefix="curr_time")`。初学者面对带参数的类装饰器往往容易犯懵,但只要还原到万能公式的等价形式,整个推导就变得非常清晰:
$$\text{add} = \text{Timer}(\text{prefix}=\text{"curr\_time"})(\text{add})$$
从这个等价形式可以看出,程序首先执行 `Timer(prefix="curr_time")` 建立了一个 `Timer` 类的实例对象,紧接着对该对象进行调用并传入 `add` 函数,最后将调用的返回值重新赋给 `add`。因此,类的内部结构必须做相应改造: 1. `Timer` 类的 `__init__` 方法不再接收函数作为参数,而是接收配置参数 `prefix` 并保存为 `self.prefix`。 2. 实例的 `__call__` 方法在此时扮演了原先外层装饰器工厂的角色,它的入参必须是待装饰的函数 `func`。 3. 在 `__call__` 内部定义一个闭包包装函数 `wrapper`,在这个 `wrapper` 中执行计时、打印 `self.prefix` 以及调用 `func`,并将 `wrapper` 作为 `__call__` 的返回值。 高天演示了修改后的代码,运行后打印的前缀立刻变成了传入的自定义字符串。他指出,这种“可以成为装饰器的类”,在中文语境下称之为“装饰器类”更为贴切。
随后,高天转向了另一侧的正交概念——“真正的装饰类的装饰器”。在 Python 中,打印一个自定义类的实例通常只会输出默认的类名和内存地址等无用信息,除非在类中重载 `__str__` 方法。为了免去在每个类里手动编写的繁琐,可以编写一个装饰器 `add_str` 装饰在目标类 `MyObject` 上:
```python def add_str(cls): def __str__(self): return str(self.__dict__) cls.__str__ = __str__ return cls
@add_str class MyObject: def __init__(self, a, b): self.a = a self.b = b ```
面对类作为装饰目标的写法,高天再次强调使用等价形式来化解迷茫:“在 Python 中定义的 class 本身也是一个 object,那么第七行的 `@add_str` 就相当于第十四行的 `MyObject = add_str(MyObject)`。” - `add_str` 是一个接收类作为参数、同时返回一个类的函数(输入为 `cls`,输出为被修改后的 `cls`)。 - 装饰器在内部将 `cls.__str__` 替换成了返回 `str(self.__dict__)` 的新函数,从而打印出实例中的属性数据(例如 `{'a': 1, 'b': 2}`)。 - 如果装饰类的装饰器本身还需要传递参数,同样只需要利用等价公式外包一层返回函数的函数,其返回的函数签名依然保持“接收类并返回类”即可。
---
[10:00](https://www.youtube.com/watch?v=g_KBIEpljzY&t=600s) 等价形式的威力与实际应用场景
通过上述几个维度的推导,高天指出不论装饰器本身还是被修饰的对象到底是用函数还是用类实现,“只要你可以把它转换成等价形式,它都是比较好读的”。面对任何复杂的组合,只要写出等价形式,就能一眼看破本质:“而转换成等价形式之后,你就知道我需要写一个什么东西,它的输入应该是什么,它的输出应该是什么,就再也不用照着网上教程抄,然后还不知道对不对了。”
```mermaid flowchart LR accTitle: 装饰器编写决策推导流 accDescr: 展示遇到复杂装饰器时,通过写出等价形式来明确输入与输出类型的思考路径。 A["遇见复杂装饰器需求"] --> B["写出语法糖的等价赋值语句"] B --> C{"分析调用链条"} C --> D["确定各层输入参数类型 (函数/类/配置项)"] C --> E["确定各层返回值类型 (callable/object/class)"] D & E --> F["快速写出结构严谨的实现"] ```
针对部分人可能产生的“这么偏门的东西在实际中到底有没有用”的疑问,高天给出了非常肯定的回答:“当然有用了。”他以自己开源编写的实用工具库 `object-print` 为例,其中就包含了一个名为 `add_object_print` 的类装饰器。开发者只需在任何自定义类前加上 `@add_object_print`,就可以在调用 `print()` 时自动输出格式化后的完整类结构以及内部成员变量(例如对象名 `MyObject` 及其成员变量 `a = 1, b = 2`),大大简化了日常调试和 Debug 的成本。最后,高天向提供字幕支持的志愿者杨茜茜表达了感谢,并提醒观众点赞、关注与“一键三连”。
## Notable quotes > [10:00](https://www.youtube.com/watch?v=g_KBIEpljzY&t=600s) 转换成等价形式之后,你就知道我需要写一个什么东西,它的输入应该是什么,它的输出应该是什么,就再也不用照着网上教程抄,然后还不知道对不对了。 — 高天
## Mentioned 高天, Python, object-print, 杨茜茜
## Video description 这期视频在之前一期装饰器的基础之上,又引入了类作为装饰器,和装饰类的装饰器。各种复杂的组合,只需要记住一个公式,就全都解决了!
[Source](https://www.youtube.com/watch?v=g_KBIEpljzY)