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?
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()