Python 有一个不太为人所知的语法特性,就是类装饰器。其语法和工作方式都与第 2 章介绍的函数装饰器完全相同。唯一的区别在于它的返回值是一个类,而不是函数对象。 下面是一个类装饰器的例子,修改__repr__()方法并返回缩短的可打印对象表示,缩短后 的长度可任意取值,如下所示:
def short_repr(cls):
cls.__repr__ = lambda self: super(cls, self).__repr__()[:8] return cls
@short_repr
class ClassWithRelativelyLongName: pass
你将会看到以下输出:
>>> ClassWithRelativelyLongName()
<ClassWi
当然,上面的代码片段并不是很好的代码示例,因为其含义过于模糊。不过,它展示
了本章介绍的多种语言特性可以综合使用。
• 在运行时不仅可以修改实例,还可以修改类对象。
• 函数也是描述符,之所以也可以在运行时添加到类中,是因为根据描述符协议,在
属性查找时将执行实际绑定的实例。
• 只要提供了正确的参数,super()调用可以在类定义作用域之外使用。
• 最后,类装饰器可以用于类的定义。 编写函数装饰器的其他内容也适用于类装饰器。最重要的是,它可以使用闭包,也可
以被参数化。利用这一点,可以将上一个例子重写成更加易于阅读和维护的形式:
def parametrized_short_repr(max_width=8): """缩短表示的参数化装饰器"""
def parametrized(cls):
"""内部包装函数,是实际的装饰器""" class ShortlyRepresented(cls): """提供装饰器行为的子类"""
def __repr__(self):
return super().__repr__()[:max_width]
return ShortlyRepresented
return parametrized
在类装饰器中这样使用闭包的主要缺点是,生成的对象不再是被装饰的类的实例,而是在 装饰器函数中动态创建的子类的实例。这会影响类的__name__和__doc__等属性,如下所示:
@parametrized_short_repr(10)
class ClassWithLittleBitLongerLongName:
pass
类装饰器的这种用法会使类的元数据发生以下变化:
>>> ClassWithLittleBitLongerLongName().__class__ <class 'ShortlyRepresented'>
>>> ClassWithLittleBitLongerLongName().__doc__ 'Subclass that provides decorated behavior'
不幸的是,这个问题不能用第 2 章“(4)保存内省的装饰器”一节介绍的方法(使用额外 的 wraps 装饰器)简单解决。这样的话,在某些情况下以这种形式使用类装饰器会受到限制。 如果没有做其他工作来保存旧类的元数据,那么这可能会破坏许多自动生成文档工具的结果。
虽然有这样的警告,但类装饰器仍然是对流行的混入(mixin)类模式的一种简单又轻 量级的替代方案。
Python 中的混入类是一种不应被初始化的类,而是用来向其他现有类提供某种可复用 的 API 或功能。混入类几乎总是使用多重继承来添加,其形式如下:
class SomeConcreteClass(MixinClass, SomeBaseClass):
pass
混入类是很有用的设计模式,在许多库中都有应用。举个例子,Django 就是大量使用 这种模式的框架之一。虽然混入类很有用也很流行,但如果设计不好的话可能会导致一些 麻烦,因为大部分情况下都需要开发人员依赖多重继承。我们前面说过,由于 MRO 的存 在,Python 对多重继承的处理相对较好。但如果仅因为不需要额外工作且使代码变得简单 的话,最好避免将多个类子类化。这也是类装饰器能很好地代替混入类的原因。