Ir al contenido
wannacode

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):
pass
Una clase que acepta el contrato implementa la interfaz y escribe cada uno de sus métodos:
class 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()} €")
Salida
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._name

Ahora 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 = 0
for p in payments:
print(f"{p.payee()}: {p.amount_due()} €")
total += p.amount_due()
print(f"Total: {total} €")
Salida
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 = 0
for p in payments:
total += p.amount_due()
print(f"Total: {total} €")
Paso a paso, en palabras
  1. La lista guarda tres objetos de tres clases. Todos son Payable, y las flechas solo llegan a su contrato.
  2. total empieza en 0.
  3. p apunta al empleado: amount_due() devuelve el salario, 2000.
  4. El gerente es un empleado, así que también se le puede pagar. Salario más bono: 3500.
  5. La factura no es un empleado, pero cumple el mismo contrato: 340.
  6. 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())
Respuesta

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())
Salida
Compañía eléctrica;340
Casero;1800

Si 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.

Pruébalo

¿Qué falta?

class Invoice():
    # ...

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».
A menudo se usan juntas: 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()
Salida
El pájaro bate las alas
Planea por el aire
El avión despega
Planea 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 sin amount_due() pasa por un Payable. Lo comprueba un comprobador de tipos aparte, antes de que arranque el programa. Con @runtime_checkable e isinstance (la lección «Comprobaciones de tipo») puedes preguntar si el objeto tiene los métodos:
from typing import Protocol, runtime_checkable
@runtime_checkable
class 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))
Salida
True
False

Ejercicio

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.

Salida
¡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.

Salida
Nombre: Jorge, edad: 45
Nombre: Elena, edad: 19
Nota media: 5.5

3. Baile. Una interfaz Danceable con un método dance(). DanceMove (un nombre y una duración) la implementa.

Salida
Bailo salsa durante 30 segundos

4. 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.

Salida
No hay ninguna canción elegida
Reproduzco 'Bohemian Rhapsody'

5. El estanque. Interfaces Swimmable con swim() y Flyable con fly(). Fish nada. Duck nada y vuela.

Salida
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 en Subscription (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 Employee también implemente Exportable, con una línea «nombre;salario mensual». Pon empleados y facturas en una sola lista.

Test

Ponte a prueba

1. ¿Cuántos contratos (clases abstractas) puede cumplir una clase?
2. Payable hereda de ABC y tiene dos métodos abstractos. ¿Qué pasa con Payable()?
3. Invoice(Payable) escribe amount_du() en lugar de amount_due(). ¿Cuándo te enteras?
4. ¿Quién comprueba un contrato escrito con typing.Protocol?
5. Hay que pagar a los empleados y las facturas en un solo bucle. No comparten código. ¿Qué encaja mejor?

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