如何在 Python 中使用类装饰器来混合行为?

Posted

技术标签:

【中文标题】如何在 Python 中使用类装饰器来混合行为?【英文标题】:How to mixin behavior using class decorators in Python? 【发布时间】:2016-11-19 05:24:25 【问题描述】:

我有一个有很多直接子类的基类。多个子类共享多个独立的特征。这是 Python 协作继承的一个很好的用例。但是,这些特性应该从外部包装行为,因此它们需要在方法解析顺序中更早。

class WrappedSub(FeatureA, FeatureB, FeatureC, RealSub):

    def __init__(self, *args, **kwargs):
        FeatureA.__init__(foo=42)
        FeatureB.__init__(bar=13)
        FeatureC.__init__(foobar=546)
        RealSub.__init__(*args, **kwargs)

class RealSub(Base):
    # Lots of code ...

改为装饰子类会很好。

@Mixin(FeatureA, 42)
@Mixin(FeatureB, 13)
@Mixin(FeatureC, 546)
class RealSub(Base):
    # Lots of code ...

确切地说,我需要一个 @Mixin 装饰器,其中下面的第一个块等同于第二个。

@Mixin(Sub, *feature_args, **feature_kwargs)
class RealSub:
    # Lots of code ...

class RealSub:
    # Lots of code ...
class WrappedSub(Feature, RealSub):
    def __init__(self, *sub_args, **sub_kwargs):
        Feature.__init__(self, *feature_args, **feature_kwargs)
        RealSub.__init__(self, *sub_args, **sub_kwargs)
RealSub = WrappedSub

这在 Python 3 中怎么可能?

【问题讨论】:

【参考方案1】:

您可能可以使用 Python 的协作多重继承系统来编写您的 mixin 类,而不是尝试将它们实现为类装饰器。这就是我对 Python OOP 中使用的术语“mixin”的一般理解。

class Base:
    def method(self, param):
        value = param + 18
        return value

class FeatureOne:               # this could inherit from Base 
    def method(self, param):
        if param == 42:
            return 13
        else:
            return super().method(param)   # call next class in inheritance chain

class Child(FeatureOne, Base):
    def method(self, param):
        value = super().method(param)
        value *= 2
        return value

这与您想要的不太一样,因为它在BaseChild 类的版本之间调用FeatureOne 类的method 实现,而不是在Child 执行它之前。您可以添加一个新的 Grandchild 类,该类继承自您首先关心的 Features,然后是 Child,如果您无法调整方法以按此顺序工作(Grandchild 类的主体可以为空)。

如果你真的想使用装饰器来翻转顺序,我认为你可以让它工作,装饰器为你构建一个“孙子”类(尽管它对正常的继承层次结构一无所知) .这是对 mixin 装饰器的粗略尝试,几乎可以像您想要的那样工作:

def mixin(*mixin_classes, **mixin_kwargs): # decorator factory function
    def decorator(cls): # decorator function
        class wrapper(*mixin_classes, cls):
            def __init__(self, *args, **kwargs):
                wrapped_kwargs = mixin_kwargs.copy() # use the passed kwargs to update the
                wrapped_kwargs.update(kwargs)        # mixin args, so caller can override
                super().__init__(*args, **wrapped_kwargs)
        # maybe modify wrapper's __name__, __qualname__, __doc__, etc. to match cls here?
        return wrapper
    return decorator

mixin 类应该从它们自己的 __init__ 方法(如果有的话)调用 super().__init__(*args, **kwargs),但它们可以接受(而不是传递)它们自己想要由mixin装饰者:

class FeatureOne:
    def __init__(self, *args, foo, **kwargs): # note that foo is a keyword-only argument
        self.foo = foo
        super().__init__(*args, **kwargs)

    def method(self, param):
        if param == self.foo:
            return 13
        else:
            return super().__method__(param)

@mixin(FeatureOne, foo=42)
class Child(Base):
    def method(self, param):
        return super().method(param) * 2

装饰器应该与传递给一个装饰器调用(例如@mixin(FeatureA, FeatureB, FeatureC, foo=42, bar=13, foobar=546))的所有混合类一起使用,或者与多个嵌套装饰器调用一起使用。最后一班的 MRO 都是一样的。

【讨论】:

我想到了合作继承并更新了问题。正如您所说,问题在于功能包装了子类行为。也许协作继承可以与@Mixin 类装饰器结合使用?另一个想法是复制包装要素类对象并将包装的子类添加到其__bases__。你认为这可行吗?

以上是关于如何在 Python 中使用类装饰器来混合行为?的主要内容,如果未能解决你的问题,请参考以下文章

如何编写一个装饰器来在上下文管理器中包装一些东西,它需要参数?

Python装饰器

如何使用@property 装饰器来限制列表中的值

为啥 Python 中没有 @override 装饰器来帮助提高代码的可读性? [关闭]

Python,如何添加另一个装饰器来过滤现有多装饰器的输出与python中的属性?

你如何使用 Flask-Login 和自定义 Python 装饰器来分配用户权限?