Search code examples
python-3.xtkintermultiple-inheritancettkmethod-resolution-order

Best practices for multiple inheritance in this Python code


I'm having some doubts with the design of mutiple inheritance in some Python classes.

The thing is that I wanted to extend the ttk button. This was my initial proposal (I'm omitting all the source code in methods for shortening, except init methods):

import tkinter as tk
import tkinter.ttk as ttk

class ImgButton(ttk.Button):
    """
    This has all the behaviour for a button which has an image
    """
    def __init__(self, master=None, **kw):
        super().__init__(master, **kw)
        self._img = kw.get('image')

    def change_color(self, __=None):
        """
        Changes the color of this widget randomly
        :param __: the event, which is no needed
        """
        pass

    def get_style_name(self):
        """
        Returns the specific style name applied for this widget
        :return: the style name as a string
        """
        pass

    def set_background_color(self, color):
        """
        Sets this widget's background color to that received as parameter
        :param color: the color to be set
        """
        pass

    def get_background_color(self):
        """
        Returns a string representing the background color of the widget
        :return: the color of the widget
        """
        pass

    def change_highlight_style(self, __=None):
        """
        Applies the highlight style for a color
        :param __: the event, which is no needed
        """
        pass

But I realized later that I wanted also a subclass of this ImgButton as follows:

import tkinter as tk
import tkinter.ttk as ttk

class MyButton(ImgButton):
    """
    ImgButton with specifical purpose
    """

    IMG_NAME = 'filename{}.jpg'
    IMAGES_DIR = os.path.sep + os.path.sep.join(['home', 'user', 'myProjects', 'myProject', 'resources', 'images'])
    UNKNOWN_IMG = os.path.sep.join([IMAGES_DIR, IMG_NAME.format(0)])
    IMAGES = (lambda IMAGES_DIR=IMAGES_DIR, IMG_NAME=IMG_NAME: [os.path.sep.join([IMAGES_DIR, IMG_NAME.format(face)]) for face in [1,2,3,4,5] ])()

    def change_image(self, __=None):
        """
        Changes randomly the image in this MyButton
        :param __: the event, which is no needed
        """
        pass

    def __init__(self, master=None, value=None, **kw):
        # Default image when hidden or without value

        current_img = PhotoImage(file=MyButton.UNKNOWN_IMG)
        super().__init__(master, image=current_img, **kw)
        if not value:
            pass
        elif not isinstance(value, (int, Die)):
            pass
        elif isinstance(value, MyValue):
            self.myValue = value
        elif isinstance(value, int):
            self.myValue = MyValue(value)
        else:
            raise ValueError()
        self.set_background_color('green')
        self.bind('<Button-1>', self.change_image, add=True)


    def select(self):
        """
        Highlights this button as selected and changes its internal state
        """
        pass

    def toggleImage(self):
        """
        Changes the image in this specific button for the next allowed for MyButton
        """
        pass

The inheritance feels natural right to his point. The problem came when I noticed as well that most methods in ImgButton would be reusable for any Widget I may create in the future.

So I'm thinking about making a:

class MyWidget(ttk.Widget):

for putting in it all methods which help with color for widgets and then I need ImgButton to inherit both from MyWidget and ttk.Button:

class ImgButton(ttk.Button, MyWidget):  ???

or

class ImgButton(MyWidget, ttk.Button):  ???

Edited: Also I want my objects to be loggable, so I did this class:

class Loggable(object):
    def __init__(self) -> None:
        super().__init__()
        self.__logger = None
        self.__logger = self.get_logger()

        self.debug = self.get_logger().debug
        self.error = self.get_logger().error
        self.critical = self.get_logger().critical
        self.info = self.get_logger().info
        self.warn = self.get_logger().warning


    def get_logger(self):
        if not self.__logger:
            self.__logger = logging.getLogger(self.get_class())
        return self.__logger

    def get_class(self):
        return self.__class__.__name__

So now:

class ImgButton(Loggable, ttk.Button, MyWidget):  ???

or

class ImgButton(Loggable, MyWidget, ttk.Button):  ???

or

class ImgButton(MyWidget, Loggable, ttk.Button):  ???

# ... this could go on ...

I come from Java and I don't know best practices for multiple inheritance. I don't know how I should sort the parents in the best order or any other thing useful for designing this multiple inheritance.

I have searched about the topic and found a lot of resources explaining the MRO but nothing about how to correctly design a multiple inheritance. I don't know if even my design is wrongly made, but I thought it was feeling pretty natural.

I would be grateful for some advice, and for some links or resources on this topic as well.

Thank you very much.


Solution

  • I've been reading about multiple inheritance these days and I've learnt quite a lot of things. I have linked my sources, resources and references at the end.

    My main and most detailed source has been the book "Fluent python", which I found available for free reading online.

    This describes the method resolution order and design sceneries with multiple inheritance and the steps for doing it ok:

    1. Identify and separate code for interfaces. The classes that define methods but not necessarily with implementations (these ones should be overriden). These are usually ABCs (Abstract Base Class). They define a type for the child class creating an "IS-A" relationship

    2. Identify and separate code for mixins. A mixin is a class that should bring a bundle of related new method implementations to use in the child but does not define a proper type. An ABC could be a mixin by this definition, but not the reverse. The mixin doesn't define nor an interface, neither a type

    3. When coming to use the ABCs or classes and the mixins inheriting, you should inherit from only one concrete superclass, and several ABCs or mixins:

    Example:

    class MyClass(MySuperClass, MyABC, MyMixin1, MyMixin2):
    

    In my case:

    class ImgButton(ttk.Button, MyWidget):
    
    1. If some combination of classes is particularly useful or frequent, you should join them under a class definition with a descriptive name:

    Example:

     class Widget(BaseWidget, Pack, Grid, Place):
         pass
    

    I think Loggable would be a Mixin, because it gathers convenient implementations for a functionality, but does not define a real type. So:

    class MyWidget(ttk.Widget, Loggable): # May be renamed to LoggableMixin
    
    1. Favor object composition over inheritance: If you can think of any way of using a class by holding it in an attribute instead of extending it or inheriting from it, you should avoid inheritance.

      "Fluent python" - (Chapter 12) in Google books

      Super is super

      Super is harmful

      Other problems with super

      Weird super behaviour