Search code examples
pythonmultiple-inheritancemixinsdiamond-problem

Diamond problem when using MixIns in Python


Please consider the following code implementing a simple MixIn:

class Story(object):
    def __init__(self, name, content):  
     self.name = name
     self.content = content    

class StoryHTMLMixin(object):
    def render(self):
     return ("<html><title>%s</title>"
         "<body>%s</body></html>"
         % (self.name, self.content))

def MixIn(TargetClass, MixInClass):
    if MixInClass not in TargetClass.__bases__:
     TargetClass.__bases__ += (MixInClass,)

if __name__ == "__main__":
    my_story = Story("My Life", "<p>Is good.</p>")
    # plug-in the MixIn here
    MixIn(Story, StoryHTMLMixin)
    # now I can render the story as HTML
    print my_story.render()

Running main leads to the following error:

TypeError: Cannot create a consistent method resolution
order (MRO) for bases object, StoryHTMLMixin

The problem is that both Story and StoryHTMLMixin are derived from object, and the diamond problem arises.

The solution is simply to make StoryHTMLMixin an old-style class, i.e., remove the inheritance from object, thus, changing the definition of the class StoryHTMLMixin to:

class StoryHTMLMixin:
    def render(self):
     return ("<html><title>%s</title>"
         "<body>%s</body></html>"
         % (self.name, self.content))

leads to the following result when running main:

<html><title>My Life</title><body><p>Is good.</p></body></html>

I don't like having to use old style classes, so my question is:

Is this the correct way to handle this problem in Python, or is there a better way?

Edit:

I see that the class UserDict in the latest Python source defines a MixIn resorting to the old style classes (as presented in my example).

As recommended by all, I may resort to redefining the functionality that I want to attain (namely, the binding of methods at run time) without using MixIns. However, the point still remains - is this the only use case where messing with the MRO is unsolvable without resorting to reimplementation or falling back to old-style classes?


Solution

  • It's easier to see what's happening if we eliminate the __bases__ magic and write our the classes you're creating explicitly:

    class StoryHTMLMixin(object):
        def render(self):
            return ("<html><title>%s</title>"
                "<body>%s</body></html>"
                % (self.name, self.content))
    
    class Story(object, StoryHTMLMixin):
        def __init__(self, name, content):
            self.name = name
            self.content = content
    

    That's the end result of what you're doing--or what it would be if it succeeded. It results in the same error.

    Notice that this isn't actually diamond inheritance. That involves four classes, where two base classes each inherit a common fourth class; Python's multiple inheritance deals with that.

    Here you have just three classes, leading to inheritance that looks like this:

    StoryHTMLMixin <--- Story
                |   _____/
                |  |
                v  v
               object
    

    Python doesn't know how to resolve this.

    I don't know about a workaround for this. In principle the solution would be to remove object from the bases of Story at the same time you add StoryHTMLMixin to it, but that isn't allowed for somewhat opaque, internal reasons (TypeError: __bases__ assignment: 'StoryHTMLMixin' deallocator differs from 'object').

    I've never found any practical, real-world use for modifying classes like this anyway. It just seems obfuscated and confusing--if you want a class that derives from those two classes, just create a class normally.

    Ed:

    Here's an approach that does something similar to yours, but without modifying classes in-place. Note how it returns a new class, deriving dynamically from the arguments of the function. This is much clearer--you can't inadvertently modify objects that are already instantiated, for example.

    class Story(object):
        def __init__(self, name, content):
            self.name = name
            self.content = content
    
    class StoryHTMLMixin(object):
        def render(self):
            return ("<html><title>%s</title>"
                "<body>%s</body></html>"
                % (self.name, self.content))
    
    def MixIn(TargetClass, MixInClass, name=None):
        if name is None:
            name = "mixed_%s_with_%s" % (TargetClass.__name__, MixInClass.__name__)
    
        class CombinedClass(TargetClass, MixInClass):
            pass
    
        CombinedClass.__name__ = name
        return CombinedClass
    
    if __name__ == "__main__":
        MixedStory = MixIn(Story, StoryHTMLMixin, "MixedStory")
        my_story = MixedStory("My Life", "<p>Is good.</p>")
        print my_story.render()