Interfaces como clases abstractas
En resumen: Python no tiene interface: el contrato es una clase abstracta con solo métodos abstractos, que toda clase que lo acepta tiene que escribir.
Problema
Salarios y facturas
A final de mes la empresa paga dos tipos de cosas: los salarios de los empleados y las facturas de los proveedores. Quieres una sola lista y un solo bucle: a quién se paga, cuánto, y el total al final.
Una factura no es un empleado. Hacer que herede de Employee está mal. La frase «una factura es un empleado» no es cierta, y además la factura tendría un nombre, un salario y un aumento.
Una clase padre común Payment con campos y código tampoco es la respuesta: los empleados y las facturas no tienen datos comunes ni código común. Lo único que tienen en común es que se pueden pagar.
La idea
Una interfaz: un contrato «puede hacer»
Una interfaz es una lista de métodos sin cuerpo: qué tiene que saber hacer una clase, pero no cómo. Python no tiene una palabra aparte para ella. Escribes el contrato como una clase abstracta en la que todos los métodos son abstractos:
from abc import ABC, abstractmethod
class Payable(ABC): @abstractmethod def amount_due(self): pass
@abstractmethod def payee(self): passclass Invoice(Payable): def __init__(self, supplier, amount): self._supplier = supplier self._amount = amount
def amount_due(self): return self._amount
def payee(self): return self._supplier
power = Invoice("Compañía eléctrica", 340)print(f"{power.payee()}: {power.amount_due()} €")Compañía eléctrica: 340 €Payable va entre los paréntesis como una clase padre. Python no distingue una clase padre con código de una clase padre contrato: la diferencia es que Payable no tiene campos ni cuerpos de métodos. La comprobación funciona como en una clase abstracta: no puedes crear objetos de Payable. Payable() se detiene con
TypeError: Can't instantiate abstract class Payable without an implementation for abstract methods 'amount_due', 'payee'Míralo
Un solo bucle para todo lo que se paga
El empleado también acepta el contrato. Su importe es el salario mensual:
class Employee(Payable): # ... _name, _salary, __init__, monthly_pay()
def amount_due(self): return self.monthly_pay()
def payee(self): return self._nameAhora tanto los empleados como las facturas son Payable: las dos clases lo tienen como clase padre. La lista no tiene tipo, pero todo lo que hay en ella cumple el contrato, así que el bucle puede llamar a amount_due() y payee() en cada uno.
payments = [ Employee("Juan", 2000), Manager("María", 3000, 500), Invoice("Compañía eléctrica", 340), Invoice("Casero", 1800),]total = 0for p in payments: print(f"{p.payee()}: {p.amount_due()} €") total += p.amount_due()print(f"Total: {total} €")Juan: 2000 €María: 3500 €Compañía eléctrica: 340 €Casero: 1800 €Total: 7640 €El gerente no escribe nada nuevo. De Employee hereda tanto el contrato como amount_due(). Y amount_due() llama a monthly_pay(), que el gerente ha sobrescrito. Por eso María recibe 3500 €.
payments = [ Employee("Juan", 2000), Manager("María", 3000, 500), Invoice("Compañía eléctrica", 340),]total = 0for p in payments: total += p.amount_due()print(f"Total: {total} €")Paso a paso, en palabras
- La lista guarda tres objetos de tres clases. Todos son Payable, y las flechas solo llegan a su contrato.
- total empieza en 0.
- p apunta al empleado: amount_due() devuelve el salario, 2000.
- El gerente es un empleado, así que también se le puede pagar. Salario más bono: 3500.
- La factura no es un empleado, pero cumple el mismo contrato: 340.
- El bucle no conoce las clases. Solo sabe que a cada uno se le puede pagar.
Pruébalo
¿Cuál es el importe?
¿Qué imprimirá el programa? Las clases son de la tarjeta de arriba.
p = Manager("María", 3000, 500)print(p.amount_due())Manager es un Employee, y Employee implementa el contrato. Así que el gerente también lo implementa, sin escribirlo. La llamada va al método sobrescrito del gerente: 3000 + 500.
La idea
Una clase padre, muchos contratos
Contabilidad también quiere las facturas en formato CSV: una línea «proveedor;importe» por cada una. Ese es un segundo contrato. En Python una clase puede tener todas las clases padre que quiera, separadas por comas, así que los contratos son simplemente más clases padre:
class Exportable(ABC): @abstractmethod def to_csv(self): pass
class Invoice(Payable, Exportable): # ... como antes
def to_csv(self): return f"{self._supplier};{self._amount}"for_accounting = [Invoice("Compañía eléctrica", 340), Invoice("Casero", 1800)]for e in for_accounting: print(e.to_csv())Compañía eléctrica;340Casero;1800Si la clase tiene a la vez una clase padre y contratos, la clase padre va primero:
class Manager(Employee, Exportable):Entonces el gerente escribe también to_csv(), y es a la vez un Employee, un Payable y un Exportable. El orden entre los paréntesis importa: cuando dos clases padre tienen un método con el mismo nombre, gana la primera.
Puede haber muchas clases padre, pero lo mejor es que solo una tenga código y datos. Las demás son contratos: solo promesas, sin campos.
Pruébalo
Encuentra el error
Invoice("Compañía eléctrica", 340) se detiene con TypeError. ¿Qué línea tiene el error?Elige la línea con el error.
amount_du en lugar de amount_due. Para Python es solo otro método, y el prometido sigue sin estar escrito. Así que no puedes crear una factura: TypeError: Can't instantiate abstract class Invoice without an implementation for abstract method 'amount_due'.Pruébalo
¿Qué falta?
class Invoice():
# ...Payable: el contrato va entre los paréntesis como una clase padre. Sin él, la factura sigue teniendo los métodos, pero no es un Payable, y Python no comprueba que estén todos escritos.La idea
¿Clase abstracta o interfaz?
En Python las dos son clases abstractas: un contrato es una clase abstracta sin campos y sin código. La diferencia no está en el lenguaje, sino en lo que dicen:
| Clase abstracta con código | Contrato | |
|---|---|---|
| Dice | «X es un Y» | «X puede» |
| Cuántas por clase | todas las que quieras, pero mejor una | todos los que quieras |
Campos y __init__ |
sí | no |
| Código común | sí | casi nada |
- Una clase abstracta, cuando las clases de debajo son un mismo tipo de cosa y comparten código y datos: las figuras tienen un nombre y una descripción, los empleados tienen un nombre y un salario.
- Un contrato, cuando las clases son distintas pero necesitan una capacidad común: un empleado y una factura «se pueden pagar», un pájaro y un avión «pueden volar».
Employee es la clase padre del gerente y del becario, y a la vez implementa Payable.Más precisamente, para curiosos
- Un contrato también puede tener un método por defecto: un método normal con cuerpo junto a los abstractos. Las clases lo reciben ya hecho, y también lo pueden sobrescribir. Así puedes añadir un método nuevo al contrato sin romper las clases que ya lo cumplen:
class Flyable(ABC): @abstractmethod def fly(self): pass
def glide(self): print("Planea por el aire")
class Bird(Flyable): def fly(self): print("El pájaro bate las alas")
class Plane(Flyable): def fly(self): print("El avión despega")
flyers = [Bird(), Plane()]for f in flyers: f.fly() f.glide()El pájaro bate las alasPlanea por el aireEl avión despegaPlanea por el aire- Hay una segunda manera de escribir un contrato:
typing.Protocol. Entonces la clase no escribe el contrato entre sus paréntesis. Mientras tenga los métodos, encaja, igual que en JavaScript. El propio Python no comprueba el protocolo: la entrada sinamount_due()pasa por unPayable. Lo comprueba un comprobador de tipos aparte, antes de que arranque el programa. Con@runtime_checkableeisinstance(la lección «Comprobaciones de tipo») puedes preguntar si el objeto tiene los métodos:
from typing import Protocol, runtime_checkable
@runtime_checkableclass Payable(Protocol): def amount_due(self): ...
def payee(self): ...
class Invoice: def __init__(self, supplier, amount): self._supplier = supplier self._amount = amount
def amount_due(self): return self._amount
def payee(self): return self._supplier
class Ticket: def payee(self): return "Cine"
p: Payable = Ticket()print(isinstance(Invoice("Casero", 1800), Payable))print(isinstance(p, Payable))TrueFalseEjercicio
Contratos
Resuelve las tareas en tu editor. En Python cada «interfaz» es una clase abstracta (ABC) con solo métodos abstractos.
1. Claxon. Una interfaz Honkable con un método honk(). SimpleCar del módulo «Clases y objetos» la implementa.
¡Bip, bip!2. Datos. Una interfaz Printable con un método print_details(). Impleméntala en Person, y haz que Student (una clase hija de Person) la sobrescriba y añada la nota. Pon los dos en una sola lista.
Nombre: Jorge, edad: 45Nombre: Elena, edad: 19Nota media: 5.53. Baile. Una interfaz Danceable con un método dance(). DanceMove (un nombre y una duración) la implementa.
Bailo salsa durante 30 segundos4. DJ. Una clase Song con un título. Una interfaz Playable con un método play(). DJ la implementa y tiene un método play_song(song) que recuerda la canción y la reproduce. Si todavía no hay canción, play() lo dice.
No hay ninguna canción elegidaReproduzco 'Bohemian Rhapsody'5. El estanque. Interfaces Swimmable con swim() y Flyable con fly(). Fish nada. Duck nada y vuela.
El pez nada.El pato nada.El pato vuela.Más tareas
- Descuento. Una interfaz Discountable con un método que devuelve el precio después del descuento. Impleméntala en
Product(10 % de descuento) y enSubscription(25 % de descuento para estudiantes). Calcula el total de un carrito con un producto de 50 € y una suscripción de estudiante de 12 €: 45 € + 9 € = 54 €. - CSV para todos. Haz que
Employeetambién implemente Exportable, con una línea «nombre;salario mensual». Pon empleados y facturas en una sola lista.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Encapsulamiento · Herencia · Polimorfismo · Clases abstractas
- Continúa en: Comprobaciones de tipo · Principios de la POO · Lanzar excepciones y crear las tuyas · Archivos
- Ejercicios sobre el tema: Herencia y polimorfismo