Herencia en Python — teoría con la biblioteca de la facultad: super(), override, polimorfismo y clases abstractas (y el libro digital que se quedó sin título)
La semana en que terminamos el catálogo de la biblioteca de la facultad (el del tema de clases en Python), alguien propuso añadir los libros digitales. «Fácil», dije: una clase LibroDigital que herede de Libro, con su propio __init__ para guardar los megas. La creé, imprimí los megas, todo bien. Y en cuanto el catálogo pidió la ficha del libro, esto:
class Libro:
def __init__(self, titulo, autor):
self.titulo = titulo
self.autor = autor
def ficha(self):
return f'«{self.titulo}», de {self.autor}'
class LibroDigital(Libro):
def __init__(self, titulo, autor, megas):
# falta super().__init__(titulo, autor): titulo y autor nunca se guardan
self.megas = megas
ebook = LibroDigital('Python para todos', 'Severance', 2.4)
print(ebook.megas)
print(ebook.ficha())
La salida, real:
2.4
Traceback (most recent call last):
File "gancho.py", line 18, in <module>
print(ebook.ficha())
~~~~~~~~~~~^^
File "gancho.py", line 7, in ficha
return f'«{self.titulo}», de {self.autor}'
^^^^^^^^^^^
AttributeError: 'LibroDigital' object has no attribute 'titulo'
¿Cómo que LibroDigital no tiene titulo, si hereda de Libro y Libro lo guarda en su __init__? Ahí está la trampa: al escribir mi propio __init__, el de Libro ya no se ejecuta solo. Nadie guardó el título. Ese olvido es el error más repetido del tema en FP2, y en este artículo vas a ver por qué pasa, cómo se arregla con super() y todo lo demás que entra en el examen: sustitución de métodos (override), polimorfismo, isinstance frente a type, la clase object, herencia múltiple y el orden de búsqueda (MRO), atributos privados en las subclases y clases abstractas con ABC.
Seguimos con la biblioteca de la facultad: libros, revistas y tres tipos de socio (estudiantes, PDI y visitantes) con plazos y multas distintos. Los datos son inventados (no son las normas reales de la biblioteca de la ULPGC), pero se parecen a los de verdad.
Tabla de Contenidos
—
¿Qué es la herencia en Python?
La herencia permite crear una clase nueva a partir de otra que ya existe. La clase de la que se parte se llama superclase, clase base o «madre»; la nueva, subclase, clase derivada o «hija». La hija recibe todos los atributos y métodos de la madre sin escribirlos otra vez, y puede añadir los suyos o sustituir los que no le sirvan.
En Python se escribe poniendo la madre entre paréntesis detrás del nombre de la hija:
class Libro:
"""Lo que tiene cualquier libro del catálogo de la biblioteca."""
def __init__(self, titulo, autor):
self.titulo = titulo
self.autor = autor
def ficha(self):
return f'«{self.titulo}», de {self.autor}'
def dias_prestamo(self):
return 15 # plazo normal de la biblioteca
class Revista(Libro): # Revista ES UN Libro: hereda todo
pass # sin escribir ni una línea más
r = Revista('Investigación y Ciencia', 'Varios autores')
print(r.ficha()) # método heredado de Libro
print(r.dias_prestamo()) # también heredado
print(type(r).__name__, 'hereda de', Revista.__bases__[0].__name__)
print('¿Revista tiene ficha propia?', 'ficha' in Revista.__dict__)
print('¿Libro tiene ficha propia? ', 'ficha' in Libro.__dict__)
Salida:
«Investigación y Ciencia», de Varios autores 15 Revista hereda de Libro ¿Revista tiene ficha propia? False ¿Libro tiene ficha propia? True
Revista no tiene ni una línea propia (pass) y, sin embargo, r.ficha() y r.dias_prestamo() funcionan. Las dos últimas líneas lo demuestran: ficha no está en el diccionario de Revista (Revista.__dict__), está en el de Libro. Cuando pides r.ficha(), Python no la encuentra en la clase del objeto y sube a la madre. Esa búsqueda hacia arriba es todo el secreto de la herencia: los métodos no se copian, se buscan.
¿Cuándo tiene sentido heredar? Cuando entre las dos clases hay una relación «es un»: una revista es un material de la biblioteca, un estudiante es un socio. Si la relación es «tiene un» (una biblioteca tiene libros, un socio tiene préstamos), no se hereda: se guarda un objeto dentro de otro, como hicimos con Biblioteca en el tema de clases.
| Relación | Ejemplo | Qué se hace |
|---|---|---|
| «es un» | Estudiante es un Socio | Herencia: class Estudiante(Socio) |
| «tiene un» | Biblioteca tiene Libros | Composición: una lista de libros como atributo |
| «es un», pero no se comporta igual | Un Dvd que no se puede prestar a domicilio | Herencia + sustituir el método que cambia |
En los apuntes de FP2 verás la misma idea en Java con extends: class Two extends One. En Python son los paréntesis: class Two(One). Y una diferencia que cae en el test: Python permite herencia múltiple (varias madres separadas por comas); Java no.
—
Inicialización de subclases: super()
Volvamos al gancho. Si la hija no escribe __init__, hereda el de la madre y no hay problema. Pero en cuanto escribe el suyo, ese __init__ tapa al de la madre: Python encuentra uno en la hija y ya no sube. Por eso, cuando la hija necesita su propio __init__, lo primero que hace es llamar al de la madre con super():
class Libro:
def __init__(self, titulo, autor):
self.titulo = titulo
self.autor = autor
def ficha(self):
return f'«{self.titulo}», de {self.autor}'
class LibroDigital(Libro):
def __init__(self, titulo, autor, megas):
super().__init__(titulo, autor) # 1) la madre guarda lo común
self.megas = megas # 2) la hija añade lo suyo
ebook = LibroDigital('Python para todos', 'Severance', 2.4)
print(ebook.ficha())
print(vars(ebook)) # los tres atributos, en el mismo objeto
Salida:
«Python para todos», de Severance
{'titulo': 'Python para todos', 'autor': 'Severance', 'megas': 2.4}
Fíjate en vars(ebook): los tres atributos viven en el mismo objeto. No hay un «objeto Libro» dentro del «objeto LibroDigital»: hay un solo objeto, y el __init__ de la madre le pone titulo y autor mientras el de la hija le pone megas. El dibujo compara los dos casos:
La regla práctica tiene dos partes:
- El
__init__de la hija recibe sus parámetros y los de la madre (aquítitulo, autor, megas). - Pasa a
super().__init__(…)los de la madre y guarda él los suyos.
¿Importa el orden? Casi siempre conviene llamar a super().__init__ lo primero, porque así, si la hija necesita usar algo que pone la madre (por ejemplo, calcular algo con el título), ya existe. En la práctica verás una excepción con sentido: el Programador valida su nivel antes de llamar a la madre, para no crear nada si el dato está mal.
Escribir el __init__ de la hija y olvidar super().__init__(…). No da error al crear el objeto: el error sale más tarde, en el primer método heredado que use un atributo de la madre (AttributeError: 'LibroDigital' object has no attribute 'titulo'). Si ves ese mensaje con un atributo que «debería» estar, mira el __init__ de la hija.
Por qué super() y no el nombre de la clase
También funciona llamar a la madre por su nombre:
class Libro:
def __init__(self, titulo, autor):
self.titulo = titulo
self.autor = autor
class LibroDigital(Libro):
def __init__(self, titulo, autor, megas):
Libro.__init__(self, titulo, autor) # funciona, pero con self a mano
self.megas = megas # y el nombre de la madre escrito
ebook = LibroDigital('Python para todos', 'Severance', 2.4)
print(vars(ebook))
Salida:
{'titulo': 'Python para todos', 'autor': 'Severance', 'megas': 2.4}
El resultado es el mismo, entonces ¿por qué insisten los apuntes en super()? Por tres razones:
- No escribes
selfa mano. ConLibro.__init__(self, …)estás llamando a la función de la clase y tienes que pasarle el objeto tú. Consuper().__init__(…)elselfva solo, como en cualquier método. - No repites el nombre de la madre. Si mañana
LibroDigitalpasa a heredar deMaterialen vez deLibro, consuper()no tocas nada; con el nombre, tienes que buscar todas las llamadas. - Respeta el orden de búsqueda (MRO) cuando hay herencia múltiple, que veremos más abajo.
super()no significa «mi madre», sino «la siguiente clase en el orden de búsqueda de este objeto». Con una sola madre coinciden; con varias, no.
—
Sustitución de métodos: override
La hija puede redefinir un método heredado escribiendo uno con el mismo nombre. Se llama override o sustitución: cuando el objeto es de la hija, Python encuentra primero el suyo y ya no sube.
class Socio:
def __init__(self, nombre):
self.nombre = nombre
def limite(self): # cuántos libros puede llevarse a la vez
return 3
def dias_prestamo(self):
return 15
def descripcion(self):
return f'{self.nombre}: {self.limite()} libros, {self.dias_prestamo()} días'
class Estudiante(Socio):
pass # usa todo lo de Socio
class SocioPDI(Socio): # profesorado e investigación
def limite(self): # SUSTITUYE (override) el de Socio
return 10
def dias_prestamo(self):
return 60
def descripcion(self): # AMPLÍA el de Socio: lo llama y añade
return super().descripcion() + ' (PDI, sin multas)'
for s in [Estudiante('Laura'), SocioPDI('Dra. Ramos')]:
print(s.descripcion())
Salida:
Laura: 3 libros, 15 días Dra. Ramos: 10 libros, 60 días (PDI, sin multas)
Aquí pasan tres cosas a la vez, y conviene separarlas:
Estudianteno sustituye nada:laura.limite()sube hastaSocioy devuelve 3.SocioPDIsustituyelimiteydias_prestamo: sus objetos ya no ven los deSocio.descripcionestá definido enSocio, pero llama aself.limite(). Cuando el objeto es unSocioPDI, eseself.limite()busca desde la clase del objeto, así que encuentra el 10 deSocioPDI. La madre llama a un método que la hija ha cambiado, y sale el de la hija. Esto es lo que más sorprende la primera vez.
Para saber qué método se ejecuta, no mires dónde está escrita la llamada: mira de qué clase es el objeto (self) y busca desde ahí hacia arriba. self.limite() dentro de Socio no significa «el limite de Socio».
Llamar al método de la superclase desde el override
Sustituir no obliga a tirar lo que hacía la madre. En SocioPDI.descripcion, super().descripcion() devuelve el texto de Socio («Dra. Ramos: 10 libros, 60 días») y la hija le añade « (PDI, sin multas)». Son dos formas de override:
| Forma | Cómo se escribe | Cuándo |
|---|---|---|
| Sustituir | def limite(self): return 10 | El comportamiento de la madre no sirve para nada |
| Ampliar | return super().descripcion() + ' (PDI…)' | Lo de la madre vale y la hija añade algo antes o después |
Ampliar y olvidar el return: texto = super().descripcion() + '…' y nada más. El método devuelve None y el print saca None. Lo verás con salida real en el «¿Qué falla aquí?» de los ejercicios.
En Java el override se marca con @Override y el compilador avisa si la firma no coincide. Python no tiene esa red: si en la hija escribes def limites(self) (con una s de más), no sustituyes nada, creas un método nuevo y la madre sigue usando el suyo. Las clases abstractas, al final del artículo, te protegen en parte de eso.
—
Polimorfismo: misma interfaz, comportamiento diferente
Polimorfismo significa que el mismo código funciona con objetos de clases distintas, y cada uno responde a su manera. La función multa de abajo calcula la multa de cualquier socio sin preguntar de qué tipo es: le pide los días de préstamo y la multa por día, y cada objeto contesta lo suyo.
class Socio:
def __init__(self, nombre):
self.nombre = nombre
def dias_prestamo(self):
return 15
def multa_dia(self): # euros por día de retraso
return 0.20
class SocioPDI(Socio):
def dias_prestamo(self):
return 60
def multa_dia(self):
return 0.0
class Visitante(Socio): # carné de un día, sin domiciliar
def dias_prestamo(self):
return 7
def multa_dia(self):
return 0.50
def multa(socio, dias_fuera):
"""No sabe (ni le importa) de qué clase es el socio: le pregunta."""
retraso = max(0, dias_fuera - socio.dias_prestamo())
return retraso * socio.multa_dia()
for s in [Socio('Laura'), SocioPDI('Dra. Ramos'), Visitante('Pablo')]:
print(f'{type(s).__name__:<9} {s.nombre:<10} 20 días fuera -> {multa(s, 20):.2f} €')
Salida:
Socio Laura 20 días fuera -> 1.00 € SocioPDI Dra. Ramos 20 días fuera -> 0.00 € Visitante Pablo 20 días fuera -> 6.50 €
Comprueba las cuentas a mano: 20 días fuera con 15 de plazo son 5 de retraso; 5 × 0,20 = 1,00 € para Laura. Para el PDI, 20 − 60 sale negativo y max(0, …) lo deja en 0. Para el visitante, 20 − 7 = 13 días × 0,50 = 6,50 €.
Lo importante es lo que no hay en multa: ni un if type(socio) == …. Si mañana la biblioteca crea SocioAlumni con 30 días y 0,10 €/día, la función funciona sin tocarla. Esa es la ventaja práctica del polimorfismo: el código que usa los objetos no cambia cuando aparecen tipos nuevos.
En Python el polimorfismo ni siquiera necesita herencia. A multa le basta con que el objeto tenga dias_prestamo() y multa_dia(); si una clase sin relación con Socio los tiene, también funciona. Es el duck typing («si camina como un pato y hace cuac…»). La herencia sirve para no repetir código y para dejar escrito el contrato; el polimorfismo viene de llamar a los mismos métodos.
—
isinstance vs type: la diferencia que más confunde
Dos funciones parecen hacer lo mismo y no lo hacen:
type(obj)devuelve la clase exacta del objeto.isinstance(obj, Clase)dice si el objeto es de esa clase o de cualquier hija suya.
class Socio:
def dias_prestamo(self):
return 15
class Estudiante(Socio):
pass
class SocioPDI(Socio):
def dias_prestamo(self):
return 60
e = Estudiante()
print(type(e) == Estudiante, type(e) == Socio) # tipo EXACTO
print(isinstance(e, Estudiante), isinstance(e, Socio)) # respeta la herencia
print(isinstance(e, object), issubclass(SocioPDI, Socio))
print([c.__name__ for c in Estudiante.__mro__]) # orden de búsqueda
print('__str__' in dir(e), '__str__' in Socio.__dict__) # lo pone object
Salida:
True False True True True True ['Estudiante', 'Socio', 'object'] True False
Línea a línea: type(e) == Socio es False porque e es exactamente un Estudiante; isinstance(e, Socio) es True porque un estudiante es un socio. issubclass hace la misma pregunta con clases en vez de objetos. Y __mro__ (Method Resolution Order) es la lista exacta de clases que Python recorre al buscar un método: Estudiante → Socio → object.
| Quiero saber… | Uso | Con e = Estudiante() |
|---|---|---|
| Si puedo tratarlo como un Socio | isinstance(e, Socio) | True |
| Su clase exacta | type(e) | Estudiante |
| Si una clase hereda de otra | issubclass(Estudiante, Socio) | True |
| En qué orden se buscan los métodos | Estudiante.__mro__ | Estudiante, Socio, object |
La regla para el examen y para la vida: isinstance casi siempre. Comparar con type rompe la herencia: si filtras «los socios» con type(s) == Socio, se te escapan todos los estudiantes y PDI. En la práctica verás exactamente ese fallo con los coches de gasolina y el híbrido.
Usar isinstance para decidir qué hacer con cada tipo (if isinstance(s, SocioPDI): … elif isinstance(s, Visitante): …). Funciona, pero es lo contrario del polimorfismo: cada tipo nuevo te obliga a tocar ese if. Si te ves escribiendo esa cadena, ese comportamiento debería ser un método que cada clase sustituye.
—
La clase object: la raíz de todo
La última línea de la salida anterior tiene trampa: '__str__' in dir(e) es True, pero '__str__' in Socio.__dict__ es False. ¿De dónde sale ese __str__? De object: en Python 3 toda clase hereda de object aunque no lo escribas (class Socio: es lo mismo que class Socio(object):). Por eso cualquier objeto tiene __str__, __repr__, __eq__… aunque tú no los hayas definido, y por eso print(laura) sin __str__ saca el famoso <__main__.Estudiante object at 0x…>: es el __str__ de object.
Cuando en el tema de clases escribías __str__ o __repr__, estabas haciendo un override de los de object. Y en el tema siguiente, sobrecarga de operadores, harás lo mismo con __eq__, __lt__ o __add__.
—
Herencia múltiple y MRO
Python deja que una clase tenga varias madres. Un becario de investigación da clase (como el PDI) y a la vez estudia el máster (como un estudiante). ¿Qué límite de préstamos tiene?
class Estudiante:
def limite(self):
return 3
def dias_prestamo(self):
return 15
class SocioPDI:
def limite(self):
return 10
def dias_prestamo(self):
return 60
class Becario(SocioPDI, Estudiante): # da clase Y estudia el máster
def dias_prestamo(self):
return 30 # el suyo propio
b = Becario()
print([c.__name__ for c in Becario.__mro__])
print('límite:', b.limite()) # no lo tiene: gana SocioPDI (va primero)
print('días:', b.dias_prestamo()) # lo tiene él: gana Becario
Salida:
['Becario', 'SocioPDI', 'Estudiante', 'object'] límite: 10 días: 30
Python decide con el MRO: busca en Becario, después en las madres en el orden en que las escribiste (SocioPDI, luego Estudiante) y al final en object. limite no está en Becario, así que gana el de SocioPDI (10). dias_prestamo sí está en Becario, así que gana el suyo (30). Si hubieras escrito class Becario(Estudiante, SocioPDI), el límite sería 3.
La herencia múltiple tiene usos buenos (clases pequeñas que añaden una capacidad, llamadas mixins), pero en FP2 lo que se pide es entender el MRO y saber predecir qué método gana. Si dudas, imprime Clase.__mro__: es la respuesta exacta.
—
Atributos de las subclases
La hija ve todo lo que la madre guarda en el objeto… con una excepción que confunde mucho: los atributos con dos guiones bajos.
class Socio:
def __init__(self, nombre, codigo):
self.nombre = nombre # público
self._carne = codigo[:3] # «protegido»: convenio, sin cambio de nombre
self.__codigo = codigo # privado: Python lo guarda como _Socio__codigo
class Estudiante(Socio):
def resumen(self):
return f'{self.nombre} ({self._carne}) {self.__codigo}'
e = Estudiante('Laura', 'EST-4471')
print([k for k in vars(e)])
print(e.resumen())
Salida:
['nombre', '_carne', '_Socio__codigo']
Traceback (most recent call last):
File "privados.py", line 15, in <module>
print(e.resumen())
~~~~~~~~~^^
File "privados.py", line 10, in resumen
return f'{self.nombre} ({self._carne}) {self.__codigo}'
^^^^^^^^^^^^^
AttributeError: 'Estudiante' object has no attribute '_Estudiante__codigo'
vars(e) lo explica: el atributo __codigo que escribió Socio se guardó como _Socio__codigo (name mangling, lo vimos en el tema de clases). Dentro de Estudiante, escribir self.__codigo se convierte en self._Estudiante__codigo, que no existe. Por eso el traceback nombra _Estudiante__codigo.
| Cómo se llama | Ejemplo | ¿La hija lo usa con ese nombre? |
|---|---|---|
| Público | self.nombre | Sí |
| «Protegido» (un guion) | self._carne | Sí: es un convenio, «uso interno, también de las hijas» |
| Privado (dos guiones) | self.__codigo | No: se llama _Socio__codigo; usa una propiedad o un método de la madre |
La salida limpia: si las hijas necesitan un dato, la madre lo ofrece con una propiedad (@property) o lo guarda con un solo guion. Lo que no debes hacer es escribir self._Socio__codigo en la hija: funciona, pero te salta el encapsulamiento que querías.
—
Clases abstractas: contratos que deben cumplirse
En la biblioteca no existe «un material» a secas: existen libros, revistas, DVD. Material es una idea que reúne lo común, pero no tiene sentido crear un Material('¿Qué soy?'). Para eso están las clases abstractas: una clase que hereda de ABC (Abstract Base Class, del módulo abc) y marca con @abstractmethod los métodos que cada hija está obligada a escribir.
from abc import ABC, abstractmethod
class Material(ABC): # ABC: Abstract Base Class
def __init__(self, titulo):
self.titulo = titulo
@property
@abstractmethod
def tipo(self): # propiedad abstracta (como en los apuntes)
pass
@abstractmethod
def dias_prestamo(self):
pass
def ficha(self): # método normal que USA los abstractos
return f'[{self.tipo}] {self.titulo}: {self.dias_prestamo()} días'
class Libro(Material):
@property
def tipo(self):
return 'Libro'
def dias_prestamo(self):
return 15
class Revista(Material):
@property
def tipo(self):
return 'Revista'
def dias_prestamo(self):
return 7
for m in [Libro('Cálculo'), Revista('Investigación y Ciencia')]:
print(m.ficha())
try:
Material('¿Qué soy?')
except TypeError as err:
print('TypeError:', err)
Salida:
[Libro] Cálculo: 15 días [Revista] Investigación y Ciencia: 7 días TypeError: Can't instantiate abstract class Material without an implementation for abstract methods 'dias_prestamo', 'tipo'
Tres cosas que mirar:
tipoes una propiedad abstracta:@propertyencima de@abstractmethod, en ese orden (es lo que aparece en los apuntes conarea). Las hijas la implementan también como propiedad, y por esofichaescribeself.tiposin paréntesis.fichaes un método normal dentro de la clase abstracta, y usa los abstractos. La madre deja escrita la receta y las hijas ponen los ingredientes. A este patrón se le llama método plantilla y lo vas a ver en los tres programas de la práctica.- Crear un
MaterialdaTypeErrory el mensaje dice exactamente qué falta:without an implementation for abstract methods 'dias_prestamo', 'tipo'.
¿Y si una hija cumple solo la mitad del contrato?
from abc import ABC, abstractmethod
class Material(ABC):
def __init__(self, titulo):
self.titulo = titulo
@abstractmethod
def dias_prestamo(self):
pass
@abstractmethod
def tipo(self):
pass
class Dvd(Material):
def dias_prestamo(self): # cumple solo la mitad del contrato
return 3
d = Dvd('Documental de la EII')
Salida:
Traceback (most recent call last):
File "incompleta.py", line 22, in <module>
d = Dvd('Documental de la EII')
TypeError: Can't instantiate abstract class Dvd without an implementation for abstract method 'tipo'
Dvd escribió dias_prestamo pero no tipo, así que también es abstracta y tampoco se puede instanciar. Lo bueno es cuándo sale el error: al crear el objeto, no días después cuando alguien llame a tipo(). Es la red de seguridad que Python no tiene con el @Override de Java.
Heredar de ABC pero olvidar @abstractmethod. Entonces el método «abstracto» con pass es un método normal que devuelve None, la clase se puede instanciar y el fallo vuelve a salir tarde. Las dos cosas van juntas: class X(ABC) y @abstractmethod en cada método obligatorio.
Si una pregunta del test dice «¿qué pasa al ejecutar Figura() si Figura tiene un método abstracto sin implementar?», la respuesta es TypeError al crear el objeto, no al llamar al método.
—
Clase abstracta vs clase normal: cuándo usar cada una
| Situación | Elige | Ejemplo de la biblioteca |
|---|---|---|
| La madre tiene sentido sola y las hijas son variantes | Clase normal | Socio con valores por defecto; SocioPDI cambia el plazo |
| La madre es solo una idea común; no existen objetos «genéricos» | Clase abstracta (ABC) | Material: siempre es libro, revista o DVD |
| Quieres que olvidar un método dé error al crear el objeto | Clase abstracta con @abstractmethod | dias_prestamo en todos los materiales |
| Hay un valor razonable por defecto que algunas hijas cambian | Método normal en la madre (gancho) | multa_dia() devuelve 0,20 y el PDI lo sustituye por 0 |
La última fila es la más útil en la práctica: no todo tiene que ser abstracto. Un método normal con un valor por defecto que algunas hijas sustituyen (un gancho) ahorra escribir lo mismo en cinco clases.
—
Errores típicos con herencia
Olvidar super().__init__ en una hija con __init__ propio: AttributeError en el primer método heredado que use un atributo de la madre.
Cambiar la firma al sustituir un método (def dias_prestamo(self, en_verano) en la hija cuando la madre lo llama sin argumentos): TypeError: … missing 1 required positional argument. Las hijas deben aceptar lo mismo que la madre.
Usar self.__atributo de la madre en la hija: AttributeError con _Hija__atributo. Usa una propiedad o un guion bajo.
Filtrar con type(x) == Madre: se te escapan las hijas. Usa isinstance.
Escribir mal el nombre del método que sustituyes (dias_prestamos): no sustituyes nada. Con una clase abstracta, el error salta al crear el objeto; sin ella, la madre sigue usando el suyo en silencio.
—
Míralo paso a paso en Python Tutor
pythontutor.com ejecuta el código línea a línea y dibuja los objetos, sus atributos y la pila de llamadas. Elige Python 3, pega este programa y pulsa Visualize Execution:
class Socio:
def __init__(self, nombre):
self.nombre = nombre
def dias(self):
return 15
def ficha(self):
return self.nombre + ': ' + str(self.dias()) + ' días'
class SocioPDI(Socio):
def __init__(self, nombre, depto):
super().__init__(nombre)
self.depto = depto
def dias(self):
return 60
socios = [Socio('Laura'), SocioPDI('Ramos', 'Informática')]
for s in socios:
print(s.ficha())
Salida:
Laura: 15 días Ramos: 60 días
Qué observar paso a paso:
- Cuando se crea
SocioPDI('Ramos', 'Informática'), fíjate en la pila de la derecha: aparecen dos marcos de__init__a la vez, el deSocioPDIy, encima, el deSociollamado porsuper(). Los dos tienen el mismoself(la flecha apunta al mismo objeto). - Al terminar, el objeto tiene
nombreydepto: uno lo puso la madre y otro la hija, pero es un solo objeto. - En la segunda vuelta del
for, entra enficha(que está enSocio) y desde ahí llama aself.dias(). Mira a qué función salta: a la deSocioPDI, porqueselfes unSocioPDI. - En la primera vuelta, el mismo
self.dias()salta a la deSocio. La misma línea, dos destinos: eso es el polimorfismo visto en directo.
—
Resumen rápido
| Concepto | Cómo se escribe | Qué recordar |
|---|---|---|
| Heredar | class Hija(Madre): | Los métodos no se copian: se buscan hacia arriba |
| Inicializar | super().__init__(…) | Obligatorio si la hija escribe su propio __init__ |
| Sustituir | mismo nombre en la hija | Misma firma que la madre |
| Ampliar | return super().metodo() + … | No olvides el return |
| Polimorfismo | mismas llamadas, objetos distintos | Nada de cadenas de if type(…) |
| Comprobar tipo | isinstance(obj, Clase) | type no ve la herencia |
| Orden de búsqueda | Clase.__mro__ | En herencia múltiple, de izquierda a derecha |
| Privados | __x → _Madre__x | La hija no lo ve con ese nombre |
| Abstracta | class X(ABC) + @abstractmethod | TypeError al crear si falta algo |
Tres ideas para quedarte:
- Python busca los métodos desde la clase del objeto hacia arriba, y el primero que encuentra, gana. Casi todo lo del tema sale de ahí.
- Si la hija escribe
__init__, llama asuper().__init__: si no, lo de la madre no llega al objeto. - Una clase abstracta convierte un olvido en un error temprano: mejor un
TypeErroral crear el objeto que unNoneraro tres días después.
En la práctica de herencia construimos tres jerarquías completas (una nómina, una flota de coches con un híbrido que hereda de un coche de gasolina y una tienda con IGIC) y verás el método plantilla en acción.
Siguiente paso: prácticaHerencia en Python práctica — una nómina, una flota con un híbrido y una tienda con IGIC (y la fórmula que el híbrido copió en vez de heredar)¿Te ha servido este artículo? Así puedes apoyar SergioLearns →




Qué es un sistema operativo — teoría con tu portátil en una tarde de estudio, una llamada a write() y la CPU que nunca descansa (y los tres programas que no se ejecutaban a la vez)