Una clase construida sobre otra clase
La herencia te permite definir una clase nueva como una extensión de otra que ya existe. La clase existente es el padre (también llamada clase base o superclase), y la nueva es la hija (subclase). La hija obtiene todos los métodos y atributos de clase del padre sin repetirlos, y puede añadir los suyos o reemplazar los que deban comportarse de otra forma.
El padre se declara entre paréntesis después del nombre de la clase:
Salida:
Rex is an animal
Woof
Dog no tiene __init__ ni describe propios. Cuando llamas a Dog("Rex"), Python busca __init__ en Dog, no lo encuentra y usa el de Animal. La misma búsqueda ocurre con rex.describe(). Con rex.speak() encuentra speak primero en Dog y se detiene ahí.
Ese orden de búsqueda es todo el mecanismo: Python busca en la propia clase del objeto, luego en el padre, luego en el padre del padre, y usa la primera coincidencia. Si las clases son nuevas para ti, la página de clases cubre antes __init__, self y los métodos.
Sobrescribir un método
Un método de la hija con el mismo nombre que uno del padre lo reemplaza para las instancias de la hija. Esto se llama sobrescribir. Cada subclase puede responder a la misma llamada a su manera:
Salida:
Rectangle with area 12
Square with area 25
Shape with area 0
summary se define una sola vez, en Shape, pero llama a self.area(). Como self es el objeto real, un Rectangle ejecuta Rectangle.area y un Square encuentra area en su padre Rectangle. El código del padre llama al método de la hija sin saber que la hija existe. Esto es lo que hace útil la herencia para compartir un flujo común y dejar que cada subclase complete los detalles.
Llamar al padre con super()
Cuando una hija define su propio __init__, el __init__ del padre ya no se ejecuta por sí solo. Si la hija sigue necesitando la preparación del padre, llámalo con super().__init__():
Salida:
Ada 1050.0
super() devuelve un objeto que busca métodos empezando por el padre de la clase actual. Por eso super().__init__(owner, balance) ejecuta Account.__init__ sobre el mismo objeto, y después existen self.owner y self.balance.
Si omites esa línea, el objeto queda construido a medias:
Salida:
0.05
AttributeError: 'SavingsAccount' object has no attribute 'owner'
Puedes escribir Account.__init__(self, owner) en su lugar, y el código antiguo lo hace a menudo. Se prefiere super() porque no repite el nombre del padre y coopera correctamente con la herencia múltiple, que se explica más abajo.
Ampliar un método en lugar de reemplazarlo
super() funciona en cualquier método, no solo en __init__. Llamar a la versión del padre dentro de una sobrescritura te permite añadir comportamiento alrededor de ella en lugar de reescribirla:
Salida:
started
[12:00] started
INFO [12:00] started
Cada nivel añade una pieza y delega el resto hacia arriba. Si más adelante cambia el format del padre, las dos hijas recogen el cambio.
isinstance e issubclass
Una instancia de una subclase también es instancia de todas las clases que están por encima. isinstance(obj, cls) comprueba esa relación, e issubclass(child, parent) la comprueba entre dos clases:
Prefiere isinstance a comparar type(obj) directamente. Una comprobación como type(obj) is Animal rechaza todas las subclases, lo que se rompe en cuanto alguien añade Dog.
Toda clase que escribes hereda de object, aunque no lo digas. class Animal: es lo mismo que class Animal(object):. De ahí vienen métodos por defecto como __str__, __repr__ y __eq__, y por eso sobrescribir __str__ cambia lo que muestra print():
Salida:
(2, 3)
True
Obligar a las subclases a implementar un método
A veces el padre nunca debería usarse directamente, solo como plantilla. El módulo abc marca una clase así como abstracta. Cualquier método decorado con @abstractmethod tiene que sobrescribirse antes de que la clase pueda instanciarse:
Las tres primeras líneas de la salida son la exportación:
exporting 2 rows
1,2
3,4
La última línea es un TypeError que nombra a Exporter y su método export pendiente (la redacción exacta cambia entre versiones de Python). El error aparece al crear el objeto, no más tarde cuando por fin se llama a export.
Herencia múltiple y la MRO
Una clase puede indicar más de un padre. Entonces hereda de todos ellos:
Salida:
paddling flapping
swims
['Duck', 'Swimmer', 'Flyer', 'object']
Los dos padres definen move, y Duck recibe el de Swimmer. Python decide con el orden de resolución de métodos (MRO): una única lista de clases, guardada en Duck.__mro__, que toda búsqueda de atributos recorre de principio a fin. Empieza por la propia clase, mantiene los padres en el orden en que los escribiste, coloca cada clase antes que sus propios padres y termina con object. Cambia el orden a class Duck(Flyer, Swimmer) y move devuelve "flies".
super() sigue la misma lista. Dentro de un método de Swimmer, super() significa "la siguiente clase después de Swimmer en la MRO de la clase del objeto", que para un Duck es Flyer, no object. Así es como varias clases cooperativas pueden llamar cada una a super() y conseguir que cada clase de la cadena se ejecute exactamente una vez, incluso en un diamante donde dos padres comparten un abuelo.
En la práctica, la herencia múltiple se usa sobre todo para mixins: clases pequeñas que añaden una capacidad, como un JsonMixin que añade un método to_json, combinadas con un padre principal. Las jerarquías profundas con varios padres completos son difíciles de seguir y rara vez hacen falta.
¿Herencia o composición?
La herencia modela una relación "es un". Una SavingsAccount es una Account: cualquier código que funcione con una Account debería funcionar con ella sin cambios. Cuando esa frase suena mal, probablemente quieras composición, que modela "tiene un": el objeto guarda otro objeto como atributo y lo llama.
Salida:
Roadster: 300 hp engine running
class Car(Engine) también funcionaría, pero afirmaría que un coche es un tipo de motor, le daría a Car todos los métodos del motor e impediría cambiar a otro motor sin crear otra clase. Con composición las piezas quedan separadas, y reemplazar el motor es cuestión de pasar otro objeto.
Algunas señales de que la herencia no es la herramienta adecuada:
- La hija sobrescribe la mayoría de los métodos del padre, o desactiva algunos.
- Heredas solo para reutilizar uno o dos métodos auxiliares.
- La jerarquía pasa de dos o tres niveles y tienes que leer varios archivos para ver qué hace un método.
Errores comunes
- Olvidar
super().__init__(). El__init__de la hija reemplaza al del padre, así que los atributos del padre nunca se asignan y más tarde recibes unAttributeError. - Pasar
selfen las llamadas asuper(). Escribesuper().__init__(name), nosuper().__init__(self, name).super()ya vincula el método al objeto actual. - Firmas incompatibles al sobrescribir. Si código compartido llama al
speak(self)del padre, unspeak(self, volume)en la hija con un argumento extra obligatorio rompe ese código. Mantén las sobrescrituras compatibles, o da valores por defecto a los parámetros nuevos. - Comprobar
type(obj) == SomeClass. Ignora las subclases. Usaisinstance. - Usar herencia para compartir código entre clases que no tienen relación. Mueve el código compartido a una función o a un objeto auxiliar.
Preguntas frecuentes
¿Cómo se hereda de una clase en Python?
Pon la clase padre entre paréntesis después del nombre de la nueva clase: class Dog(Animal):. Dog obtiene todos los métodos y atributos de clase de Animal, y puede añadir otros nuevos o sobrescribir los existentes.
¿Qué hace super().__init__()?
Llama al __init__ de la clase padre para que el padre pueda preparar sus propios atributos. Cuando una subclase define su propio __init__, Python no llama automáticamente al del padre, así que llamas a super().__init__(...) con los argumentos que necesita el padre y después asignas los atributos adicionales de la subclase.
¿Cuál es la diferencia entre isinstance y type en Python?
isinstance(obj, Animal) es True para un Animal y para instancias de cualquier subclase de Animal. type(obj) is Animal es True solo cuando el objeto se creó a partir del propio Animal. Prefiere isinstance, porque sigue funcionando cuando alguien añade una subclase.
¿Python admite herencia múltiple?
Sí: class C(A, B): hereda de ambas. Cuando los dos padres definen el mismo método, Python elige uno siguiendo el orden de resolución de métodos (MRO), que puedes consultar con C.__mro__. Enumera primero C, luego sus padres de izquierda a derecha, y object al final.
¿Cuándo debo usar composición en lugar de herencia?
Usa herencia para una relación "es un", donde la subclase puede usarse en cualquier lugar donde se espera el padre (una SavingsAccount es una Account). Usa composición, guardando un objeto como atributo, para una relación "tiene un" (un Car tiene un Engine). Si dudas, la composición es más fácil de cambiar después.