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?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With