0

Is there a way in python to inherit different mixin classes depending on some other argument? Example:

a = MyClass(case='A')  # spawns a class instance which inherits MixinA
b = MyClass(case='B')  # spawns a class instance which inherits MixinB

MixinA and MixinB both need access to the data of MyClass but do not host any data themselves. They both define different versions of some_method, which will be referred to by self.some_method() from within MyClass, but does different things depending on the case-argument passed to MyClass.

I am not sure whether this makes sense or whether there is a better design. In my application performance is key, so I want to try to avoid overhead as much as possible when calling the case-specific methods defined by MixinA and MixinB. I read somewhere that using mixins would be good for that. But perhaps there are better approaches.

martineau
  • 119,623
  • 25
  • 170
  • 301
Bob
  • 428
  • 3
  • 11
  • [Premature optimization](http://wiki.c2.com/?PrematureOptimization). – martineau Feb 28 '21 at 21:18
  • @martineau What would be the non-optimized way here to switch methods of `MyClass` depending on `case`? – Bob Feb 28 '21 at 21:26
  • One would be by making `MyClass` an instance of a custom metaclass. You could also define a [_class factory_](https://stackoverflow.com/questions/2526879/what-exactly-is-a-class-factory/2949205#2949205). – martineau Feb 28 '21 at 21:48

1 Answers1

1

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.

martineau
  • 119,623
  • 25
  • 170
  • 301
  • I played around a bit with this and it seems to do exactly what I want. Three quick questions: (i) What's the purpose of the `/` argument to `__init_subclass__`? (ii) Why the ellipsis in the definitions of `ClassA` and `ClassB` and not `pass`? If apart from the 2 mix-in versions all methods are shared, then there is no reason to add anything else to `ClassA` and `ClassB`, right? (iii) In my case it doesn't matter, because the mix-in methods don't overload anything from `MyClass`, but would it make sense in general to swap the order of `cls.__bases__ + (mixin,)`? – Bob Feb 28 '21 at 23:11
  • **(i)** The `/` means keywords only from that point on (you have to pass `case` as a keyword). **(ii)** The ellipsis is just a placeholder effectively just like `pass` (but conveys the intent better IMO and is one character shorter, to boot). Anyway, there's no reason in this scenario to add anything else. **(iii)** Where you place the mix-in in base class tuple would depend on exactly what you're doing—but generally mix-ins don't overload methods of the class they're mixed with. Parting thought: Since my answer does what you want, please consider accepting it. – martineau Feb 28 '21 at 23:31
  • I just saw your edit - thanks for adding the metaclass approach. I was wondering how to implement that one when you mentioned it. It's nice to see the two approaches side-by-side. Also good idea about passing the mix-in directly as an argument. That makes it even cleaner. – Bob Mar 03 '21 at 01:30
  • I've add yet another approach that's probably the simplest. Again, I suggest you run some timing tests and see if the reason you want to do this (better performance) is worth the trouble or not. More complicated code is its own form of "overhead" you know… – martineau Mar 03 '21 at 15:53