Ir al contenido
wannacode

Herencia

En resumen: La herencia es cuando una clase (la hija) recibe los campos y los métodos de otra clase (la clase padre), así que la parte común se escribe en un solo lugar.

Problema

Dos copias del mismo código

El programa de nóminas tiene una clase Employee con un nombre, un salario y un método para un aumento. Después hizo falta también un gerente, que además recibe un bono. Lo más rápido fue copiar Employee en una clase nueva Manager y añadir el bono.

Un mes después alguien introduce un aumento del -10 % en lugar del 10 %. Así que añades una comprobación a Employee. Te olvidas de ella en Manager (el método es raise_salary, porque raise es una palabra reservada en Python):

# en Employee
def raise_salary(self, percent):
if percent > 0:
self._salary += self._salary * percent // 100
# en Manager: copia de antes de la comprobación
def raise_salary(self, percent):
self._salary += self._salary * percent // 100
ivan = Employee("Juan", 2000)
maria = Manager("María", 3000, 500)
ivan.raise_salary(-10)
maria.raise_salary(-10)
print(ivan)
print(maria)
Salida
Juan: 2000 €
María: 2700 €

El salario de Juan está protegido, pero el de María bajó 300 €. Las dos clases son iguales en un 90 %, y aun así cada corrección hay que hacerla dos veces.

La idea

La parte común va en la clase padre

Un gerente es un empleado, solo que con algo más. Dilo en el código: class Manager(Employee):. En Manager escribes solo lo nuevo, el bono:

class Manager(Employee):
def __init__(self, name, salary, bonus):
super().__init__(name, salary)
self._bonus = bonus
@property
def bonus(self):
return self._bonus
maria = Manager("María", 3000, 500)
maria.raise_salary(-10)
maria.raise_salary(10)
print(maria)
print(f"Bono: {maria.bonus} €")
Salida
María: 3300 €
Bono: 500 €

Ahora Manager tiene todo lo que tiene Employee: un nombre, un salario, raise_salary() con la comprobación, __str__(). Por eso el aumento del -10 % no pasó. La comprobación está escrita una vez y funciona para las dos clases.

Esto se llama herencia. Employee es la clase padre (clase base), y Manager es la clase hija. La hija lo toma todo de la clase padre y añade lo suyo.

Para comprobar si la herencia tiene sentido, usa la frase «X es un Y». «Un gerente es un empleado» es cierto. «Una factura es un empleado» no lo es, así que la factura no hereda de Employee, aunque tenga un importe y un nombre.

Pruébalo

¿Qué recibe el becario?

Intern no tiene nada propio: el cuerpo es solo pass.

¿Qué imprimirá el programa?

class Intern(Employee):
pass
ana = Intern("Ana", 1200)
ana.raise_salary(5)
print(ana)
Respuesta

La idea

Primero la clase padre

Manager tiene su propio __init__, porque además tiene un bono. Pero el nombre y el salario los prepara el __init__ de Employee. ¿Quién lo llama?

Python llama a un solo __init__: el de la clase del objeto. Al de la clase padre lo tienes que llamar tú:

def __init__(self, name, salary, bonus):
super().__init__(name, salary)
self._bonus = bonus

super() es «la clase padre de esta clase», y super().__init__(name, salary) llama a su __init__ con estos argumentos. Prepara _name y _salary, y después Manager prepara _bonus. Python permite esta línea en cualquier lugar de __init__, pero escríbela primero: así la parte de la clase padre está lista antes de que la hija la use.

Si la olvidas, Python no dice nada hasta que hace falta el campo. print(maria) se detiene con AttributeError: 'Manager' object has no attribute '_name': el campo simplemente nunca se creó.

Hay un solo objeto. Contiene tanto la parte de la clase padre como la de la hija, y cada parte la prepara su propio constructor.

