Herencia y super(): reutilizar y extender clases
La herencia permite crear jerarquías de clases que comparten comportamiento. super() es la forma correcta de llamar al padre — evita hard-codear el nombre de la clase.
Herencia básica
Para que una clase herede de otra, se escribe el nombre de la clase padre entre paréntesis. La clase hija hereda todos los métodos y atributos del padre y puede añadir los suyos propios.
class Animal:
"""Clase base para todos los animales."""
def __init__(self, name: str, species: str) -> None:
self.name = name
self.species = species
self.is_alive = True
def breathe(self) -> str:
return f"{self.name} está respirando"
def eat(self, food: str) -> str:
return f"{self.name} come {food}"
def __repr__(self) -> str:
return f"{self.species}('{self.name}')"
# Dog hereda de Animal
class Dog(Animal):
def __init__(self, name: str, breed: str) -> None:
# Llama al __init__ del padre con super()
super().__init__(name, species="Canis lupus familiaris")
self.breed = breed
def bark(self) -> str:
return f"{self.name}: ¡Guau!"
class Cat(Animal):
def __init__(self, name: str, indoor: bool = True) -> None:
super().__init__(name, species="Felis catus")
self.indoor = indoor
def meow(self) -> str:
return f"{self.name}: ¡Miau!"
rex = Dog("Rex", "Labrador")
whiskers = Cat("Whiskers")
# Métodos heredados del padre
print(rex.breathe()) # Rex está respirando
print(whiskers.eat("atún")) # Whiskers come atún
# Métodos propios de cada subclase
print(rex.bark()) # Rex: ¡Guau!
print(whiskers.meow()) # Whiskers: ¡Miau!Rex está respirando
Whiskers come atún
Rex: ¡Guau!
Whiskers: ¡Miau!super().__init__()
super() devuelve un proxy del padre en la jerarquía MRO (Method Resolution Order). Usar super().__init__() garantiza que el constructor del padre se ejecuta correctamente, incluso en jerarquías complejas con herencia múltiple.
class Vehicle:
def __init__(self, make: str, model: str, year: int) -> None:
self.make = make
self.model = model
self.year = year
self.mileage: float = 0.0
def drive(self, km: float) -> None:
self.mileage += km
def info(self) -> str:
return f"{self.year} {self.make} {self.model} ({self.mileage:.0f} km)"
class ElectricVehicle(Vehicle):
def __init__(self, make: str, model: str, year: int, battery_kwh: float) -> None:
# super().__init__ inicializa todos los atributos del padre
super().__init__(make, model, year)
# Atributo adicional específico de vehículos eléctricos
self.battery_kwh = battery_kwh
self.charge_level: float = 100.0
def charge(self, percent: float) -> None:
self.charge_level = min(100.0, self.charge_level + percent)
def info(self) -> str:
base = super().info() # reutiliza el método del padre
return f"{base} | Batería: {self.charge_level:.0f}%"
tesla = ElectricVehicle("Tesla", "Model 3", 2024, 75.0)
tesla.drive(150)
tesla.charge(20)
print(tesla.info())
# 2024 Tesla Model 3 (150 km) | Batería: 100%2024 Tesla Model 3 (150 km) | Batería: 100%| Sin super() — frágil | Con super() — robusto |
|---|---|
| Vehicle.__init__(self, make, model, year) | super().__init__(make, model, year) |
| Si renombras la clase padre, hay que actualizar todas las referencias | Funciona sin cambios aunque se renombre la clase padre |
Sobrescribir métodos (override)
Una subclase puede redefinir cualquier método del padre. Python siempre llama al método de la clase más específica primero. Puedes llamar al padre con super().metodo() para extender en vez de reemplazar.
class Shape:
def area(self) -> float:
return 0.0
def describe(self) -> str:
return f"Soy una figura con área {self.area():.2f}"
class Circle(Shape):
def __init__(self, radius: float) -> None:
self.radius = radius
# Sobreescribir area() completamente
def area(self) -> float:
import math
return math.pi * self.radius ** 2
# Extender describe() usando el método del padre
def describe(self) -> str:
base = super().describe()
return f"{base} y radio {self.radius}"
class Square(Shape):
def __init__(self, side: float) -> None:
self.side = side
def area(self) -> float:
return self.side ** 2
c = Circle(5.0)
s = Square(4.0)
print(c.area()) # 78.53981633974483
print(s.area()) # 16.0
print(c.describe()) # Soy una figura con área 78.54 y radio 5.0
print(s.describe()) # Soy una figura con área 16.0078.53981633974483
16.0
Soy una figura con área 78.54 y radio 5.0
Soy una figura con área 16.00isinstance() y issubclass()
isinstance(obj, Clase) comprueba si un objeto es una instancia de una clase o de cualquiera de sus subclases. issubclass(Hija, Padre) comprueba la relación entre clases.
class Animal:
pass
class Dog(Animal):
pass
class Cat(Animal):
pass
class GoldenRetriever(Dog):
pass
golden = GoldenRetriever()
# isinstance: sube por toda la jerarquía
print(isinstance(golden, GoldenRetriever)) # True
print(isinstance(golden, Dog)) # True
print(isinstance(golden, Animal)) # True
print(isinstance(golden, Cat)) # False
# issubclass: relación entre clases
print(issubclass(GoldenRetriever, Dog)) # True
print(issubclass(GoldenRetriever, Animal)) # True
print(issubclass(Dog, Cat)) # False
# Caso práctico: procesar una lista de animales mixtos
animales = [Dog(), Cat(), GoldenRetriever(), Animal()]
perros = [a for a in animales if isinstance(a, Dog)]
print(f"Perros encontrados: {len(perros)}") # 2 (Dog y GoldenRetriever)True
True
True
False
True
True
False
Perros encontrados: 2Usa isinstance(obj, Clase) en vez de type(obj) == Clase. El primero respeta la herencia; el segundo solo coincide con el tipo exacto. type(GoldenRetriever()) == Dog es False, pero isinstance(GoldenRetriever(), Dog) es True.