码农高天
Python在类定义阶段会将类体作为局部作用域运行,提前使用@staticmethod语法糖会将其包装为不可直接调用的描述符,因此必须在类定义末尾显式执行函数赋值才能兼顾类内装饰与外部调用。
AI-assisted summary of 【python】如何在class内部定义一个装饰器?这里的坑你要么不知道,要么不会填! — original source. Check the original for full context.
Source published:
Key ideas
- 将通用装饰器封装到类中有助于代码组织,但若定义为普通实例方法或类方法,会导致外部使用时必须多余实例化或在签名中残留无用的 self / cls 参数。 (0:00)
- 使用 @staticmethod 封装装饰器仅适用于类外调用;若尝试在同一个类体内部直接用它修饰同类方法,会因描述符包装导致类加载期调用报错。 (5:00)
- 终极解决方案是利用 @staticmethod 的等价赋值语法:在类体顶部裸写纯函数供内部修饰,在类体末尾追加 `func = staticmethod(func)` 导出为静态方法,兼顾类内外双向兼容。 (7:28)
Detailed notes
## Background 该视频由 YouTube 频道“码农高天”于 2024 年 4 月 17 日发布。视频主要探讨如何在 Python 类的内部定义和使用装饰器,并深入剖析背后诸多与封装和命名空间相关的非直观知识与避坑指南。
## TL;DR 视频从一个简单的日志装饰器切入,逐步演示了将其封装进类中的演进过程:从笨重的普通实例方法到残留无用参数的 `@classmethod`,再到适用于纯外部调用的 `@staticmethod`。随后,高天重点剖析了“用类内装饰器修饰同类方法”时直接加静态方法会报错的深坑,解释了类定义体在加载期作为局部作用域执行的底层原理。最终,视频给出了兼顾类内直接修饰与类外静态调用的终极方案——在类体末尾使用 `log_function = staticmethod(log_function)` 等价语法进行后置转换。
## Atmosphere & style 视频展现出一种硬核、严谨而又抽丝剥茧的技术极客风格。高天循序渐进地抛出反直觉的报错场景与代码坏味道,再带领观众深入 Python 解释器的执行模型探寻本质原因。观看体验如同解开一层层精巧的底层机制谜题,从最初对语法特性的困惑自然过渡到豁然开朗的透彻领悟。
## Bulletin - [00:00](https://www.youtube.com/watch?v=srSA-aiHf8Y&t=0s) 将通用装饰器封装到类中有助于代码组织,但若定义为普通实例方法或类方法,会导致外部使用时必须多余实例化或在签名中残留无用的 self / cls 参数。 - [05:00](https://www.youtube.com/watch?v=srSA-aiHf8Y&t=300s) 使用 @staticmethod 封装装饰器仅适用于类外调用;若尝试在同一个类体内部直接用它修饰同类方法,会因描述符包装导致类加载期调用报错。 - [07:28](https://www.youtube.com/watch?v=srSA-aiHf8Y&t=448s) 终极解决方案是利用 @staticmethod 的等价赋值语法:在类体顶部裸写纯函数供内部修饰,在类体末尾追加 `func = staticmethod(func)` 导出为静态方法,兼顾类内外双向兼容。
## Key takeaways - [00:00](https://www.youtube.com/watch?v=srSA-aiHf8Y&t=0s) 随着项目膨胀,将功能相关或高度内聚的装饰器归拢到独立的工具类中是常见的工程设计需求。 - [00:45](https://www.youtube.com/watch?v=srSA-aiHf8Y&t=45s) 将装饰器定义为常规实例方法(带 self 参数)会导致调用链冗余,迫使外部必须先实例化对象(如 @d.log_function)才能使用。 - [01:30](https://www.youtube.com/watch?v=srSA-aiHf8Y&t=90s) 若装饰器本身并不依赖类或实例的内部状态,在方法签名中保留 self 参数属于无意义的接口污染。 - [02:15](https://www.youtube.com/watch?v=srSA-aiHf8Y&t=135s) 改用 @classmethod 可以免除实例化步骤直接通过类名调用,但函数签名中依然残留无法消除的 cls 参数。 - [03:00](https://www.youtube.com/watch?v=srSA-aiHf8Y&t=180s) 对于单纯起命名空间归类作用的外部装饰器,定义为 @staticmethod 并彻底移除 self / cls 是最干净的标准做法。 - [05:00](https://www.youtube.com/watch?v=srSA-aiHf8Y&t=300s) 当尝试在同一个类体内部用该装饰器装饰其他同类方法时,保留 @staticmethod 会在类定义阶段触发异常。 - [06:00](https://www.youtube.com/watch?v=srSA-aiHf8Y&t=360s) 在类定义执行期间,类体本质上是一个局部的代码块作用域(local namespace),尚未形成完整的类对象。 - [06:35](https://www.youtube.com/watch?v=srSA-aiHf8Y&t=395s) 在类体中不加任何装饰器、不带 self 的裸函数,在类定义阶段可作为普通局部可调用对象直接修饰其他方法。 - [07:05](https://www.youtube.com/watch?v=srSA-aiHf8Y&t=425s) 完全裸写的类内函数无法妥善支持外部访问,类构建完成后通过实例访问会触发方法绑定并因缺失 self 参数报错。 - [08:00](https://www.youtube.com/watch?v=srSA-aiHf8Y&t=480s) 装饰器语法糖本质上是对函数对象的就地重新赋值(即 func = decorator(func))。 - [08:25](https://www.youtube.com/watch?v=srSA-aiHf8Y&t=505s) 前置使用 @staticmethod 语法糖会立即使其变成静态方法描述符对象,破坏其在类定义作用域内的可调用性。 - [07:28](https://www.youtube.com/watch?v=srSA-aiHf8Y&t=448s) 后置显式调用 `log_function = staticmethod(log_function)` 能够在类定义期保留裸函数特性,在类导出时转换为合法的静态方法。 - [09:00](https://www.youtube.com/watch?v=srSA-aiHf8Y&t=540s) 理解这一技巧的关键在于掌握 Python 类定义的加载执行模型,以及函数转变为方法时的属性描述符绑定机制。
## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=srSA-aiHf8Y&t=0s) 为什么要把装饰器封装到类中?从实例方法到类方法
视频开篇以一个基础的日志装饰器 `log_function` 切入。这个装饰器的逻辑非常直观:在目标函数执行前先打印函数开始运行及其入参,在目标函数执行完毕后再打印函数结束。高天在下方定义了一个简单的斐波那契数列函数来演示效果,终端输出表明装饰器能够正常拦截并打印递归调用的每一层细节。通常情况下,开发者习惯直接把装饰器写在模块全局命名空间的最外层,但随着项目规模扩大,并不是所有工具函数都适合丢在全局。当开发者有多个功能相近的装饰器,或者出于高内聚的抽象设计理念,便会萌生将装饰器收拢并封装到一个独立的类(如 `Decorators` 类)中的想法。然而,对于习惯了普通函数装饰器的人来说,这一步往往会踩入诸多不直观的深坑。
最容易想到的实现方式是把装饰器当成普通类方法,为它硬加上一个 `self` 参数后直接塞进类体。但这种做法立刻带来了调用链上的尴尬:由于普通类方法必须由实例调用,若想在外部装饰一个函数,就不得不先实例化一个对象,写出 `d = Decorators()`,然后再通过 `@d.log_function` 来修饰函数。高天指出这种方式的体验非常差:“首先非常明显的,我们为了使用装饰器,迫不得已的要建立一个新的对象,这个事情非常不爽;其次我这个装饰器里面莫名其妙出现了一个 self argument,然而我这个装饰器压根就没有用到它。”
为了摆脱“必须先实例化”的束缚,下一步改进是引入 `@classmethod` 装饰器。将方法标记为类方法后,其第一个参数变为 `cls`,外部调用时便可以直接使用 `@Decorators.log_function`,省去了建立实例对象的冗余步骤。但这仍未彻底解决代码洁癖的问题——只要装饰器本身的功能不依赖该类或对象的内部属性,那个强行挂在函数签名第一位的 `cls` 参数就依然显得格格不入且毫无用处。
```mermaid flowchart TD accTitle: 类中定义装饰器的演进路径 accDescr: 展示从普通实例方法到类方法再到静态方法的逐步演进过程
subgraph S1["初步方案:普通实例方法"] A["定义 def log_function(self, func)"] --> B["必须先实例化:d = Decorators()"] B --> C["使用 @d.log_function 装饰"] C -.-> D["痛点:产生多余对象与无用 self 参数"] end
subgraph S2["改进方案:类方法"] E["标记 @classmethod<br/>def log_function(cls, func)"] --> F["直接通过类调用:@Decorators.log_function"] F -.-> G["痛点:签名仍残留无用的 cls 参数"] end ```
---
[03:00](https://www.youtube.com/watch?v=srSA-aiHf8Y&t=180s) 使用 @staticmethod 实现无副作用的类装饰器封装
当然,在某些特定业务场景中,装饰器确实需要读取类成员甚至实例成员的动态状态,从而根据类或对象的变化来改变自身的拦截行为,此时保留对象方法或类方法的形态是合理的。但在绝大多数纯粹的抽象与封装需求中,装饰器仅仅是挂靠在该命名空间下的通用工具,与宿主类的具体属性毫无羁绊。
针对这种纯粹的封装诉求,最佳手段是将装饰器转变为静态方法(`@staticmethod`)。高天演示了具体修改:直接将 `log_function` 签名中多余的 `self` 或 `cls` 参数彻底删掉,仅保留真正的目标函数参数 `func`,并在函数定义上方标注 `@staticmethod`。这样一来,`log_function` 成为挂在 `Decorators` 类下的静态成员,既没有改变原先纯函数的内部签名与逻辑,又通过类的命名空间完成了归类组织。在外部使用时,开发者依然只需书写 `@Decorators.log_function` 即可顺畅调用。当仅仅需要把装饰器封装到某个类中供外部使用时,这无疑是一种干净、标准且利落的处理模式。
---
[05:00](https://www.youtube.com/watch?v=srSA-aiHf8Y&t=300s) 类内互调的暗坑:为什么给装饰器加 @staticmethod 会报错?
紧接着,高天抛出了一个在实际项目中极为高频且无数人最终铩羽而归的难题:“如果我想用我类里面定义的属于这个类的装饰器,去装饰我这个类里面的方法,我应该怎么做?”比如,现在直接把待修饰的 `fib` 斐波那契函数也挪到 `Decorators` 类中,希望用同类里定义的 `log_function` 去装饰它。
许多人按照直觉保留刚才的 `@staticmethod` 并在同类方法上直接标注 `@log_function`,结果 Python 解释器在类加载阶段就会直接抛出报错;若尝试改成普通的实例方法或类方法,也同样无法正常生效。高天揭晓了一个让许多初学者大为震惊的反直觉答案:在这种场景下,正确的做法是直接把 `log_function` 原封不动、不带任何装饰器、也不带任何 `self` 参数地裸写在类体中。
看到类里面定义了一个既没有 `@staticmethod` 也没有 `self` 的函数,很多程序员的第一反应是觉得这不合规范,但运行后却发现它能完美运行。高天点明了背后的 Python 语言底层原理:“我们在之前的视频里面提到过,当我们在定义一个新的 class 的时候,它本质上是把 class 里面的 code block 在另外一个命名空间运行了一下。”在类定义的执行期,类体实际上就是一个局部的局部作用域(local namespace);只要在上面定义了普通的函数名字 `log_function`,紧随其后的方法就可以直接将它作为作用域内的普通可调用对象(callable)使用。它在定义阶段根本不是面向对象意义上的“method”,而是一个纯粹的临时辅助函数。
然而,这种裸写方式存在致命的外部兼容性缺陷。当类定义完成之后,由于它没有被声明为静态方法,若在类外部尝试通过实例 `d.log_function` 去访问它,Python 会试图将其绑定为普通实例方法并传递 `self`,从而引发参数不匹配的报错。
```mermaid flowchart LR accTitle: 类定义体执行原理与静态方法报错机制 accDescr: 说明类定义期间代码块作为局部命名空间执行的特性
subgraph ClassScope["类定义体执行阶段(局部命名空间)"] direction TB N1["执行代码块:创建裸函数 log_function"] N2["紧随其后的方法执行 @log_function(fib)"] N1 -->|"作为普通可调用对象"| N2 end
subgraph MethodDescriptor["外部访问阶段(属性描述符触发)"] direction TB E1["外部实例访问 d.log_function"] E2["触发函数描述符,尝试自动注入 self"] E1 --> E2 E2 -->|"缺少 self 入参"| E3["抛出 TypeError 报错"] end
ClassScope --> MethodDescriptor ```
---
[07:28](https://www.youtube.com/watch?v=srSA-aiHf8Y&t=448s) 终极解法:利用装饰器等价语法实现内外通用的静态装饰器
面对既要在类内部直接修饰同类方法,又要在类外部支持通过类名(`Decorators.log_function`)或实例(`d.log_function`)任意调用的终极需求,高天给出了最优雅的终极解法——手动利用静态方法的等价赋值语法。
具体做法是在定义 `log_function(func)` 时先不要添加 `@staticmethod` 语法糖,使其保持纯函数状态供类体下方的同类方法(如 `fib`)直接作为装饰器使用;而在整个类体定义的最后一行,显式补上一行赋值语句: ```python log_function = staticmethod(log_function) ```
高天解释了这行代码的本质机制:在 Python 中,`@staticmethod` 装饰器语法本质上就是 `func = staticmethod(func)` 的语法糖。如果在函数定义处直接加 `@staticmethod`,该函数在类定义体中立刻被包装为静态方法描述符对象,导致其在接下来的类作用域内无法像普通函数那样直接被当作装饰器调用;“只不过用装饰器的语法,你只能在定义它的同时装饰它。而当我们使用它的等价语法的时候,我们可以先定义它成一个正常的函数,然后在 class 的 definition 里面把它当成一个正常的装饰器去使用,最后再通过 staticmethod 把这个 log_function 变成一个 static method。”
加上这行后置处理代码后,类内方法成功吃到了裸函数的装饰效果,而在类定义完成后,该函数又被正式转换为静态方法,无论是通过 `Decorators.log_function` 还是 `d.log_function` 访问都能完美符合 Python 的描述符语义。高天最后总结道,这些看似神奇的奇技淫巧,其背后全都是 Python 基础执行模型与装饰器本质概念的自然推论。
- 关键机制总结: - 类体在加载时是作为独立的局部作用域执行的,未加工的裸函数在该作用域内具有直接可见性与可调用性。 - `@` 语法糖是就地立即求值的,提前将函数变为 `staticmethod` 对象会破坏其在类作用域内的直接可调用状态。 - 后置显式调用 `log_function = staticmethod(log_function)` 完美兼顾了“类内直接当装饰器使用”与“类外作为静态方法导出”的双重需求。
## Notable quotes > [05:00](https://www.youtube.com/watch?v=srSA-aiHf8Y&t=300s) 当我们在定义一个新的 class 的时候,它本质上是把 class 里面的 code block 在另外一个命名空间运行了一下。 — 码农高天
## Mentioned Python, 码农高天, Decorators, log_function, fib, staticmethod, classmethod
## Video description 这期视频讲一下如何在类的内部定义装饰器,尤其是如何在类的内部使用这个装饰器。这里有很多和封装有关的并不直观的知识,看了才知道!
[Source](https://www.youtube.com/watch?v=srSA-aiHf8Y)