在Python中使用MixIns时的钻石问题
请看下面这段代码,它实现了一个简单的 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()
运行 main
时会出现以下错误:
TypeError: Cannot create a consistent method resolution
order (MRO) for bases object, StoryHTMLMixin
问题在于 Story
和 StoryHTMLMixin
都是从 object
这个类派生出来的,这就引发了一个叫做 菱形问题。
解决这个问题的方法很简单,就是把 StoryHTMLMixin
改成一个 旧式类,也就是说,去掉对 object
的继承。这样,我们就可以把 StoryHTMLMixin
的定义改成:
class StoryHTMLMixin:
def render(self):
return ("<html><title>%s</title>"
"<body>%s</body></html>"
% (self.name, self.content))
这样在运行 main
时就会得到以下结果:
<html><title>My Life</title><body><p>Is good.</p></body></html>
我不喜欢使用 旧式类,所以我想问:
在 Python 中,这样处理这个问题是正确的吗?还是有更好的方法?
编辑:
我发现 UserDict
类在 最新的 Python 源代码 中也使用了旧式类来定义一个 MixIn(就像我例子中的那样)。
正如大家所建议的,我可以考虑重新定义我想要实现的功能(也就是在运行时绑定方法),而不使用 MixIn。不过,问题仍然存在——这是唯一一个在处理方法解析顺序(MRO)时,无法解决而必须回退到旧式类的情况吗?
6 个回答
编辑:抱歉,我搞错了,这个bug是另一个问题(感谢@Glenn Maynard)。只要你的混入类直接继承自object
,下面的技巧还是有效的。
class Text(object): pass
class Story(Text):
....
不过,我觉得混入类并不是解决你问题的最佳方法。其他提供的解决方案(类装饰器和普通子类)都清楚地标明了Story
类是可以被渲染的,而你的方案却把这个事实隐藏了。也就是说,其他方案中的render
方法是明确存在的,而在你的方案中却是隐藏的。我觉得这样会导致以后产生困惑,尤其是当你越来越依赖混入的方法时。
就我个人而言,我更喜欢类装饰器。
你为什么不直接使用混入,而要在方法解析顺序(mro)上搞一些小动作呢?
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))
class StoryHTML(Story, StoryHTMLMixin):
pass
print StoryHTML('asd','asdf').render() # works just fine
如果你真的、真的、真的想给一个类添加额外的方法,这其实不是什么大问题。嗯,除了这其实是个坏主意和不好的做法。不过,你随时都可以改变一个类:
# first, the story
x = Story('asd','asdf')
# changes a class object
def stick_methods_to_class( cls, *methods):
for m in methods:
setattr(cls, m.__name__, m)
# a bare method to glue on
def render(self):
return ("<html><title>%s</title>"
"<body>%s</body></html>"
% (self.name, self.content))
# change the class object
stick_methods_to_class(Story, render)
print x.render()
但最终,问题还是来了:为什么类会突然多出一些方法呢?这可是恐怖片的素材呀;-)
如果我们去掉__bases__
这个神奇的东西,直接写出你要创建的类,会更容易理解发生了什么:
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
这就是你所做的事情的最终结果——或者说如果成功的话会是这样的结果。结果还是会出现同样的错误。
注意,这其实并不是所谓的“钻石继承”。钻石继承涉及四个类,其中两个基类各自继承一个共同的第四个类;而Python的多重继承是可以处理这种情况的。
在这里,你只有三个类,导致的继承关系看起来是这样的:
StoryHTMLMixin <--- Story
| _____/
| |
v v
object
Python不知道该如何解决这个问题。
我不知道有什么解决办法。原则上,解决方案是在你添加StoryHTMLMixin
的同时,去掉Story
的object
,但由于一些不太清楚的内部原因,这是不允许的(TypeError: __bases__ assignment: 'StoryHTMLMixin' deallocator differs from 'object'
)。
我从来没有找到过在实际应用中修改类的好处。这看起来只是让事情变得复杂和困惑——如果你想要一个从这两个类派生的类,直接正常创建一个类就好了。
补充:
这里有一种方法,做的事情和你类似,但不直接修改类。注意它是如何返回一个新类,动态地从函数的参数中派生出来的。这要清晰得多——比如你不会不小心修改已经实例化的对象。
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()