def __init__(self, name, salary, bonus):
super().__init__(name, salary)
self._bonus = bonus
maria = Manager("María", 3000, 500)
print(f"{maria}, bono {maria.bonus} €")
Paso a paso, en palabras
  1. Manager(...) crea un objeto. Por ahora está vacío: cada campo aparece cuando __init__ le da un valor.
  2. Empieza el __init__ de Manager con los tres argumentos.
  3. Primero la clase padre: super().__init__(name, salary) llama al __init__ de Employee. Python no lo llama por ti.
  4. El __init__ de Employee prepara su parte: _name y _salary.
  5. Solo entonces el cuerpo de Manager prepara el bono.
  6. El objeto es uno solo, y cada parte la prepara su propio constructor.
Más precisamente, para curiosos
  • __init__ se hereda como cualquier otro método. Por eso Intern, sin un __init__ propio, toma el nombre y el salario de Employee. Escribes tu propio __init__ cuando la hija tiene algo nuevo, y entonces reemplaza al de la clase padre.
  • En código antiguo verás super(Manager, self).__init__(...) o Employee.__init__(self, ...). Hacen lo mismo. super() sin nada entre los paréntesis es como se escribe hoy.

Pruébalo

¿Qué constructor se ejecuta primero?

Aquí la hija no llama a la clase padre de forma explícita:

¿Qué imprimirá el programa?

class Account:
def __init__(self):
print("Cuenta abierta")
class SavingsAccount(Account):
def __init__(self):
print("Interés: 3%")
savings = SavingsAccount()
Respuesta

Pruébalo

Encuentra el error

María debería tener un salario de 3000 € y un bono de 500 €. Pero el programa imprime:

Salida
María: 500 €

¿En qué línea está el error?

Elige la línea con el error.

La idea

El guion bajo: para la clase y sus hijas

A fin de año, el bono del gerente se suma al salario. El método está en Manager, y el salario es _salary en Employee. La propiedad salary solo lo lee, y aquí hay que cambiarlo.

En Java y C# hay un tercer nivel de acceso para esto, protected: la clase y sus hijas lo ven, y todos los demás no. Python no tiene ni private ni protected. El guion bajo es una convención justo para esto: «interno, para la clase y sus hijas». Por eso Manager escribe directamente en self._salary, y Employee no cambia:

class Employee:
def __init__(self, name, salary):
self._name = name
self._salary = salary
# ... el resto, como antes
class Manager(Employee):
# ... _bonus y __init__
def merge_bonus(self):
self._salary += self._bonus
self._bonus = 0
maria = Manager("María", 3000, 500)
maria.merge_bonus()
print(maria)
print(f"Bono: {maria.bonus} €")
Salida
María: 3500 €
Bono: 0 €

La convención no es una protección: maria._salary = 0 en el programa también pasa, sin ningún error. El guion bajo le dice a la gente «no lo toques desde fuera», y la hija es «de la familia». Así que la hija lo usa solo cuando realmente tiene que cambiar el campo, no para saltarse la comprobación de raise_salary().

Más precisamente, para curiosos
  • Dos guiones bajos, self.__salary, son lo más parecido a private. Python renombra el campo en silencio a _Employee__salary. En Manager, self.__salary ya es _Manager__salary, y ese campo no existe: AttributeError: 'Manager' object has no attribute '_Manager__salary'. Esto protege de choques de nombres accidentales, no de las personas: todavía puedes llegar a maria._Employee__salary.

Pruébalo

¿Qué falta?

Un becario es un empleado. ¿Qué va en el hueco?

class Intern():
    pass

La idea

Clases padre y un antepasado común

En Java, C# y JavaScript, una clase tiene exactamente una clase padre. Python también permite varias: class WorkingStudent(Employee, Student): recibe los métodos de las dos. Esto se llama herencia múltiple. Aquí usaremos una clase padre cada vez. Necesitaremos varias en la lección sobre interfaces.

La cadena puede ser larga. Intern hereda de Employee. ¿Y de qué hereda Employee, si no tiene clase padre entre paréntesis? Toda clase sin padre hereda de object. Es el padre común de todas las clases. Por eso incluso una clase vacía tiene métodos:

