装饰器与继承

25

在两种方式都可行的情况下,如何决定使用装饰器和继承?

例如,这个问题有两种解决方法。

我对Python特别感兴趣。


1
如果两者都可能,则问题的范围太小。 - Ignacio Vazquez-Abrams
@Neil G:这是一个人为制造的问题,因为没有足够的限制条件来做出选择,所以有很多选择。 - S.Lott
1
@S.Lott: 我明白你的意思。为了记录,这不是“勉强凑合”的——它是一段真正的代码,它让我思考软件设计,并且我想知道其他人如何看待在装饰器和继承之间做出选择。作为一个来自C++背景的人,我并没有经常做出这个选择。无论如何,谢谢你指出缺乏约束的问题。这是一个好观点。 - Neil G
@Neil G:实际上,mouad在https://dev59.com/p2w15IYBdhLWcg3wy-zO#6394966提供的解决方案非常好;他和我所做的工作是将装饰器重新定义为“卫生装饰器v2.0”,因此当应用装饰器v2.0时,它会执行其应该执行的操作(记忆化),但还会确保传输像`__doc__`这样的特殊属性。两者之间唯一的区别在于他用一个类替换了`memoize(...)`装饰器,而我用一个函数替换了它。没有太大的区别。 - ninjagecko
@ninjagecko:是的,我已经弄清楚了。对我来说,想到装饰器并保持一切顺利仍然很奇怪。 - Neil G
5个回答

23

装饰器...:

  • 如果你想要做的是“包装”,则应该使用装饰器。包装包括获取某些内容,修改(或将其注册到其他内容中),和/或返回行为“几乎完全”类似于原始对象的代理对象。
  • 只要不创建大量代理对象堆栈,就可以将装饰器用于应用类似混合的行为。
  • 具有隐含的“堆栈”抽象:

例如:

@decoA
@decoB
@decoC
def myFunc(...): ...
    ...

等同于:

def myFunc(...): ...
    ...
myFunc = decoA(decoB(decoC(myFunc)))  #note the *ordering*

多重继承...:

  • ...最适合为类添加方法;您不能轻松地使用它来装饰函数。在这种情况下,如果您只需要一组“鸭式类型”额外方法,它可以用于实现混入(mixin)行为。
  • ...如果您的问题与之不匹配,则可能有些笨拙,存在超类构造函数等问题。例如,子类的__init__方法将不会被调用,除非显式调用它(通过方法解析顺序协议)!

总之,如果装饰器不返回代理对象,我会使用装饰器进行混合(mixin)行为。其中一些示例包括任何返回经过略微修改的原始函数(或在某处注册或将其添加到某个集合后)的装饰器。

常见的装饰器主要用于记忆(memoization)等方面也是一个不错的选择,但是如果它们返回代理对象,则应该适度使用;应用它们的顺序很重要。将太多的装饰器叠加在一起就像滥用了它们本不想要的方式。

如果问题是“传统继承问题”,或者如果我所需要的混入(mixin)行为仅涉及方法,则可以考虑使用继承。传统的继承问题是指您可以在任何可以使用父类的地方使用子类。

总的来说,我尽可能编写不需要增强任意事物的代码。


3
进一步思考后,我认为关键点在于继承提供的 issubclassisinstance。如果你需要这些功能,那么你可能需要使用继承。这就是使用继承而不是装饰器实现 Collections.* 的合理性所在。 - Neil G
1
@Neil:这正是我所说的“您可以在任何可以使用父对象的地方使用子对象”的意思 =) - ninjagecko
2
我还想指出,因为你的回答中并不明显,Python 中的多重继承(不像 C++)由于协作继承而具有堆栈抽象。每个连续的基类的 __init__ 都会看到一个修改后的对象。 - Neil G

6
其他答案都很好,但我想给出一个简洁的优缺点列表。
Mixin的主要优点是可以使用`isinstance`在运行时检查类型,并且可以像MyPy这样的linters进行检查。与所有继承一样,应在具有is-a关系时使用。例如,dataclass 应该成为一个mixin,以公开特定于 dataclass 的内省变量,如 dataclass 字段列表。
当您没有is-a关系时,应优先考虑使用装饰器。例如,从另一个类传播文档或将类注册到某些集合的装饰器。
装饰通常仅影响其装饰的类,而不影响从基类继承的类:
@decorator
class A:
    ...  # Can be affected by the decorator.

class B(A):
    ...  # Not affected by the decorator in most cases.

现在 Python 有了 __init_subclass__,所有装饰器能做的事情都可以通过 mixin 来完成,并且它们通常会影响子类:

class A(Mixin):
    ... # Is affected by Mixin.__init_subclass__.

class B(A):
    ... # Is affected by Mixin.__init_subclass__.

Mixins有另一个优点,它们可以提供空的基类方法。子类可以用一些“增强”行为覆盖这些方法,然后调用super。装饰器不能轻易提供这样的基类方法。这是Mixin更加灵活的另一种方式。
总之,在决定使用Mixin或Decoration时,您应该问自己以下问题: - 是否存在is-a模式? - 您是否会调用isinstance? - 是否会在类型注释中使用Mixin? - 是否希望行为影响子类? - 是否需要增强方法?
通常情况下,倾向于使用继承。

5
您所涉及的问题并不是在装饰器和类之间做出决策。问题在于使用装饰器,但您可以选择使用以下任一选项:
- 返回类的装饰器 - 返回函数的装饰器
装饰器只是“包装器”模式的一个花哨名称,即用其他东西替换某个东西。实现取决于您(类或函数)。
在选择它们之间时,完全是个人喜好的问题。您可以使用其中一种方法做到另一种方法所能做的一切。
- 如果要装饰函数,则可能更喜欢返回代理函数的装饰器。 - 如果要装饰类,则可能更喜欢返回代理类的装饰器。
(为什么这是个好主意?可能会有一些假设,即经过装饰的函数仍然是函数,而经过装饰的类仍然是类。)
在两种情况下,更好的方法是使用一个装饰器,该装饰器只返回原始内容,以某种方式进行修改。
编辑:更好地理解您的问题后,我已经在Python functools.wraps equivalent for classes上发布了另一种解决方案。

1
如果两者等效,我更喜欢使用装饰器,因为您可以将同一个装饰器用于多个类,而继承只适用于一个特定的类。

1
如果这是OP实际想要问的内容,那么在单继承语言中,这将是完美的答案。我认为在Python中使用子类应该是可行的,因为可以使用多重继承来实现混合类似的行为。一方面,我发现它们有点难处理;另一方面,混入不适用于装饰器所暗示的堆栈抽象。 - ninjagecko
1
@ninjagecko:这个评论正是我所需要的。你所说的“堆栈抽象”是什么意思? - Neil G
抱歉,我几乎确定尼尔是在问其他的事情。 - ninjagecko

1

个人而言,我更倾向于考虑代码重用。装饰器有时比继承更灵活。

让我们以缓存为例。如果您想要将缓存功能添加到系统中的两个类A和B中,使用继承,您可能最终会拥有ACached和BCached。并且通过覆盖这些类中的一些方法,您可能会为相同的缓存逻辑重复大量代码。但是如果在这种情况下使用装饰器,则只需要定义一个装饰器来装饰两个类。

因此,在决定使用哪种方法时,您可能首先要检查扩展功能是否仅特定于此类,或者是否可以在系统的其他部分重用相同的扩展功能。如果不能重用,则继承可能是合适的选择。否则,您可以考虑使用装饰器。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接