A lo largo del curso, hemos visto ejemplos de jerarquías de clases. Quizá la más reciente sea la jerarquía de excepciones:
assert issubclass(ModuleNotFoundError, ImportError)
assert issubclass(ImportError, Exception)
assert issubclass(Exception, BaseException)
assert issubclass(BaseException, object)
La herencia es un mecanismo de abstracción y reutilización de código. Tiene sentido cuando podemos respetar el principio de sustitución de Liskov (la "L" de "SOLID" que estudiamos en el bloque 2). Es fácilmente implementable en valores inmutables (como las excepciones) y establece una relación "es un/una".
Las instancias de una clase, son instancias también de todas las clases base del tipo de la instancia.
exception = ModuleNotFoundError()
assert isinstance(exception, ModuleNotFoundError)
assert isinstance(exception, ImportError)
assert isinstance(exception, Exception)
assert isinstance(exception, BaseException)
assert isinstance(exception, object)
La raíz de toda la jerarquía de Python es object
:
issubclass(object, object) # a class is always subclass of itself!
issubclass(type, object)
issubclass(BaseException, object)
La herencia permite la reutilización de código si respetamos el principio de Liskov estudiado en el bloque 2. Si necesitamos reutilizar código pero no podemos respetar el principio de Liskov, es mejor usar composición.
Por defecto, cualquier nuevo tipo hereda de object
:
class Duck:
...
issubclass(Duck, object)
Para indicar explícitamente de qué tipo hereda una nueva clase utilizaremos paréntesis tras el nombre de la clase:
class Animal:
noise = '...'
def say_something(self):
print(self.noise + '!')
# Duck inherits from Animal
class Duck(Animal):
noise = 'quack'
little_duck = Duck()
assert issubclass(Duck, Animal)
assert isinstance(little_duck, Duck)
assert isinstance(little_duck, Animal)
-
Las clases bases de un tipo (aquellas de las que hereda el tipo) pueden consultarse en:
Duck.__bases__ Duck.__bases__[0].__bases__
-
Hablamos en plural porque Python soporta multiherencia y una clase puede heredar de varias:
class Mortal: def die(self): print('X..X') class Duck(Animal, Mortal): noise = 'quack' little_duck = Duck() little_duck.say_something() little_duck.die()
Considera el ejemplo anterior completo:
class Animal:
noise = '...'
def say_something(self):
print(self.noise + '!')
class Mortal:
def die(self):
print('X..X')
class Duck(Animal, Mortal):
noise = 'quack'
little_duck = Duck()
little_duck.noise # `noise` is in the Duck class
little_duck.say_something() # `say_something` is in the Animal class
little_duck.die() # `die` is in the Mortal class
Fíjate como la instancia little_duck
"hereda" los métodos de las clases
base:
dir(little_duck)
-
Una subclase puede sobreescribir un método o atributo de una clase base:
class ScreamingDuck(Duck): def say_something(self): print(self.noise.uppercase() + '!')
El nuevo comportamiento es específico a las instancias de esa clase:
intense_duck = ScreamingDuck() intense_duck.say_something() regular_duck = Duck() regular_duck.say_something()
-
Un subtipo puede extender los atributos del supertipo:
class NamedDuck(Duck): def __init__(self, name): self.name = name dutch_duck = NamedDuck('Alfre J. Kwak') dutch_duck.name
De nuevo, estos atributos son específicos de las instancias de esta clase:
regular_duck = Duck() regular_duck.name
-
Un subtipo puede acceder a los atributos de la clase base a través del objeto devuelto por la clase
super
:class VerboseDuck(Duck): def say_something(self): for _ in range(10): super().say_something() talking_duck = VerboseDuck() talking_duck.say_something()
Sí,
super
es una clase:type(super) super
-
Si una subclase implementa
__init__
, la llamada asuper().__init__
resulta casi obligatoria, para garantizar la correcta inicialización del objeto:class Profile: def __init__(self, name, surname): self.name = name self.surname = surname class RichProfile(Profile): def __init__(self, name, surname, picture_path): super().__init__(name, surname) self.picture_path = picture_file salva = RichProfile('Salva', 'de la Puente', 'imgs/portrait.jpg') salva.name salva.surname salva.picture_path
¿Qué hubiera pasado de no llamar a
super().__init__
?
Considera la siguiente jerarquía, llamada "jerarquía en diamante", por la forma del grafo de dependencias:
A
/ \
/ \
P Q
\ /
\ /
Z
class A:
def where_am_i(self):
print('This is A')
class P(A):
def where_am_i(self):
print('This is P')
class Q(A):
def where_am_i(self):
print('This is Q')
class Z(P, Q):
def where_am_i(self):
print('This is Z')
z = Z()
z.where_am_i()
-
Forzar a que se ejecute el método de una clase concreta consiste en llamar al método de esa clase en particular:
class Z(P, Q): def where_am_i(self): A.where_am_i(self) z = Z() z.where_am_i()
Observa que llamar al método desde la clase require pasar la instancia expícitamente. ¿Por qué?
-
Cambia la definición de
Z
para que no contenga el métodowhere_am_i
:class Z(P, Q): pass z = Z() z.where_am_i()
La ejecución de
z.where_am_i()
muestra que estamos usando el de la claseP
. -
Haz los mismo eliminando el cuerpo de la clase
P
también:class P(A): pass class Z(P, Q): pass z = Z() z.where_am_i()
¿Te lo esperabas?
-
La secuencia de búsqueda para una instancia se encuentra en el atributo
__mro__
de la clase del objeto. MRO son las siglas de Method Resolution Order aunque también funciona con atributos.Z.__mro__ is type(z).__mro__ type(z).__mro__
En realidad, llamar a super
en un método de instancia como en:
class Z:
def where_am_i(self):
super().where_am_i()
Es equivalente a llamar a super
de la siguiente forma:
class Z:
def where_am_i(self):
super(__class__, self).where_am_i()
La variable __class__
es una variable especial que contiene la clase en
la que la función está definida.
Ese super
viene a significar "interpreta self
como si se tratara de una
instancia de la clase que sigue a __class__
—de ahí el "super"—
en el MRO de type(self)
".
Para calcular el orden de búsqueda, Python no sólo tiene en cuenta el arbol de herencia, como el diamante que veíamos al comienzo de la charla sinon que, además, también tiene en cuenta el orden en el que se declararon las clases base cuando se definió la clase.
No todas las jerarquías son susceptibles de ser linearizadas. Para que la linearización sea posible tiene que existir un orden en el que:
- Las subclases precedan a las superclases.
- El orden de búsqueda respete el orden en el que se declararon las clases base.
Si no existe ningún orden que satisfaga estas propiedades, el grafo no se puede linearizar.
La linearización es una definición recursiva. Comienza así:
-
La linearización de
object
es la lista[object]
. -
La linearización de una clase
C
con bases[B1, ..., BN]
es:linear(C) = [C] + merge(linear(B1), ..., linear(B2), [B1, ..., BN])
-
La mezcla
merge
se resuelve así:- Crea una lista vacía
mix
. - Considera la cabeza de la primera lista de la mezcla.
- Si este elemento no es parte de la cola de ninguna otra lista
del
merge
, añádelo amix
y elimínalo de todas las listas. Repite hasta que sólo quede mezclar listas vacías, en cuyo caso has terminado. - Si el elemento está en alguna cola, haz lo mismo con la cabeza de
la siguente lista del
merge
. Si no hay más listas, la linearización falla.
- Si este elemento no es parte de la cola de ninguna otra lista
del
- Devuelve la lista
mix
que contiene la mezcla.
- Crea una lista vacía
Trata de resolver las linearizaciones que se plantean en el primer enlace:
- The Python 2.3 method resolution order*
- Method Resolution Order at Python History