装饰器与继承

Posted

技术标签:

【中文标题】装饰器与继承【英文标题】:Decorators versus inheritance 【发布时间】:2011-09-18 09:14:42 【问题描述】:

如果可以同时使用装饰器和继承,你如何决定?

例如,this problem 有两种解决方案。

我对 Python 特别感兴趣。

【问题讨论】:

如果两者都可能,那么问题的范围太小了。 @Neil G:这是一个人为的问题,因为没有足够的约束来做出选择,所以有很多选择是可能的。 @S.Lott:我明白你在说什么。郑重声明,它不是“人为的”——它是一段真实的代码,它让我开始思考软件设计,我想了解其他人如何看待装饰器和继承之间的选择。来自 C++ 背景,我没有经常做出这个选择。无论如何,感谢您关于缺乏约束的观点。这是一个很好的观点。 @Neil G:实际上 mouad 在***.com/questions/6394511/… 的解决方案很好;他和我的工作方式是将装饰器重新定义为“卫生装饰器 v2.0”,以便在应用装饰器 v2.0 时,它会做它应该做的事情(记忆),但也确保像 @ 这样的特殊属性987654323@被转移。两者之间的唯一区别是他用一个类替换了memoize(...) 装饰器,而我的用一个函数替换了它。差别不大。 @ninjagecko:是的,我想通了。想到装饰器并保持一切正常对我来说仍然很奇怪。 【参考方案1】:

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

【讨论】:

如果这是 OP 真正想要问的,这将是单继承语言的完美答案。我觉得在 python 中使用子类会很好,因为可以使用多重继承来实现类似 mixin 的行为。一方面,我发现它们有点难以处理;另一方面,mixin 并不适合装饰器所暗示的堆栈抽象。 @ninjagecko:这条评论正是我想要的。 “堆栈抽象”是什么意思? 抱歉,我几乎可以肯定 Neil 在问别的问题。【参考方案2】:

您提到的问题不是在装饰器和类之间做出决定。它正在使用装饰器,但您可以选择使用以下任何一种:

一个装饰器,它返回一个类 一个装饰器,它返回一个函数

装饰器只是“包装器”模式的一个花哨名称,即用其他东西替换某些东西。实现取决于您(类或函数)。

在它们之间做出决定时,完全取决于个人喜好。你可以做所有你能做的事情。

如果装饰一个函数,您可能更喜欢返回代理函数的装饰器 如果装饰一个类,您可能更喜欢返回代理类的装饰器

(为什么这是个好主意?可能会假设修饰函数仍然是函数,而修饰类仍然是类。)

在这两种情况下更好的是使用一个装饰器,它只返回原始的,以某种方式修改。

编辑:在更好地理解您的问题后,我在Python functools.wraps equivalent for classes 发布了另一个解决方案

【讨论】:

【参考方案3】:

装饰器...:

...如果您尝试做的是“包装”,则应使用。包装包括获取某些内容、修改(或将其注册到某些内容)和/或返回行为“几乎完全”与原始对象相同的代理对象。 ...适用于类似 mixin 的行为,只要您不创建大量代理对象。 ...具有隐含的“堆栈”抽象:

例如

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

相当于:

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

多重继承...:

... 最适合向类添加方法;你不能用它来轻松地装饰功能。在这种情况下,如果您只需要一组“duck-typing style”额外方法,它就可以用来实现类似 mixin 的行为。 ... 如果您的问题不适合它,可能会有点笨拙,超类构造函数等问题。例如,除非显式调用子类 __init__ 方法,否则不会调用它(通过方法-解析-顺序协议)!

总而言之,如果装饰器不返回代理对象,我会使用装饰器来实现类似 mixin 的行为。一些示例将包括返回原始函数的任何装饰器,稍作修改(或在某处注册或将其添加到某个集合之后)。

你经常会发现装饰器的东西(比如记忆化)也是不错的选择,但如果它们返回代理对象,则应适度使用;它们的应用顺序很重要。太多的装饰器以一种不打算使用的方式使用它们。

如果这是一个“经典继承问题”,或者如果我需要的 mixin 行为只是方法,我会考虑使用继承。一个经典的继承问题是您可以在可以使用父级的任何地方使用子级。

一般来说,我会尝试在不需要增强任意东西的地方编写代码。

【讨论】:

再想一想,我认为一个关键点是通过继承提供的issubclassisinstance。如果你想要那些,你可能想要继承。这证明了 Collections.* 是使用继承而不是装饰器实现的。 @Neil:当我说“在哪里可以使用父级时,你可以使用子级”时,这正是我所要表达的意思 =) 我还想指出,因为在您的回答中,Python 中的多重继承(与例如 C++ 不同)由于协作继承而确实具有堆栈抽象,这一点并不明显。每个连续基类的__init__ 都会看到一个修改过的对象。【参考方案4】:

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

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

因此,在决定使用哪一个时,您可能首先要检查扩展功能是否仅特定于此类,或者是否可以在系统的其他部分中重复使用相同的扩展功能。如果它不能被重用,那么继承应该可以完成这项工作。否则,你可以考虑使用装饰器。

【讨论】:

【参考方案5】:

其他答案都很好,但我想给出一个简洁的利弊清单。

mixins 的主要优点是可以在运行时使用isinstance 检查类型,并且可以使用 MyPy 之类的 linter 检查。像所有继承一样,当你有 is-a 关系时应该使用它。例如,dataclass 可能应该是一个 mixin,以便公开特定于数据类的自省变量,例如数据类字段列表。

当您没有 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__,装饰器可以做的所有事情都可以用 mixins 来完成,而且它们通常会影响子子类:

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

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

总之,在选择 mixin 和装饰时应该问的问题是:

是否存在 is-a 模式? 你会打电话给isinstance吗? 您会在类型注释中使用 mixin 吗? 您希望该行为影响子类吗?

【讨论】:

以上是关于装饰器与继承的主要内容,如果未能解决你的问题,请参考以下文章

装饰器模式与继承的例子

Python 装饰器和继承

叠加装饰器与迭代器

Python装饰器与面向切面编程

如何在 QML 中将 @pyqtSlot 装饰器与其他装饰器一起使用?

设计模式-装饰者模式