class Ticket:
pass
a = Ticket()
b = Ticket()
print(a == a)
print(a == b)
Salida
True
False

== llama al método __eq__, y viene de object. Por ahora solo te dice si las dos variables apuntan al mismo objeto.

__str__ también viene de ahí. Si no lo escribes, print(ticket) imprime algo como <__main__.Ticket object at 0x7f3a2c1d5e50>: el nombre de la clase y una dirección que cambia en cada ejecución. En el módulo «Clases y objetos» escribiste tu propio __str__. Ahora sabes que así reemplazas la versión de object. Hay más sobre esto en la lección siguiente.

Más precisamente, para curiosos
  • En Python no hay ninguna clase de la que no se pueda heredar. typing.final lo promete, pero solo lo comprueba una herramienta como mypy, no el propio Python.
  • Con varias clases padre, Python busca un método en orden: primero en la clase, después en las clases padre de izquierda a derecha, y al final en object. El orden se ve en WorkingStudent.__mro__.

Ejercicio

Clases hijas

Resuelve las tareas en tu editor. Las clases padre son del módulo «Clases y objetos». Si no las tienes a mano, escríbelas otra vez: son cortas.

1. Coche familiar. SimpleCar tiene una velocidad y un método accelerate() que la sube en 10. Escribe FamilyCar, que hereda de SimpleCar, empieza con 4 asientos y tiene un método add_seat(). Crea un coche familiar, acelera y añade un asiento:

Salida
Velocidad: 10 km/h
Asientos: 5

2. Estudiante. Person tiene un nombre, una edad e introduce(). Escribe Student, que hereda de Person y añade una nota media grade (un número decimal). Usa el constructor de la clase padre.

Salida
Soy Elena, tengo 19 años.
Nota media: 5.5

3. Breakdance. DanceMove tiene un nombre, una duración en segundos y perform(). Escribe BreakDanceMove con una complejidad del 1 al 10 y un método show_complexity().

Salida
Ejecuto moonwalk durante 5 segundos
Ejecuto windmill durante 12 segundos
windmill: complejidad 8 de 10

4. Coche eléctrico. Car tiene una marca, combustible en litros y los kilómetros recorridos. refuel(liters) añade combustible, y drive(distance) gasta 1 litro cada 10 km y suma los kilómetros (si no hay combustible suficiente, lo dice y no se mueve). Escribe ElectricCar:

  • sin combustible, y con una batería que empieza al 100 %;
  • con una eficiencia: cuántos km recorre con el 1 % de batería (5 para el «Tesla»);
  • charge() carga la batería al 100 %;
  • drive_electric(distance) gasta batería en lugar de combustible y también suma los kilómetros.
La hija también cambia los kilómetros. ¿Cómo debería llamarse el campo en Car? Pista: la tarjeta sobre el guion bajo.
Salida
Škoda: 150 km, combustible 35 l
Tesla: 200 km, batería 60%
No alcanza la batería para 400 km
Tesla: 600 km, batería 20%

Más tareas

  • Trabajador. Worker hereda de Person y añade un puesto de trabajo y un método work():
Salida
Soy Pablo, tengo 34 años.
Trabajo como contable
  • Becario con mentor. Añade a Intern un campo con el nombre del mentor y pásalo en el constructor, junto con el nombre y el salario para la clase padre. Ahora Intern sí necesita su propio __init__.

Test

Ponte a prueba

1. class Manager(Employee). ¿Qué es cierto?
2. El campo _name está en Employee. ¿Puede Manager escribir self._name = "María" en uno de sus métodos?
3. ¿Qué hace super().__init__(name, salary) en el __init__ de Manager?
4. ¿Cuántas clases padre puede tener una clase en Python?
5. Manager tiene su propio __init__, que no llama a super().__init__(). ¿Qué pasa?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·