Python self и 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 в сценарии множественного наследования?


person Marc Tudurí    schedule 04.05.2015    source источник
comment
что за пицца унаследовала от фабрики по производству теста? Этот код странный.   -  person user2357112 supports Monica    schedule 05.05.2015
comment
self ведет себя точно так же, как и любая другая ссылка на объект. Если вы делаете p = OrganicPizza(), то self.get_dough() в любом методе p эквивалентно p.get_dough(), независимо от того, где в графе наследования происходит использование self. Вы понимаете, что сделал бы p.get_dough()?   -  person user2357112 supports Monica    schedule 05.05.2015
comment
@ user2357112 почему странно? Это всего лишь простой пример, который Рэймонд предложил в своем выступлении.   -  person Marc Tudurí    schedule 05.05.2015
comment
@MarcTudurí Я думаю, что они имеют в виду с точки зрения реального объектного моделирования - было бы разумнее назвать это Pizzeria/OrganicPizzeriaDoughFactory все еще mix-in!)   -  person jonrsharpe    schedule 05.05.2015


Ответы (2)


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

  1. super(), (Pizza, OrganicDoughFactory) (исходное): 'Making pie with pure untreated wheat dough'
  2. self, (Pizza, OrganicDoughFactory): 'Making pie with pure untreated wheat dough'
  3. super(), (OrganicDoughFactory, Pizza): 'Making pie with insecticide treated wheat dough'
  4. 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 после этого класса).

person jonrsharpe    schedule 04.05.2015

Хочу поделиться некоторыми наблюдениями по этому поводу.

Вызов 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 документов https://docs.python.org/3.5/library/functions.html?highlight=super#super слабый назад, исходящий из опыта работы с C++, это было похоже на «вау, хорошо, вот правила, но как это может когда-либо работать, а не ударить тебя в спину?». Теперь я больше разбираюсь в этом, но все равно неохота везде вставлять super. Я думаю, что большая часть кодовой базы, которую я видел, делает это только потому, что super() удобнее набирать, чем имя базового класса. И это даже не говоря об экстремальном использовании super() в цепочке __init__ функций. Что я наблюдаю на практике, так это то, что все пишут конструкторы с удобной для класса (а не универсальной) сигнатурой и используют super() для вызова того, что они считают своим конструктором базового класса.

person Alexander Shekhovtsov    schedule 02.12.2017