Introduzione
Quando un programma cresce, può essere utile definire una struttura comune che diverse classi devono rispettare. Per esempio, un’applicazione può gestire più sistemi di pagamento: carta di credito, PayPal e bonifico bancario. Ogni sistema esegue il pagamento in modo diverso, ma tutti devono offrire un’operazione fondamentale, come paga().
In questi casi Python mette a disposizione le classi astratte, definite generalmente tramite il modulo abc, acronimo di Abstract Base Classes. Una classe astratta descrive un’interfaccia comune e può contenere metodi che le sottoclassi sono obbligate a implementare.
Le classi astratte sono utili perché:
- rendono esplicite le operazioni richieste da una famiglia di classi;
- evitano implementazioni incomplete o incoerenti;
- facilitano la sostituzione di un’implementazione con un’altra;
- migliorano la leggibilità e la manutenzione del codice;
- permettono di progettare il programma partendo dai comportamenti attesi.
È importante distinguere tra una classe astratta e una classe normale. Una classe normale può essere istanziata direttamente, mentre una classe astratta che contiene metodi astratti non può essere usata per creare oggetti finché non vengono implementati tutti i comportamenti obbligatori.
Codice completo
from abc import ABC, abstractmethod
class MetodoPagamento(ABC):
"""Interfaccia comune per tutti i metodi di pagamento."""
def __init__(self, intestatario: str):
if not intestatario.strip():
raise ValueError("L´intestatario non può essere vuoto")
self.intestatario = intestatario
@abstractmethod
def paga(self, importo: float) -> str:
"""Esegue il pagamento e restituisce un messaggio."""
pass
@abstractmethod
def descrizione(self) -> str:
"""Restituisce una descrizione del metodo di pagamento."""
pass
def riepilogo(self) -> str:
"""Metodo concreto, già disponibile per le sottoclassi."""
return f"{self.descrizione()} - intestatario: {self.intestatario}"
class CartaDiCredito(MetodoPagamento):
def __init__(self, intestatario: str, ultime_cifre: str):
super().__init__(intestatario)
if len(ultime_cifre) != 4 or not ultime_cifre.isdigit():
raise ValueError("Le ultime cifre devono essere esattamente quattro numeri")
self.ultime_cifre = ultime_cifre
def paga(self, importo: float) -> str:
if importo <= 0:
raise ValueError("L´importo deve essere positivo")
return (
f"Pagamento di {importo:.2f} euro effettuato "
f"con carta **** {self.ultime_cifre}"
)
def descrizione(self) -> str:
return f"Carta di credito **** {self.ultime_cifre}"
class PayPal(MetodoPagamento):
def __init__(self, intestatario: str, email: str):
super().__init__(intestatario)
if "@" not in email:
raise ValueError("L´indirizzo email non è valido")
self.email = email
def paga(self, importo: float) -> str:
if importo <= 0:
raise ValueError("L´importo deve essere positivo")
return f"Pagamento di {importo:.2f} euro effettuato con PayPal ({self.email})"
def descrizione(self) -> str:
return f"PayPal ({self.email})"
def completa_acquisto(metodo: MetodoPagamento, importo: float) -> None:
"""Funzione compatibile con qualsiasi MetodoPagamento."""
print(metodo.riepilogo())
print(metodo.paga(importo))
pagamento_carta = CartaDiCredito("Laura Rossi", "4821")
pagamento_paypal = PayPal("Marco Bianchi", "[email protected]")
completa_acquisto(pagamento_carta, 49.90)
print()
completa_acquisto(pagamento_paypal, 19.50)
# Questa istruzione genererebbe un errore:
# metodo_generico = MetodoPagamento("Nome")
Spiegazione
La classe ABC
Per creare una classe astratta si importa ABC dal modulo abc e si fa ereditare la classe da ABC. Nel codice, MetodoPagamento rappresenta il concetto generale di pagamento.
from abc import ABC
class MetodoPagamento(ABC):
pass
L’ereditarietà da ABC consente a Python di riconoscere la classe come base per una gerarchia di classi astratte.
Il decoratore abstractmethod
Un metodo astratto si definisce aggiungendo il decoratore @abstractmethod. Questo metodo stabilisce un obbligo per le sottoclassi, ma non fornisce necessariamente un’implementazione completa.
from abc import ABC, abstractmethod
class Notifica(ABC):
@abstractmethod
def invia(self, messaggio: str) -> None:
pass
Una classe che eredita da Notifica deve implementare invia(). Se non lo fa, non sarà possibile istanziarla.
Metodi astratti e metodi concreti
Una classe astratta può contenere anche metodi concreti, cioè già implementati. Nell’esempio, riepilogo() usa i metodi descrizione() e paga(), che saranno forniti dalle sottoclassi.
Questo approccio permette di condividere logica comune senza duplicare codice. Le sottoclassi si occupano delle differenze specifiche, mentre la classe base gestisce il comportamento generale.
Polimorfismo in pratica
La funzione completa_acquisto() accetta un parametro di tipo MetodoPagamento. Non deve sapere se sta lavorando con una carta o con PayPal: chiama semplicemente i metodi definiti nell’interfaccia comune.
Questo è un esempio di polimorfismo: oggetti di classi diverse rispondono agli stessi metodi, ognuno con il proprio comportamento.
Grazie a questa struttura, aggiungere un nuovo metodo di pagamento è semplice. È sufficiente creare una nuova sottoclasse e implementare paga() e descrizione(), senza modificare la funzione completa_acquisto().
Best practice
- Definisci un’interfaccia coerente: i metodi astratti devono rappresentare comportamenti realmente comuni alle sottoclassi.
- Mantieni piccole le classi astratte: una classe base troppo grande può diventare difficile da estendere.
- Usa nomi descrittivi: metodi come paga(), invia() o calcola_totale() comunicano chiaramente il loro scopo.
- Valida i dati vicino al punto d’ingresso: controlla gli argomenti nei costruttori e nei metodi pubblici.
- Non usare una classe astratta solo per abitudine: se ti serve soltanto condividere qualche funzione, una classe normale o una funzione indipendente può essere più semplice.
- Evita controlli sul tipo concreto: preferisci chiamare i metodi comuni invece di usare molti isinstance().
- Documenta il contratto: docstring e annotazioni di tipo chiariscono cosa deve fare ogni implementazione.
Una classe astratta è particolarmente indicata quando il progetto ha più implementazioni intercambiabili o quando vuoi impedire che una nuova sottoclasse dimentichi operazioni importanti.
Riepilogo
Le classi astratte permettono di definire una base comune per oggetti che condividono gli stessi comportamenti. In Python si usano principalmente ABC e @abstractmethod dal modulo abc.
Una classe astratta può contenere sia metodi astratti, che le sottoclassi devono implementare, sia metodi concreti, che forniscono logica riutilizzabile. Il risultato è un codice più organizzato, estensibile e adatto al polimorfismo.
Nel caso dei sistemi di pagamento, la funzione principale lavora con l’interfaccia MetodoPagamento e non dipende dai dettagli di CartaDiCredito o PayPal. Questo riduce l’accoppiamento e rende più semplice aggiungere nuove funzionalità.
