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()
для вызова того, что, по их мнению, является их конструктором базового класса.