Práctica 3.4 — Clases, herencia y coches
prácticasTabla de Contenidos
- Herencia
- El metodo init() en una Clase Hija
- Definir Atributos y Metodos para la Clase Hija
- Instancias como Atributos
- Añadir mas detalle a la clase Bateria
- Resumen de Conceptos
- Diagrama de la Jerarquia
- Progresion del codigo
Herencia
Cuando escribes clases no siempre tienes que partir de cero. Si la clase que estas escribiendo es una version especializada de otra clase que ya has escrito, puedes usar herencia. Cuando una clase hereda de otra, adquiere automaticamente todos los atributos y metodos de la clase original. La clase original se llama clase padre (o parent class), y la nueva clase es la clase hija (o child class).
La clase hija hereda cada atributo y metodo de su padre, pero tambien es libre de definir atributos y metodos nuevos propios.
Continuidad: En la Practica 3.3 construimos la clase
Cochey la claseNaveEspacialpara dominar la mecanica de clases, atributos y metodos. Ahora vamos a reutilizar la claseCochecomo padre para crear un tipo mas especifico: el coche electrico.
El metodo __init__() en una Clase Hija
Cuando escribes una clase hija, lo primero que Python necesita hacer
es inicializar los atributos de la clase padre. Para ello, el
metodo __init__() de la clase hija llama al __init__() del padre.
Vamos a modelar un coche electrico. Un CocheElectrico es una
version especializada de Coche, asi que podemos basar CocheElectrico
en la clase Coche que ya tenemos y centrar nuestra codificacion
solamente en los atributos y comportamientos especificos de los
coches electricos.
Version 0 — La herencia mas basica
Empecemos creando una version sencilla de CocheElectrico que haga
todo lo que puede hacer Coche:
class Coche:
"""Intento sencillo de representar un coche."""
def __init__(self, fabricante, modelo, año):
"""Inicializa los atributos para describir un coche."""
self.fabricante = fabricante
self.modelo = modelo
self.año = año
self.cuentakilometros_lectura = 0
def nombra_descriptivamente(self):
"""Devuelve un nombre descriptivo con formato legible."""
nombre_descriptivo = f"{self.año} {self.fabricante} {self.modelo}"
return nombre_descriptivo.title()
def lee_cuentakilometros(self):
"""Imprime el kilometraje del coche."""
print(f"Este coche ha recorrido {self.cuentakilometros_lectura} kilometros.")
def update_cuentakilometros(self, kilometraje):
"""Establece la lectura del cuentakilometros al valor dado."""
if kilometraje >= self.cuentakilometros_lectura:
self.cuentakilometros_lectura = kilometraje
else:
print("¡No puedes retroceder el cuentakilometros!")
def incrementa_cuentakilometros(self, kms):
"""Suma la cantidad dada a la lectura del cuentakilometros."""
self.cuentakilometros_lectura += kms
class CocheElectrico(Coche):
"""Representa aspectos de un coche especificos de vehiculos electricos."""
def __init__(self, fabricante, modelo, año):
"""Inicializa los atributos de la clase padre."""
super().__init__(fabricante, modelo, año)
mi_leaf = CocheElectrico('nissan', 'leaf', 2024)
print(mi_leaf.nombra_descriptivamente())
2024 Nissan Leaf
Que ocurre aqui
- Empezamos con
Coche. Cuando creas una clase hija, la clase padre debe estar definida antes en el mismo archivo. Aqui situamosCocheprimero. - Definimos la clase hija:
CocheElectrico. El nombre de la clase padre debe ir entre parentesis en la definicion de la clase hija:class CocheElectrico(Coche): - La funcion
super()es una funcion especial que permite llamar a un metodo de la clase padre. La lineasuper().__init__(fabricante, modelo, año)le dice a Python que llame al__init__()deCoche, lo que da aCocheElectricotodos los atributos definidos en el padre. El nombresuperviene de la convencion de llamar a la clase padre superclase y a la hija subclase. - Probamos que la herencia funciona creando un coche electrico con los argumentos
'nissan','leaf'y2024. Llamamos anombra_descriptivamente(), que esta definida enCochepero esta disponible en cualquier instancia deCocheElectrico.
En resumen: aparte de
__init__(), todavia no hay atributos ni metodos propios de un coche electrico. De momento, solo estamos comprobando que la herencia funciona correctamente.
Definir Atributos y Metodos para la Clase Hija
Una vez que tienes una clase hija que hereda de la padre, puedes añadir cualquier atributo y metodo nuevo que sea necesario para diferenciar la clase hija de la padre.
Vamos a añadir un atributo especifico de los coches electricos (el tamaño de la bateria) y un metodo para informar sobre el.
Version 1 — Atributos y metodos propios de la clase hija
class Coche:
"""Intento sencillo de representar un coche."""
def __init__(self, fabricante, modelo, año):
"""Inicializa los atributos para describir un coche."""
self.fabricante = fabricante
self.modelo = modelo
self.año = año
self.cuentakilometros_lectura = 0
def nombra_descriptivamente(self):
"""Devuelve un nombre descriptivo con formato legible."""
nombre_descriptivo = f"{self.año} {self.fabricante} {self.modelo}"
return nombre_descriptivo.title()
def lee_cuentakilometros(self):
"""Imprime el kilometraje del coche."""
print(f"Este coche ha recorrido {self.cuentakilometros_lectura} kilometros.")
def update_cuentakilometros(self, kilometraje):
"""Establece la lectura del cuentakilometros al valor dado."""
if kilometraje >= self.cuentakilometros_lectura:
self.cuentakilometros_lectura = kilometraje
else:
print("¡No puedes retroceder el cuentakilometros!")
def incrementa_cuentakilometros(self, kms):
"""Suma la cantidad dada a la lectura del cuentakilometros."""
self.cuentakilometros_lectura += kms
class CocheElectrico(Coche):
"""Representa aspectos de un coche especificos de vehiculos electricos."""
def __init__(self, fabricante, modelo, año):
"""
Inicializa los atributos de la clase padre.
Despues inicializa los atributos especificos del coche electrico.
"""
super().__init__(fabricante, modelo, año)
self.tamaño_bateria = 40
def describir_bateria(self):
"""Imprime una descripcion del tamaño de la bateria."""
print(f"Este coche tiene una bateria de {self.tamaño_bateria} kWh.")
mi_leaf = CocheElectrico('nissan', 'leaf', 2024)
print(mi_leaf.nombra_descriptivamente())
mi_leaf.describir_bateria()
2024 Nissan Leaf
Este coche tiene una bateria de 40 kWh.
Que ocurre aqui
- Añadimos el atributo
self.tamaño_bateriay le asignamos un valor inicial de40(kWh). Este atributo se asociara a todas las instancias creadas a partir deCocheElectricopero no a las instancias deCoche. - Tambien añadimos el metodo
describir_bateria(), que imprime informacion sobre la bateria. Este metodo solo estara disponible para instancias deCocheElectrico. - No hay limite en cuanto puedes especializar la clase hija. Puedes añadir tantos atributos y metodos como necesites para modelar un coche electrico con la precision que quieras.
Consejo de diseño: Si un atributo o metodo pertenece a cualquier coche, no solo a un coche electrico, deberia ir en
Cochey no enCocheElectrico. Cualquiera que use la claseCocheobtendra esa funcionalidad, y la claseCocheElectricosolo contendra lo especifico de los vehiculos electricos.
Instancias como Atributos
Al modelar objetos del mundo real en codigo, puedes encontrar que estas añadiendo mas y mas detalle a una clase. Llega un momento en que tus listas de atributos y metodos crecen tanto que es buena idea extraer parte de la clase en una clase separada. Puedes dividir tu clase grande en clases mas pequeñas que trabajen juntas; esta tecnica se llama composicion.
Por ejemplo, si seguimos añadiendo detalles a la clase
CocheElectrico, podriamos acabar con muchos atributos y metodos
especificos de la bateria. Cuando eso ocurre, podemos parar y mover
esos atributos y metodos a una clase separada llamada Bateria.
Entonces usamos una instancia de Bateria como atributo de
CocheElectrico:
Version 2 — Instancias como atributos (composicion)
class Coche:
"""Intento sencillo de representar un coche."""
def __init__(self, fabricante, modelo, año):
"""Inicializa los atributos para describir un coche."""
self.fabricante = fabricante
self.modelo = modelo
self.año = año
self.cuentakilometros_lectura = 0
def nombra_descriptivamente(self):
"""Devuelve un nombre descriptivo con formato legible."""
nombre_descriptivo = f"{self.año} {self.fabricante} {self.modelo}"
return nombre_descriptivo.title()
def lee_cuentakilometros(self):
"""Imprime el kilometraje del coche."""
print(f"Este coche ha recorrido {self.cuentakilometros_lectura} kilometros.")
def update_cuentakilometros(self, kilometraje):
"""Establece la lectura del cuentakilometros al valor dado."""
if kilometraje >= self.cuentakilometros_lectura:
self.cuentakilometros_lectura = kilometraje
else:
print("¡No puedes retroceder el cuentakilometros!")
def incrementa_cuentakilometros(self, kms):
"""Suma la cantidad dada a la lectura del cuentakilometros."""
self.cuentakilometros_lectura += kms
class Bateria:
"""Intento sencillo de modelar la bateria de un coche electrico."""
def __init__(self, tamaño_bateria=40):
"""Inicializa los atributos de la bateria."""
self.tamaño_bateria = tamaño_bateria
def describir_bateria(self):
"""Imprime una descripcion del tamaño de la bateria."""
print(f"Este coche tiene una bateria de {self.tamaño_bateria} kWh.")
class CocheElectrico(Coche):
"""Representa aspectos de un coche especificos de vehiculos electricos."""
def __init__(self, fabricante, modelo, año):
"""
Inicializa los atributos de la clase padre.
Despues inicializa los atributos especificos del coche electrico.
"""
super().__init__(fabricante, modelo, año)
self.bateria = Bateria()
mi_leaf = CocheElectrico('nissan', 'leaf', 2024)
print(mi_leaf.nombra_descriptivamente())
mi_leaf.bateria.describir_bateria()
2024 Nissan Leaf
Este coche tiene una bateria de 40 kWh.
Que ocurre aqui
- Definimos una nueva clase llamada
Bateriaque no hereda de ninguna otra clase. Su__init__()tiene un parametrotamaño_bateriacon valor por defecto40. El metododescribir_bateria()tambien se ha movido aqui, desdeCocheElectrico. - En
CocheElectrico, ahora añadimos un atributoself.bateria. Esta linea le dice a Python que cree una nueva instancia deBateria(con el valor por defecto de 40 kWh) y la asigne al atributoself.bateria. Esto ocurrira cada vez que se llame a__init__(); cualquier instancia deCocheElectricotendra automaticamente una instancia deBateriaasociada. - Para acceder a los atributos de la bateria hay que usar doble notacion de punto:
mi_leaf.bateria.describir_bateria()
¿Por que composicion? Esto puede parecer mucho trabajo extra. Pero ahora puedes describir la bateria con todo el detalle que quieras sin ensuciar la clase
CocheElectrico. Ademas, la claseBateriaes reutilizable: podria usarse en una moto electrica, un patinete, o cualquier otro vehiculo.
Añadir mas detalle a la clase Bateria
Cuando la bateria es su propia clase, es natural seguir
enriqueciendola. Vas a añadir tu mismo/a un metodo obtener_autonomia() que
informe de la distancia que el coche puede recorrer segun el tamaño
de la bateria:
Objetivo de la practica : ampliar las caracteristicas del coche
Inventa una Version 3 que implemente sobre la Version 2 un Metodo obtener_autonomia()
Tu tarea: Estudia cada version, ejecutala, modificala. Experimenta cambiando valores, añadiendo metodos, creando nuevas subclases. La mejor forma de aprender POO es romper cosas y arreglarlas.
Reflexion sobre el diseño: En este punto podemos preguntarnos: ¿Donde deberia vivir un metodo como
obtener_autonomia()? ¿EnCocheElectricoo enBateria? Si la autonomia depende solo del tamaño de la bateria, entonces pertenece aBateria. Pero si la autonomia tambien dependiera del peso del coche, la aerodinamica, o el tipo de neumaticos… entonces quizaCocheElectricoseria mejor lugar. El arte de decidir donde poner cada metodo es lo que hace la POO interesante.
Demuestra tu arte como programador/a,
- Ampliando unas 2 o 3 caracteristicas del vehiculo usando el concepto de herencia. Inspirate en como hemos hecho con la bateria.
Resumen de Conceptos
| Concepto | Descripcion | Ejemplo |
|---|---|---|
| Herencia | Una clase hija adquiere atributos/metodos del padre | class CocheElectrico(Coche): |
| Clase padre (superclase) | La clase original de la que se hereda | Coche |
| Clase hija (subclase) | La nueva clase que hereda y especializa | CocheElectrico |
super() | Llama a un metodo de la clase padre | super().__init__(fabricante, modelo, año) |
| Atributo propio de la hija | Atributo que solo existe en la subclase | self.tamaño_bateria = 40 |
| Metodo propio de la hija | Metodo exclusivo de la subclase | describir_bateria() |
| Composicion | Usar una instancia de otra clase como atributo | self.bateria = Bateria() |
| Doble notacion de punto | Acceder a atributos de un objeto anidado | mi_leaf.bateria.describir_bateria() |
Diagrama de la Jerarquia
┌─────────────┐
│ Coche │ ← clase padre (superclase)
│─────────────│
│ fabricante │
│ modelo │
│ año │
│ cuentakm │
│─────────────│
│ nombra..() │
│ lee_ckm() │
│ update_ckm()│
│ increm_ckm()│
└──────┬──────┘
│ hereda
▼
┌───────────────┐
│CocheElectrico │ ← clase hija (subclase)
│───────────────│
│ bateria ──────┼──────┐
│───────────────│ │ composicion
│ │ ▼
└───────────────┘ ┌──────────┐
│ Bateria │
│──────────│
│ tamaño │
│──────────│
│ describir│
│ autonomia│
└──────────┘
Progresion del codigo — las 4 versiones
A continuacion tienes un resumen de como hemos ido construyendo el codigo, paso a paso:
| Version | Fichero | Que añade |
|---|---|---|
| v0 | coche_electrico_v0.py | Herencia basica: CocheElectrico(Coche) + super() |
| v1 | coche_electrico_v1.py | Atributo (tamaño_bateria) y metodo propios |
| v2 | coche_electrico_v2.py | Composicion: clase Bateria como atributo |
| A Crear por Ti | Caracteristicas adicionales del vehiculo. | |
| v3 | coche_electrico_v3.py? | Metodo obtener_autonomia() en Bateria ? |
Happy coding !