在Python中使用MixIns时的钻石问题

6 投票
6 回答
4195 浏览
提问于 2025-04-16 08:56

请看下面这段代码,它实现了一个简单的 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

问题在于 StoryStoryHTMLMixin 都是从 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 个回答

2

编辑:抱歉,我搞错了,这个bug是另一个问题(感谢@Glenn Maynard)。只要你的混入类直接继承自object,下面的技巧还是有效的。

class Text(object): pass
class Story(Text):
    ....

不过,我觉得混入类并不是解决你问题的最佳方法。其他提供的解决方案(类装饰器和普通子类)都清楚地标明了Story类是可以被渲染的,而你的方案却把这个事实隐藏了。也就是说,其他方案中的render方法是明确存在的,而在你的方案中却是隐藏的。我觉得这样会导致以后产生困惑,尤其是当你越来越依赖混入的方法时。

就我个人而言,我更喜欢类装饰器。

5

你为什么不直接使用混入,而要在方法解析顺序(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()

但最终,问题还是来了:为什么类会突然多出一些方法呢?这可是恐怖片的素材呀;-)

4

如果我们去掉__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的同时,去掉Storyobject,但由于一些不太清楚的内部原因,这是不允许的(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()

撰写回答