Python self и super в множественном наследовании

В Raymond Hettinger говорится: " Super считается супер говорить" в PyCon 2015 он объясняет преимущества использования super в Python в контексте множественного наследования, Это один из примеров, которые использовал Раймонд во время его беседы:

class DoughFactory(object):
    def get_dough(self):
        return 'insecticide treated wheat dough'


class Pizza(DoughFactory):
    def order_pizza(self, *toppings):
        print('Getting dough')
        dough = super().get_dough()
        print('Making pie with %s' % dough)
        for topping in toppings:
            print('Adding: %s' % topping)


class OrganicDoughFactory(DoughFactory):
    def get_dough(self):
        return 'pure untreated wheat dough'


class OrganicPizza(Pizza, OrganicDoughFactory):
    pass


if __name__ == '__main__':
    OrganicPizza().order_pizza('Sausage', 'Mushroom')

Кто-то в аудитории спросил Раймонда о разнице в использовании self.get_dough() вместо super().get_dough(). Я не очень хорошо понял краткий ответ Раймонда, но я закодировал две реализации этого примера, чтобы увидеть различия. Выходные данные одинаковы для обоих случаев:

Getting dough
Making pie with pure untreated wheat dough
Adding: Sausage
Adding: Mushroom

Если вы измените порядок классов от OrganicPizza(Pizza, OrganicDoughFactory) до OrganicPizza(OrganicDoughFactory, Pizza) с помощью self.get_dough(), вы получите следующий результат:

Making pie with pure untreated wheat dough

Однако, если вы используете super().get_dough(), это будет выход:

Making pie with insecticide treated wheat dough

Я понимаю поведение super(), как объяснил Раймонд. Но каково ожидаемое поведение self в сценарии множественного наследования?

Ответы

Ответ 1

Чтобы уточнить, существует четыре случая, основанных на изменении второй строки в Pizza.order_pizza и определении OrganicPizza:

  • super(), (Pizza, OrganicDoughFactory) (оригинал): 'Making pie with pure untreated wheat dough'
  • self, (Pizza, OrganicDoughFactory): 'Making pie with pure untreated wheat dough'
  • super(), (OrganicDoughFactory, Pizza): 'Making pie with insecticide treated wheat dough'
  • self, (OrganicDoughFactory, Pizza): 'Making pie with pure untreated wheat dough'

Случай 3. удивляет вас; если мы переключим порядок наследования, но все-таки используем super, мы, по-видимому, вызываем оригинальный DoughFactory.get_dough.


Что super действительно делает, это спрашивать ", который следующий в MRO (порядок разрешения метода)?" Итак, как выглядит OrganicPizza.mro()?

  • (Pizza, OrganicDoughFactory): [<class '__main__.OrganicPizza'>, <class '__main__.Pizza'>, <class '__main__.OrganicDoughFactory'>, <class '__main__.DoughFactory'>, <class 'object'>]
  • (OrganicDoughFactory, Pizza): [<class '__main__.OrganicPizza'>, <class '__main__.OrganicDoughFactory'>, <class '__main__.Pizza'>, <class '__main__.DoughFactory'>, <class 'object'>]

Ключевым вопросом здесь является: что происходит после Pizza? Поскольку мы вызываем super изнутри Pizza, то здесь Python будет искать get_dough *. Для 1 и 2. это OrganicDoughFactory, поэтому мы получаем чистое необработанное тесто, но для 3. и 4. это оригинальная обработанная инсектицидом DoughFactory.


Почему self отличается, тогда? self всегда является экземпляром, поэтому Python ищет get_dough с самого начала MRO. В обоих случаях, как показано выше, OrganicDoughFactory находится ранее в списке, чем DoughFactory, поэтому версии self всегда получают необработанное тесто; self.get_dough всегда разрешается до OrganicDoughFactory.get_dough(self).


* Я думаю, что это на самом деле яснее в форме двух аргументов super, используемой в Python 2.x, которая была бы super(Pizza, self).get_dough(); первым аргументом является класс для пропуска (т.е. Python смотрит в остальную часть MRO после этого класса).

Ответ 2

Я хотел бы поделиться несколькими замечаниями по этому поводу.

Вызов self.get_dough() может быть невозможным, если вы переопределяете метод get_dough() родительского класса, например здесь:

class AbdullahStore(DoughFactory):
    def get_dough(self):
        return 'Abdullah`s special ' + super().get_dough()

Я думаю, что это частой сценарий на практике. Если мы вызываем DoughFactory.get_dough(self) непосредственно, то поведение фиксировано. Класс, получающий AbdullahStore, должен был бы переопределить полный метод и не может повторно использовать "добавленную стоимость" AbdullahStore. С другой стороны, если мы используем super.get_dough(self), это имеет аромат шаблона: в любом классе, полученном из AbdullahStore, скажем

class Kebab(AbdullahStore):
    def order_kebab(self, sauce):
        dough = self.get_dough()
        print('Making kebab with %s and %s sauce' % (dough, sauce))

мы можем "создать экземпляр" get_dough(), используемый в AbdullahStore по-разному, перехватывая его в MRO, как это

class OrganicKebab(Kebab, OrganicDoughFactory):pass

Вот что он делает:

Kebab().order_kebab('spicy')
Making kebab with Abdullah`s special insecticide treated wheat dough and spicy sauce
OrganicKebab().order_kebab('spicy')
Making kebab with Abdullah`s special pure untreated wheat dough and spicy sauce

Так как OrganicDoughFactory имеет один родительский DoughFactory, я гарантированно будет вставлен в MRO прямо перед DoughFactory и, таким образом, переопределяет его методы для всех предыдущих классов в MRO. Мне потребовалось некоторое время, чтобы понять алгоритм линеаризации C3, используемый для построения MRO. Проблема в том, что два правила

children come before parents
parents order is preserved

из этой справки https://rhettinger.wordpress.com/2011/05/26/super-considered-super/ еще не определили порядок однозначно. В иерархии классов

D->C->B->A
 \      /
   --E--

(класс A, класс B (A), класс C (B), класс E (A), класс D (C, E)), где E будет вставлен в MRO? Это DCBEA или DCEBA? Возможно, прежде чем можно с уверенностью ответить на подобные вопросы, не стоит начинать вставлять super всюду. Я все еще не совсем уверен, но я думаю, что линеаризация C3, которая однозначна и выберет упорядочивающий DCBEA в этом примере, делает позволяют нам делать трюк с перехватом так, как мы это делали, недвусмысленно.

Теперь, я полагаю, вы можете предсказать результат

class KebabNPizza(Kebab, OrganicPizza): pass
KebabNPizza().order_kebab('hot')

который является улучшенным кебабом:

Making kebab with Abdullah`s special pure untreated wheat dough and hot sauce

Но, вероятно, вам понадобилось некоторое время для расчета.

Когда я впервые посмотрел на super docs https://docs.python.org/3.5/library/functions.html?highlight=super#super слабый назад, исходя из фона С++, это было похоже на "wow, ок здесь правила, но как это может когда-либо работать, а не заглушить вас сзади?". Теперь я больше об этом понимаю, но все же не желаю вставлять super всюду. Я думаю, что большая часть кодовой базы, которую я видел, делает это только потому, что super() удобнее вводить, чем имя базового класса. И это даже не говорит об экстремальном использовании super() в цепочке функций __init__. То, что я наблюдаю на практике, состоит в том, что все пишут конструкторы с сигнатурой, которая удобна для класса (а не универсального) и использует super() для вызова того, что, по их мнению, является их конструктором базового класса.