As I said in a comment, this sort of thing can be done using a metaclass. Here's how to do it with one:
class MixinA:
def some_method(self):
print('In MixinA.some_method()')
class MixinB:
def some_method(self):
print('In MixinB.some_method()')
class MyMetaClass(type):
mixins = {'A': MixinA, 'B': MixinB} # Supported mix-ins.
def __new__(cls, name, bases, classdict, **kwargs):
case = kwargs.pop('case')
mixin = cls.mixins.get(case)
if not mixin:
raise TypeError(f'Unknown mix-in case selector {case!r} specified')
else:
bases += (mixin,)
return type.__new__(cls, name, bases, classdict, **kwargs)
class ClassA(metaclass=MyMetaClass, case='A'): ...
class ClassB(metaclass=MyMetaClass, case='B'): ...
a = ClassA()
b = ClassB()
a.some_method() # -> In MixinA.some_method()
b.some_method() # -> In MixinB.some_method()
It could also be done without a metaclass at least a couple of ways. One would be by utilizing the __init_subclass__()
special method which was added to Python 3.6.
class MixinA:
def some_method(self):
print('In MixinA.some_method()')
class MixinB:
def some_method(self):
print('In MixinB.some_method()')
class MyClass:
mixins = {'A': MixinA, 'B': MixinB} # Supported mix-ins.
def __init_subclass__(cls, /, case, **kwargs):
super().__init_subclass__(**kwargs)
mixin = cls.mixins.get(case)
if not mixin:
raise TypeError(f'Unknown mix-in case selector {case!r} specified')
else:
cls.__bases__ += (mixin,)
class ClassA(MyClass, case='A'): ...
class ClassB(MyClass, case='B'): ...
a = ClassA()
b = ClassB()
a.some_method() # -> In MixinA.some_method()
b.some_method() # -> In MixinB.some_method()
Yet another way would be via a normal function, which is a possiblity because in Python classes are themselves "first class objects".
Here's an example of doing it via a function I've named mixer()
:
class MixinA:
def some_method(self):
print('In MixinA.some_method()')
class MixinB:
def some_method(self):
print('In MixinB.some_method()')
class MyClass: ...
MIXINS = {'A': MixinA, 'B': MixinB} # Supported mix-in classes.
def mixer(cls, *, case):
mixin = MIXINS.get(case)
if not mixin:
raise TypeError(f'Unknown mix-in case selector {case!r} specified')
mixed_classname = f'{cls.__name__}Plus{mixin.__name__}'
return type(mixed_classname, (cls, mixin), {})
ClassA = mixer(MyClass, case='A')
ClassB = mixer(MyClass, case='B')
a = ClassA()
b = ClassB()
a.some_method() # -> In MixinA.some_method()
b.some_method() # -> In MixinB.some_method()
As you can see, all these techniques suffer from a limitation often seen in the class factory pattern (which is more-or-less what we've dealing with here) often do, namely that a base or metaclass method or designated function has to know all the possibilities of subclass or mix-in classed for them to be usable.
There are workarounds to mitigate that though — at least when using other approaches, see my answer to Improper use of __new__
to generate classes? which can be used with the "class factory" pattern.
However, in this specific case, you could avoid the hardcoding of the mix-in classes by simply passing the mix-in class as the argument instead of a case
selector for it.