Python self y super en herencia múltiple.

En la charla de Raymond Hettinger, ” Super considerado súper habla ” en PyCon 2015, explica las ventajas de usar super en Python en el contexto de herencia múltiple. Este es uno de los ejemplos que Raymond utilizó durante su charla:

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

Alguien en la audiencia le preguntó a Raymond sobre la diferencia de usar self.get_dough() lugar de super().get_dough() . No entendí muy bien la breve respuesta de Raymond, pero codifiqué las dos implementaciones de este ejemplo para ver las diferencias. La salida es la misma para ambos casos:

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

Si modifica el orden de clase de OrganicPizza(Pizza, OrganicDoughFactory) a OrganicPizza(OrganicDoughFactory, Pizza) usando self.get_dough() , obtendrá este resultado:

Making pie with pure untreated wheat dough

Sin embargo, si usas super().get_dough() esta es la salida:

Making pie with insecticide treated wheat dough

Entiendo el comportamiento super() como Raymond explicó. Pero, ¿cuál es el comportamiento esperado del self en un escenario de herencia múltiple?

Solo para aclarar, hay cuatro casos, basados ​​en el cambio de la segunda línea en Pizza.order_pizza y la definición de OrganicPizza :

  1. super() , (Pizza, OrganicDoughFactory) (original) : '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'

Caso 3. es el que te ha sorprendido; Si cambiamos el orden de herencia pero aún usamos super , aparentemente terminamos llamando al DoughFactory.get_dough original.


Lo que realmente hace super es preguntar “¿cuál es el siguiente en el MRO (orden de resolución de métodos)?” Entonces, ¿qué OrganicPizza.mro() tiene OrganicPizza.mro() ?

  • (Pizza, OrganicDoughFactory) : [, , , , ]
  • (OrganicDoughFactory, Pizza) : [, , , , ]

La pregunta crucial aquí es: ¿qué viene después de la Pizza ? Como llamamos super desde dentro de Pizza , ahí es donde Python irá a buscar get_dough *. Para 1. y 2. es OrganicDoughFactory , así que obtenemos la masa pura y sin tratar, pero para 3. y 4. es la DoughFactory original, tratada con DoughFactory .


¿Por qué el self es diferente, entonces? self es siempre la instancia , por lo que Python busca get_dough desde el inicio del MRO. En ambos casos, como se muestra arriba, OrganicDoughFactory está más temprano en la lista que DoughFactory , por lo que las versiones self siempre reciben una masa no tratada; self.get_dough siempre se resuelve en OrganicDoughFactory.get_dough(self) .


* Creo que esto es en realidad más claro en la forma de dos argumentos de super utilizado en Python 2.x, que sería super(Pizza, self).get_dough() ; el primer argumento es la clase a omitir (es decir, Python busca en el rest de la MRO después de esa clase).

Me gustaría compartir algunas observaciones sobre esto.

Puede que no sea posible llamar a self.get_dough() si está anulando el método get_dough() de la clase padre, como aquí:

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

Creo que este es un escenario frecuente en la práctica. Si llamamos a DoughFactory.get_dough(self) directamente, entonces el comportamiento es fijo. Una clase que derive AbdullahStore tendría que anular el método completo y no puede reutilizar el “valor agregado” de AbdullahStore . Por otro lado, si usamos super.get_dough(self) , esto tiene el sabor de una plantilla: en cualquier clase derivada de AbdullahStore , digamos

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

podemos ‘instanciar’ get_dough() usado en AbdullahStore diferente, al interceptarlo en MRO de esta manera

 class OrganicKebab(Kebab, OrganicDoughFactory):pass 

Esto es lo que hace:

 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 

Dado que OrganicDoughFactory tiene un solo DoughFactory principal, I se garantiza que se inserte en MRO justo antes de DoughFactory y, por lo tanto, reemplaza sus métodos para todas las clases anteriores en MRO. Me tomó algo de tiempo entender el algoritmo de linealización C3 usado para construir el MRO. El problema es que las dos reglas

 children come before parents parents order is preserved 

a partir de esta referencia https://rhettinger.wordpress.com/2011/05/26/super-considered-super/ todavía no defina el ordenamiento de forma inequívoca. En la jerarquía de clases.

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

(clase A; clase B (A); clase C (B); clase E (A); clase D (C, E)) donde se insertará E en MRO? ¿Es DCBEA o DCEBA? Quizás antes de poder responder con confianza a preguntas como esta, no es una buena idea comenzar a insertar super todas partes. Todavía no estoy completamente seguro, pero creo que la linealización de C3, que no es ambigua y elegiré el DCBEA de pedido en este ejemplo, nos permite hacer el truco de intercepción de la misma manera que lo hicimos, sin ambigüedad.

Ahora, supongo que puedes predecir el resultado de

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

que es un kebab mejorado:

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

Pero probablemente te haya tomado algo de tiempo calcularlo.

Cuando miré por primera vez a los super documentos https://docs.python.org/3.5/library/functions.html?highlight=super#super hace un momento débil, proveniente de C ++ background, fue como “wow, ok, aquí están los reglas, pero ¿cómo esto puede funcionar y no apalearte en la espalda? “. Ahora comprendo más al respecto, pero aún me resisto a insertar super todas partes. Creo que la mayor parte del código base que he visto lo está haciendo solo porque super() es más cómodo de escribir que el nombre de la clase base. Y esto incluso no habla del uso extremo de super() para encadenar las funciones __init__ . Lo que observo en la práctica es que todos escriben constructores con la firma que es conveniente para la clase (y no la universal) y usan super() para llamar a lo que creen que es su constructor de clase base.