Search code examples
pythonpython-3.xmixinspython-class

Best practice for providing optional functions for a class in Python


Currently I am writing a Python program with a plugin system. To develop a new plugin a new class must be created and inherit from a base plugin class. Now it should be possible to add optional functions via mixins. Some mixins provide new functions others access builtin types of the base class and can act with them or change them.

In the following a simplified structure:

import abc
import threading

class Base:
    def __init__(self):
        self.config = dict()

        if hasattr(self, "edit_config"):
            self.edit_config()

     def start(self):
        """Starts the Plugin"""
        if hasattr(self, "loop"):
            self._loop()

class AMixin:
    def edit_config(self):
        self.config["foo"] = 123

class BMixin(abc.ABC):
    def _loop(self):
        thread = threading.Thread(target=self.loop, daemon=True)
        thread.start()

    @abc.abstractmethod
    def loop(self):
        """Override this method with a while true loop to establish a ongoing loop
        """
        pass

class NewPlugin(Base, AMixin, BMixin):
    def loop(self):
        while True:
            print("Hello")

 plugin = NewPlugin()
 plugin.start()

What is the best way to tackle this problem?

EDIT: I need to make my question more specific. The question is whether the above is the Pythonic way and is it possible to ensure that the mixin are inherited exclusively in conjunction with the Base class. Additionally it would be good in an IDE like VSCode to get support for e.g. autocomplete when accessing builtin types of the Base class, like in AMixin, without inheriting from it of course.


Solution

  • If you want to allow but not require subclasses to define some behaviour in a method called by the base class, the simplest way is to declare the method in the base class, have an empty implementation, and just call the method unconditionally. This way you don't have to check whether the method exists before calling it.

    class Base:
        def __init__(self):
            self.config = dict()
            self.edit_config()
        
        def start(self):
            self.loop()
        
        def edit_config(self):
            pass
        
        def loop(self):
            pass
    
    class AMixin:
        def edit_config(self):
            self.config["foo"] = 123
    
    class NewPlugin(AMixin, Base):
        def loop(self):
            for i in range(10):
                print("Hello")
    

    Note that you have to write AMixin before Base in the list of superclasses, so that its edit_config method overrides the one from Base, and not the other way around. You can avoid this by writing class AMixin(Base): so that AMixin.edit_config always overrides Base.edit_config in the method resolution order.

    If you want to require subclasses to implement one of the methods, then you can raise TypeError() instead of pass in the base class's method.