Проблема с алмазом при использовании MixIns в Python - PullRequest
6 голосов
/ 23 декабря 2010

Пожалуйста, рассмотрите следующий код, реализующий простое 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 или есть лучший способ?

Edit:

Я вижу, что класс UserDict в последнем исходном коде Python определяет MixIn, прибегая к классам старого стиля (как представлено в моем примере).

Как рекомендовано всеми, я могу прибегнуть к переопределению функциональности, которую я хочу достичь (а именно, привязка методов во время выполнения) без использования MixIns. Тем не менее, все еще остается вопрос - является ли это единственным случаем использования, в котором неразбериха с MRO неразрешима, не прибегая к повторной реализации или возвращаясь к классам старого стиля?

Ответы [ 6 ]

5 голосов
/ 23 декабря 2010

Почему бы вам не использовать миксины напрямую вместо взлома в 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 голосов
/ 23 декабря 2010

Проще увидеть, что происходит, если мы исключим магию __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 не знает, как решить эту проблему.

Я не знаю, как обойти это.В принципе, решение будет состоять в том, чтобы удалить object из баз Story одновременно с добавлением к нему StoryHTMLMixin, но это не допускается по несколько непрозрачным внутренним причинам (TypeError: __bases__ assignment: 'StoryHTMLMixin' deallocator differs from 'object').

Я так или иначе никогда не нашел практического, реального применения для модификации классов.Это просто кажется запутанным и запутанным - если вам нужен класс, производный от этих двух классов, просто создайте класс нормально.

Ed:

Вот подход, который делает нечто похожее на ваше,но без изменения классов на месте.Обратите внимание, как он возвращает новый класс, динамически производный от аргументов функции.Это намного понятнее - вы не можете случайно изменить объекты, которые уже созданы, например.

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()
2 голосов
/ 23 декабря 2010

Однако, точка все еще остается - это единственный случай использования, когда возиться с MRO неразрешимо, не прибегая к переопределению или возвращаясь к классам старого стиля?

Другие имеютПредлагаемые лучшие решения - такие как явное построение желаемого класса - но, чтобы ответить на ваш отредактированный вопрос, возможно , чтобы определить миксин, не прибегая к классам старого стиля:

class Base(object): pass

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

class StoryHTMLMixin(Base):
    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,)+TargetClass.__bases__

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

доходность

<html><title>My Life</title><body><p>Is good.</p></body></html>
2 голосов
/ 23 декабря 2010

РЕДАКТИРОВАТЬ: мой плохой, ошибка является отдельной проблемой (спасибо @Glenn Maynard). Следующий хак все еще работает, пока ваш миксин наследует напрямую от object.

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

Однако я думаю, что миксины - это не самое лучшее решение вашей проблемы. Оба предоставленных решения (декоратор класса и обычный подкласс) явно помечают класс Story как визуализируемый, в то время как ваше решение скрывает этот факт. Таким образом, существование метода render в других решениях является явным , в то время как в вашем решении он скрыт. Я думаю, что это приведет к путанице позже, особенно если вы будете больше полагаться на методологию mixin.

Лично мне нравится декоратор класса.

1 голос
/ 23 декабря 2010

Помимо всего сказанного в предыдущих ответах (а также злости и плохой идеи), ваша проблема:

  1. Базы следует заказывать наоборот

    TargetClass.__bases__ = (MixInClass,) + TargetClass.__bases__

  2. http://bugs.python.org/issue672115 - Самый простой обходной путь - унаследовать все от определенного пользователем класса, а не от объекта, как

    class MyBase(object): pass
    
    
    class Story(MyBase):
         ...
    
    
    class StoryHTMLMixin(MyBase):
         ...
    
1 голос
/ 23 декабря 2010

Вы можете попробовать использовать декоратор для добавления функциональности вместо:

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

def renderable(cls):
  cls.render = render
  return cls

@renderable
class Story(object):
  ...
...