La herencia es la primera herramienta a la que casi todos recurren y la que peor envejece. La razón es simple: una subclase hereda todo, incluidas las partes que no le aplican.
El problema del pingüino
# inheritance for reuse: the shape that goes wrong
class Animal:
def __init__(self, name): self.name = name
def speak(self): return '...'
def fly(self): return f"{self.name} flies away"
class Bird(Animal):
def speak(self): return f"{self.name} tweets"
class Penguin(Bird):
pass
p = Penguin('Pingu')
print(p.speak())
print(p.fly()) # inherited, and wrong
Imprime:
Pingu tweets
Pingu flies away
Un pingüino es sin ninguna duda un ave, y la jerarquía está sin ninguna duda mal. No hubo un error de modelado: fly se puso en la clase base porque la mayoría de las aves vuela, cosa que era cierta cuando se escribió.
Los parches habituales lo empeoran. Sobrescribe fly para que lance un error y tendrás una subclase que falla en la interfaz de su padre. Baja fly a una clase FlyingBird y tendrás que hacerlo de nuevo la primera vez que te encuentres con cualquier otra cosa que no vuele.
Composición: la capacidad es un objeto
# composition: the capability is an object, not an ancestor
class Wings:
def fly(self, name): return f"{name} flies away"
class Bird2:
def __init__(self, name, wings=None):
self.name, self.wings = name, wings
def speak(self): return f"{self.name} tweets"
def fly(self):
if self.wings is None:
raise TypeError(f"{self.name} cannot fly")
return self.wings.fly(self.name)
print(Bird2('Robin', Wings()).fly())
try:
Bird2('Pingu').fly()
except TypeError as err:
print(type(err).__name__ + ':', err)
Imprime:
Robin flies away
TypeError: Pingu cannot fly
Volar ahora es algo que un ave tiene, no algo que es. Agregar un ave que no vuela no necesita ninguna clase nueva ni cambiar la jerarquía.
La ganancia está en poder intercambiar comportamiento
El argumento real no es taxonómico. Es que una capacidad compuesta se puede cambiar en tiempo de ejecución y sustituir en una prueba.
# swapping behaviour at runtime is the payoff
class JsonFormat:
def render(self, row): return f'{{"name": "{row}"}}'
class CsvFormat:
def render(self, row): return f'{row}'
class Report:
def __init__(self, fmt): self.fmt = fmt
def emit(self, rows): return [self.fmt.render(r) for r in rows]
print(Report(JsonFormat()).emit(['ada']))
print(Report(CsvFormat()).emit(['ada']))
Imprime:
['{"name": "ada"}']
['ada']
Un solo Report. Con herencia esto serían JsonReport y CsvReport, y un tercer formato significa una tercera clase; más un problema de verdad el día que quieras un reporte que sea los dos.
Las pruebas muestran la diferencia con más claridad que nada. Pásale un formateador falso y ya probaste Report sin tocar JSON. Con herencia tendrías que crear una subclase para interceptarlo.
Cuándo la herencia es lo correcto
Tiene un trabajo, y es más acotado de lo que la gente cree: cuando la relación es de verdad «es un» y la clase base es abstracta, es decir, define una interfaz en vez de traer comportamiento que las subclases quizá no quieran.
# inheritance is right when it is genuinely 'is a' and the base is abstract
class Shape:
def area(self): raise NotImplementedError
def describe(self): return f"{type(self).__name__} with area {self.area():.2f}"
class Circle(Shape):
def __init__(self, r): self.r = r
def area(self): return 3.14159 * self.r ** 2
class Square(Shape):
def __init__(self, s): self.s = s
def area(self): return self.s ** 2
for s in (Circle(1), Square(2)):
print(s.describe())
Imprime:
Circle with area 3.14
Square with area 4.00
Shape promete area y ofrece describe construido sobre ella. Ninguna subclase hereda algo que no debería tener, porque no hay nada concreto que heredar. La parte 9 cubre las ABCs y los Protocols, que hacen que esa promesa se pueda exigir.
Qué recordar
-
La herencia le da a una subclase todo, incluso lo que no debería tener.
-
Recurre a la composición cuando estés heredando para reutilizar código y no para declarar un tipo.
-
El beneficio práctico es poder intercambiar y falsear comportamiento, no la pureza del modelo.
-
La herencia encaja cuando la base es abstracta y la relación es de verdad «es un».
La regla que sobrevive al contacto con código real: si estás escribiendo una subclase para tener acceso a un método, lo que quieres es composición. Si la escribes para prometer una interfaz, la herencia está bien.