发布于2026-07-17 阅读(0)
扫一扫,手机访问
先说几个核心判断:类装饰器绝对是Python进阶路上一个绕不开的坎儿。很多人觉得它不过是函数装饰器的“类版本”,但真正用起来才发现,坑多,门道也多。今天咱们就把它彻底拆开揉碎,看看这玩意儿到底该怎么玩。

你可能会好奇,类怎么就能当装饰器用呢?答案就在 __call__ 这个方法上。Python 在遇到 @MyDecorator 时,会先调用 MyDecorator.__init__ 把被装饰的对象(比如一个类)存起来,等真正要调用这个对象时,再触发 __call__ 来执行装饰逻辑。这和函数装饰器的执行时机不太一样:函数装饰器是返回一个新函数,而类装饰器通常会返回一个新类,或者直接对原类进行“原地改造”。
容易踩坑的是,只写了 __init__ 却忘了 __call__,结果一运行就报错 TypeError: 'MyDecorator' object is not callable。记住几个要点:
__call__(self, cls),这里的 cls 就是被装饰的类。__init__ 里做类改造的逻辑——它只负责初始化配置,真正的装饰动作必须放在 __call__ 里。这是类装饰器最典型的用法:不改动原始类的定义,却能让它的所有实例自动拥有新行为。比如,你想让被装饰的类自动带上 created_at 时间戳和一个 log_init 方法。
class AutoLog:
def __init__(self, prefix=""):
self.prefix = prefix
def __call__(self, cls):
original_init = cls.__init__
def new_init(self, *args, **kwargs):
import datetime
self.created_at = datetime.datetime.now()
self.log_init = lambda: print(f"{self.prefix} initialized at {self.created_at}")
original_init(self, *args, **kwargs)
cls.__init__ = new_init
return cls
@AutoLog(prefix="User:")
class User:
def __init__(self, name):
self.name = name
注意这里直接覆写了 cls.__init__,而不是返回一个新类。这样做更轻量,但有个前提:你得确保不会覆盖掉父类已有的逻辑。如果原类有继承关系,记得在 new_init 里用 super().__init__ 显式调用。
log_init)是绑定在实例上的,不是类方法。cls 就行,例如 cls.version = "1.0"。__call__ 中做耗时操作,因为类装饰发生在模块导入时,会影响启动速度。类装饰器另一个强大的地方在于控制类的“构造过程”。比如,想确保全局只有一个实例(单例),或者按参数缓存构造结果(原型模式)。关键点在于拦截 cls(...) 这个调用,而不仅仅是改个定义。
这时候只靠改 __init__ 就不够用了,得重写 __new__ 或者用元类配合。但类装饰器可以很优雅地替换掉整个类的构造逻辑:
class Singleton:
def __call__(self, cls):
instances = {}
original_new = cls.__new__
def new(cls, *args, **kwargs):
key = (cls, args, tuple(sorted(kwargs.items())))
if key not in instances:
# 注意:__new__ 必须返回实例,且不能调用 cls.__new__(cls) 两次
instance = original_new(cls)
instances[key] = instance
# 再触发 __init__(如果用户没禁用)
if hasattr(instance, '__init__') and instance.__init__ is not object.__init__:
instance.__init__(*args, **kwargs)
return instances[key]
cls.__new__ = new
return cls
这个例子有个大坑:直接在 __new__ 里调用 cls() 会引发无限递归。必须用 original_new(cls) 来绕过装饰后的逻辑,才能拿到原始实例。
key)是否包含 args/kwargs,取决于你想要的是“全局唯一”还是“参数唯一”。__new__,要小心兼容性,最好检查一下 cls.__new__ is not object.__new__。args/kwargs 的哈希操作可能会失败。带参数的类装饰器(比如 @TrackChanges(fields=["name", "email"]))需要两层封装:外层是个接收参数的函数,内层才是真正的装饰器类。很多人容易在这个地方搞混,直接在类的 __init__ 里接装饰目标,导致语法错误。
正确的结构应该是这样:
def TrackChanges(**options):
def decorator(cls):
# 这里才是真正的装饰逻辑
fields = options.get("fields", [])
original_init = cls.__init__
def new_init(self, *args, **kwargs):
self._changes = {}
original_init(self, *args, **kwargs)
# 记录初始值(简化版)
for f in fields:
if hasattr(self, f):
self._changes[f] = getattr(self, f)
cls.__init__ = new_init
return cls
return decorator # 返回函数,不是类!
# 用法:
@TrackChanges(fields=["name", "email"])
class Person:
def __init__(self, name, email):
self.name = name
self.email = email
重点在于:带参数的装饰器必须是「函数返回装饰器」这个模式。哪怕内部用类实现,外层也得是函数。否则,@TrackChanges(...) 会被解析为 TrackChanges(...)(Person),而类实例是不可调用的。
TrackChanges() 和 TrackChanges()(cls)。但这样做极易混淆,非常不推荐。fields 的类型检查)应该放在外层函数里,而不是 decorator 函数中。print(f"Decorating {cls.__name__} with {options}") 会很有帮助,因为类装饰发生在 import 阶段,不容易下断点。总结一下,类装饰器真正难的不是语法,而是厘清「什么时候执行」:模块加载时运行 __call__,实例化时才可能触发你注入的 __new__ 或 __init__ 替换逻辑。如果混用这两层时机,就很容易出现“看似装饰了,但行为没生效”的诡异问题。
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
8