James Key
James Key

Reputation: 11

What is the proper way to deal with Python overriding of class methods with a mixin in multiple inheritance without redundant code?

I'm having a minor, I hope, issue with theory and the proper way to deal with a problem. It's easier for me to show an example then to explain as I seem to fail with my vocabulary.

class Original_1:
    def __init__(self):
        pass
    def meth1(self):
        pass
    def meth2(self):
        pass

class Original_2(Original_1):
    def __init__(self):
        Original_1.__init__(self)
    def meth3(self):
        pass

class Mixin:
    def __init__(self):
        pass
    def meth4(self):
        ...
        meth1(self)
        meth2(self)

class NewClass_1(Original_1, Mixin):
    def __init__(self):
        Original_1.__init__(self)
        Mixin.__init__(self)

class NewClass_2(Original_2, Mixin):
    def __init__(self):
        Original_2.__init__(self)
        Mixin.__init__(self)

Now the goal is to extend Original_1 or Original_2 with new methods in the Mixin, but I run into some questions if I use meth1(), meth2(), or meth3() in the mixin. 1. I'm not referencing Original_1 or Origninal_2 in the mixin. (At this point it runs but I don't like it.) 2. If I make Mixin a child of Original_1, it breaks. I could make two separate NewClass_X but then I'm duplicating all of that code.

Upvotes: 0

Views: 2923

Answers (2)

ShadowRanger
ShadowRanger

Reputation: 155418

Since Mixin is not a standalone class, you can just write it to assume that the necessary methods exist, and it will find them on self assuming the self in question provides, or derives from another class which provides, meth1 and meth2.

If you want to ensure the methods exist, you can either document it in the Mixin docstring, or for programmatic enforcement, use the abc module to make Mixin an ABC and specify what methods must be defined; if a given class doesn't provide them (directly or via inheritance) then you'll get an error if you attempt to instantiate it (because the class is still abstract until those methods are defined):

from abc import ABCMeta, abstractmethod

class Mixin(metaclass=ABCMeta):
    def __init__(self):
        pass

    @abstractmethod
    def meth1(self): pass

    @abstractmethod
    def meth2(self): pass

    def meth4(self):
        ...
        self.meth1()  # Method call on self will dispatch to other class's meth1 dynamically
        self.meth2()  # Method call on self will dispatch to other class's meth2 dynamically

Beyond that, you can simplify your code significantly by using super appropriately, which would remove the need to explicitly call the __init__s for each parent class; they'd be called automatically so long as all classes use super appropriately (note: for safety, in cooperative inheritance like this, you usually accept the current class's recognized arguments plus varargs, passing the varargs you don't recognize up the call chain blindly):

class Original_1:
    def __init__(self, orig1arg, *args, **kwargs):
        self.orig1val = orig1arg           # Use what you know
        super().__init__(*args, **kwargs)  # Pass what you don't

    def meth1(self):
        pass

    def meth2(self):
        pass

class Original_2(Original_1):
    def __init__(self, orig2arg, *args, **kwargs):
        self.orig2val = orig2arg                 # Use what you know
        super().__init__(self, *args, **kwargs)  # Pass what you don't

    def meth3(self):
        pass

class Mixin(metaclass=ABCMeta):
    # If Mixin, or any class in your hierarchy, doesn't need to do anything to
    # be initialized, just omit __init__ entirely, and the super from other
    # classes will skip over it entirely
    def __init__(self, mixinarg, *args, **kwargs):
        self.mixinval = mixinarg                 # Use what you know
        super().__init__(self, *args, **kwargs)  # Pass what you don't

    @abstractmethod
    def meth1(self): pass

    @abstractmethod
    def meth2(self): pass

    def meth4(self):
        ...
        self.meth1()  # Method call on self will dispatch to other class's meth1
        self.meth2()  # Method call on self will dispatch to other class's meth1

class NewClass_1(Original_1, Mixin):
    def __init__(self, newarg1, *args, **kwargs):
        self.newval1 = newarg1                   # Use what you know
        super().__init__(self, *args, **kwargs)  # Pass what you don't

class NewClass_2(Original_2, Mixin):
    def __init__(self, newarg2, *args, **kwargs):
        self.newval2 = newarg2                   # Use what you know
        super().__init__(self, *args, **kwargs)  # Pass what you don't

Note that using super everywhere means you don't need to explicitly call each __init__ for your parents; it automatically linearizes the calls, so for example, in NewClass_2, that single super().__init__ will delegate to the first parent (Original_2), which then delegates to Original_1, which then delegates to Mixin (even though Original_1 knows nothing about Mixin).

In more complicated multiple inheritance (say, you inherit from Mixin through two different parent classes that both inherit from it), using super is the only way to handle it reasonably; super naturally linearizes and deduplicates the parent class tree, so even though two parents derive from it, Mixin.__init__ would still only be called once, preventing subtle errors from initializing Mixin more than once.

Note: You didn't specify which version of Python you're using. Metaclasses and super are both better and simpler in Python 3, so I've used Python 3 syntax. For Python 2, you'd need to set the metaclass a different way, and call super providing the current class object and self explicitly, which makes it less nice, but then, Python 2 is generally less nice at this point, so consider writing new code for Python 3?

Upvotes: 1

Blender
Blender

Reputation: 298196

Mixins are used to add functionality (usually methods) to classes by using multiple inheritance.

For example, let's say you want to make a class's __str__ method return everything in uppercase. There are two ways you can do this:

  1. Manually change every single class's __str__ method:

    class SomeClass(SomeBase):
        def __str__(self):
            return super(SomeClass, self).__str__().upper()
    
  2. Create a mixin class that does only this and inherit from it:

    class UpperStrMixin(object):
        def __str__(self):
            return super(UpperStrMixin, self).__str__().upper()
    
    class SomeClass(SomeBase, UpperStrMixin):
        ...
    

In the second example, notice how UpperStrMixin is completely useless as a standalone class. Its only purpose is to be used with multiple inheritance as a base class and to override your class's __str__ method.

In your particular case, the following will work:

class Mixin:
    def __init__(self, option):
        ...

    def meth4(self):
        ...
        self.meth1()
        self.meth2()

class NewClass_1(Original_1, Mixin):
    def __init__(self, option):
        Original_1.__init__(self)
        Mixin.__init__(self, option)
        ...

class NewClass_2(Original_2, Mixin):
    def __init__(self, option):
        Original_2.__init__(self)
        Mixin.__init__(self, option)
        ...

Even though Mixin.meth1 and Mixin.meth2 aren't defined, this isn't an issue because an instance of Mixin is never created directly and it's only used indirectly through multiple inheritance.

Upvotes: 1

Related Questions