Search code examples
pythonmultiple-inheritancemethod-resolution-order

If you store optional functionality of a base class in a secondary class, should the secondary class subclass the base class?


I know the title is probably a bit confusing, so let me give you an example. Suppose you have a base class Base which is intended to be subclassed to create more complex objects. But you also have optional functionality that you don't need for every subclass, so you put it in a secondary class OptionalStuffA that is always intended to be subclassed together with the base class. Should you also make that secondary class a subclass of Base?

This is of course only relevant if you have more than one OptionalStuff class and you want to combine them in different ways, because otherwise you don't need to subclass both Base and OptionalStuffA (and just have OptionalStuffA be a subclass of Base so you only need to subclass OptionalStuffA). I understand that it shouldn't make a difference for the MRO if Base is inherited from more than once, but I'm not sure if there are any drawbacks to making all the secondary classes inherit from Base.

Below is an example scenario. I've also thrown in the QObject class as a 'third party' token class whose functionality is necessary for one of the secondary classes to work. Where do I subclass it? The example below shows how I've done it so far, but I doubt this is the way to go.

from PyQt5.QtCore import QObject

class Base:
    def __init__(self):
        self._basic_stuff = None

    def reset(self):
        self._basic_stuff = None


class OptionalStuffA:
    def __init__(self):
        super().__init__()
        self._optional_stuff_a = None

    def reset(self):
        if hasattr(super(), 'reset'):
            super().reset()
        self._optional_stuff_a = None

    def do_stuff_that_only_works_if_my_children_also_inherited_from_Base(self):
        self._basic_stuff = not None


class OptionalStuffB:
    def __init__(self):
        super().__init__()
        self._optional_stuff_b = None

    def reset(self):
        if hasattr(super(), 'reset'):
            super().reset()
        self._optional_stuff_b = None

    def do_stuff_that_only_works_if_my_children_also_inherited_from_QObject(self):
        print(self.objectName())


class ClassThatIsActuallyUsed(Base, OptionalStuffA, OptionalStuffB, QObject):
    def __init__(self):
        super().__init__()
        self._unique_stuff = None

    def reset(self):
        if hasattr(super(), 'reset'):
            super().reset()
        self._unique_stuff = None

Solution

  • What I can get from your problem is that you want to have different functions and properties based on different condition, that sounds like good reason to use MetaClass. It all depends how complex your each class is, and what are you building, if it is for some library or API then MetaClass can do magic if used rightly.

    MetaClass is perfect to add functions and property to the class based on some sort of condition, you just have to add all your subclass function into one meta class and add that MetaClass to your main class

    From Where to start

    you can read about MetaClass here, or you can watch it here. After you have better understanding about MetaClass see the source code of Django ModelForm from here and here, but before that take a brief look on how the Django Form works from outside this will give You an idea on how to implement it.

    This is how I would implement it.

    #You can also inherit it from other MetaClass but type has to be top of inheritance
    class meta_class(type):
        # create class based on condition
    
        """
        msc: meta_class, behaves much like self (not exactly sure).
        name: name of the new class (ClassThatIsActuallyUsed).
        base: base of the new class (Base).
        attrs: attrs of the new class (Meta,...).
        """
    
        def __new__(mcs, name, bases, attrs):
            meta = attrs.get('Meta')
            if(meta.optionA){
                attrs['reset'] = resetA
            }if(meta.optionB){
                attrs['reset'] = resetB
            }if(meta.optionC){
                attrs['reset'] = resetC
            }
            if("QObject" in bases){
                attrs['do_stuff_that_only_works_if_my_children_also_inherited_from_QObject'] = functionA
            }
            return type(name, bases, attrs)
    
    
    class Base(metaclass=meta_class): #you can also pass kwargs to metaclass here
    
        #define some common functions here
        class Meta:
            # Set default values here for the class
            optionA = False
            optionB = False
            optionC = False
    
    
    class ClassThatIsActuallyUsed(Base):
        class Meta:
            optionA = True
            # optionB is False by default
            optionC = True
    

    EDIT: Elaborated on how to implement MetaClass.