Skip to content

Latest commit

 

History

History
378 lines (272 loc) · 9.3 KB

File metadata and controls

378 lines (272 loc) · 9.3 KB

Herencia y linearización de jerarquías

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.

Herencia en la sintáxis class

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)
  1. Las clases bases de un tipo (aquellas de las que hereda el tipo) pueden consultarse en:

    Duck.__bases__
    Duck.__bases__[0].__bases__
  2. 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()

Reutilización de código

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)
  1. 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()
  2. 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

Reutilización de código con super

  1. 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
  2. Si una subclase implementa __init__, la llamada a super().__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__?

Multiherencia

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()
  1. 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é?

  2. Cambia la definición de Z para que no contenga el método where_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 clase P.

  3. 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?

  4. 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__

La clase super y su relación con el 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)".

El algoritmo de linearización de Python

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:

  1. Las subclases precedan a las superclases.
  2. 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í:

  1. La linearización de object es la lista [object].

  2. La linearización de una clase C con bases [B1, ..., BN] es:

    linear(C) = [C] + merge(linear(B1), ..., linear(B2), [B1, ..., BN])
    
  3. La mezcla merge se resuelve así:

    1. Crea una lista vacía mix.
    2. Considera la cabeza de la primera lista de la mezcla.
      1. Si este elemento no es parte de la cola de ninguna otra lista del merge, añádelo a mix y elimínalo de todas las listas. Repite hasta que sólo quede mezclar listas vacías, en cuyo caso has terminado.
      2. 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.
    3. Devuelve la lista mix que contiene la mezcla.

Trata de resolver las linearizaciones que se plantean en el primer enlace: