Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to mixin behavior using class decorators in Python?

I have a base class that has a lot of direct sub classes. There are multiple independent features that are shared by multiple of the sub classes. This is a good use case for Python's cooperative inheritance. However, the features should wrap behavior from the outside, so they need to be earlier in the method resolution order.

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 ...

It would be nice to decorate the child classes instead.

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

Precisely, I need a @Mixin decorator where the first block below is be equivalent to the second.

@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

How is this possible in Python 3?

like image 210
danijar Avatar asked Jun 01 '26 22:06

danijar


1 Answers

You can probably use Python's cooperative multiple-inheritance system to write your mixin classes, rather than trying to implement them as class decorators. This is how I've generally understood the term "mixin" to be used in Python OOP.

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

This isn't quite the same as what you wanted, since it calls the FeatureOne class's method implementation between the Base and Child classes' versions, rather than before Child does its thing. You could instead add an new Grandchild class that inherits from the Features you care about first, and Child last, if you can't adjust the methods to work in this order (the Grandchild class's body could be empty).

If you really want to use decorators to flip the order around, I think you could probably make it work, with the decorator building a "grandchild" class for you (though it doesn't know anything about the normal inheritance hierarchy). Here's a rough attempt at a mixin decorator that works almost like you want:

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

The mixin classes should call super().__init__(*args, **kwargs) from their own __init__ method (if they have one), but they can accept (and not pass on) keyword-only arguments of their own that they want to be passed by the mixin decorator:

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

The decorator should work either with all the mixin classes passed to one decorator call (e.g. @mixin(FeatureA, FeatureB, FeatureC, foo=42, bar=13, foobar=546)), or with several nested decorator calls. The MRO of the final class will be the same either way.

like image 136
Blckknght Avatar answered Jun 03 '26 22:06

Blckknght