2.22. Herencia

La herencia permite que una clase extienda otra clase: parte de todos sus métodos y atributos, y luego añade o redefine lo que es diferente. La original se llama la base (o padre); la extensión se llama la subclase.

Una clase base Shape que apunta hacia abajo a una subclase Square; Square redefine un método (area) y hereda el resto.

Square reutiliza todos los métodos de Shape y redefine los que necesita especializar.

2.22.1. Definir una subclase

La clase base va entre los paréntesis de la línea class:

class Shape:
    def __init__(self, name):
        self.name = name

    def describe(self):
        return self.name + " with area " + str(self.area())

    def area(self):
        return 0

class Square(Shape):
    def __init__(self, side):
        super().__init__("square")
        self.side = side

    def area(self):
        return self.side * self.side

s = Square(4)
print(s.describe())

Salida:

square with area 16

Square hereda describe de Shape sin cambios y redefine area para que devuelva un valor real. El describe de la base sigue funcionando porque llama a self.area(), que se resuelve a la redefinición en tiempo de ejecución.

2.22.2. super()

super() devuelve un proxy que permite a un método llamar a la versión de la clase base de un método. El uso más común es llamar al __init__ de la base desde el __init__ de una subclase para que la base pueda inicializar sus propios atributos:

class Square(Shape):
    def __init__(self, side):
        super().__init__("square")    # run Shape.__init__
        self.side = side

Olvidar super().__init__ es una fuente habitual de errores: los atributos que la base habría establecido nunca se establecen, y los métodos heredados que dependen de ellos fallan más adelante.

2.22.2.1. La clase base implícita

Toda clase hereda implícitamente de object, la raíz de la jerarquía de tipos de Python. Métodos como __repr__, __eq__ y toda la maquinaria que hay detrás del acceso a atributos provienen de ahí, incluso cuando una clase no declara ninguna base. Escribir class Foo(object): explícitamente y escribir class Foo: son equivalentes en el Python moderno; lo segundo es la forma convencional.

2.22.3. Cuándo ayuda la herencia, y cuándo no

Usa la herencia cuando una clase sea genuinamente un tipo más específico de otra, compartiendo la mayor parte del comportamiento. La prueba clásica es la comprobación «es-un»: un Square es una Shape.

Cuando dos clases simplemente comparten unos cuantos auxiliares, la herencia es excesiva. Recurre en su lugar a la composición: haz que una clase contenga una instancia de la otra como atributo y la use a través de ese atributo. La forma es «tiene-un», no «es-un»:

class Logger:
    def log(self, msg):
        print("[log]", msg)

# inheritance: Worker IS-A Logger
class WorkerInherits(Logger):
    def run(self):
        self.log("starting")

# composition: Worker HAS-A Logger
class WorkerComposes:
    def __init__(self):
        self.logger = Logger()

    def run(self):
        self.logger.log("starting")

Ambas funcionan. La versión que compone suele ser mejor porque:

  • Mantiene las interfaces pequeñas. WorkerComposes expone únicamente run y logger. WorkerInherits también expone log: quien llama puede escribir worker.log(...) directamente, fuera intencionado o no.

  • Desacopla los tiempos de vida. El logger puede intercambiarse, compartirse entre workers o construirse de forma distinta para cada worker, todo ello sin tocar la clase Worker. Una subclase está atornillada a su base.

  • Evita las colisiones de nombres de método. Dos bases que ambas definen run chocan cuando una clase intenta heredar de ambas; dos atributos nunca chocan.

Una regla práctica:

  • Usa la herencia cuando la subclase realmente sea un tipo de la base y todos los métodos de la base tengan sentido también para ella: Square es una Shape; MemoryError es una Exception.

  • Usa la composición para todo lo demás, cuando solo quieras el comportamiento de otra clase, no su identidad. La mayoría del código real usa la composición mucho más que la herencia.