Classi Base Astratte in Python (ABC)
Scopri come le classi base astratte in Python (ABC) impongono un'interfaccia comune alle sottoclassi usando @abstractmethod, proprietà astratte e il modulo abc.
Una classe astratta è una classe che non può essere istanziata direttamente. Esiste esclusivamente per definire un'interfaccia condivisa — un insieme di metodi che ogni sottoclasse concreta deve implementare. Python fornisce questo meccanismo attraverso il modulo integrato abc (Abstract Base Classes).
Questo capitolo tratta:
- Perché esistono le classi astratte e quando usarle
- Il modulo
abc—ABCeabstractmethod - Imporre contratti sui metodi con
@abstractmethod - Proprietà astratte
- Metodi concreti all'interno di una classe astratta
- Classi astratte vs. sollevare
NotImplementedError - Un esempio dal mondo reale
Prima di leggere questo capitolo, assicurati di avere familiarità con le classi e gli oggetti Python e con l'ereditarietà.
Perché Usare le Classi Astratte?
Man mano che aggiungi più sottoclassi a una gerarchia, diventa facile dimenticare di implementare un metodo richiesto. Il problema emerge solo in fase di esecuzione, nel profondo del codice, rendendo il debugging doloroso.
Le classi base astratte risolvono questo problema spostando l'errore al momento in cui la classe viene istanziata — non al momento in cui viene chiamato il metodo mancante. Agiscono come un contratto: ogni sottoclasse concreta deve implementare ogni metodo astratto, oppure Python si rifiuterà di creare un oggetto da essa.
Classi astratte vs. sollevare NotImplementedError
Prima che il modulo abc esistesse, il pattern comune era quello di sollevare NotImplementedError in un metodo della classe base:
class Shape:
def area(self):
raise NotImplementedError("Subclasses must implement area()")
class Circle(Shape):
pass # forgot to implement area()
c = Circle() # No error yet — Python lets this through
c.area() # NotImplementedError only here, at call timeIl punto debole: Python crea l'oggetto senza problemi. L'errore è invisibile finché il metodo non viene effettivamente chiamato, il che potrebbe avvenire in una parte completamente diversa del programma.
Con una classe astratta, l'errore avviene immediatamente:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
class Circle(Shape):
pass # still missing area()
c = Circle()
# TypeError: Can't instantiate abstract class Circle
# with abstract method areaLa classe risulta errata nel momento stesso della creazione — esattamente il punto in cui il contratto è stato violato.
Il Modulo abc
Il modulo abc di Python fornisce due strumenti che utilizzerai continuamente:
| Nome | Descrizione |
|---|---|
ABC | Una classe base di supporto. Ereditala per rendere astratta la tua classe. |
abstractmethod | Un decoratore che contrassegna un metodo come astratto. |
Importali in questo modo:
from abc import ABC, abstractmethodCreare una classe astratta
Eredita da ABC e decora almeno un metodo con @abstractmethod:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
passShape ora dichiara che chiunque si definisca una forma deve fornire area() e perimeter(). Non puoi creare direttamente una Shape:
s = Shape()
# TypeError: Can't instantiate abstract class Shape
# with abstract methods area, perimeterImplementare una Classe Astratta
Una classe concreta è quella che eredita dalla classe astratta e implementa ogni metodo astratto. Solo allora puoi creare istanze:
Implementare una classe astratta con Circle e Rectangle
from abc import ABC, abstractmethod
import math
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return math.pi * self.radius ** 2
def perimeter(self):
return 2 * math.pi * self.radius
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
c = Circle(5)
r = Rectangle(4, 6)
print(round(c.area(), 4)) # 78.5398
print(round(c.perimeter(), 4)) # 31.4159
print(r.area()) # 24
print(r.perimeter()) # 20Sia Circle che Rectangle implementano ogni metodo astratto, quindi Python consente la loro istanziazione.
Cosa succede se si dimentica un metodo?
Se una sottoclasse implementa solo alcuni metodi astratti, rimane astratta — istanziarla genera un TypeError che indica il metodo mancante:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
class IncompleteShape(Shape):
def area(self):
return 0
# perimeter() not implemented
s = IncompleteShape()
# TypeError: Can't instantiate abstract class IncompleteShape
# with abstract method perimeterIl messaggio di errore ti indica esattamente quale metodo hai dimenticato — molto più utile di un generico crash al momento della chiamata.
Proprietà Astratte
Puoi rendere astratta anche una proprietà. Questo obbliga le sottoclassi a fornire una proprietà (o un attributo calcolato da una proprietà), non solo un metodo semplice. Usa @property e @abstractmethod insieme, con @property in cima:
Usare le proprietà astratte
from abc import ABC, abstractmethod
class Animal(ABC):
@property
@abstractmethod
def sound(self):
pass
def describe(self):
return f"I make the sound: {self.sound}"
class Dog(Animal):
@property
def sound(self):
return "Woof"
class Cat(Animal):
@property
def sound(self):
return "Meow"
dog = Dog()
cat = Cat()
print(dog.describe()) # I make the sound: Woof
print(cat.describe()) # I make the sound: MeowLa proprietà sound in ogni sottoclasse si comporta come un attributo di sola lettura dal punto di vista del chiamante, mentre la classe astratta garantisce che ogni sottoclasse concreta ne fornisca una.
Metodi Concreti in una Classe Astratta
Le classi astratte non si limitano ai metodi astratti. Possono contenere metodi completamente implementati (concreti) che tutte le sottoclassi condividono automaticamente. Questa è la differenza fondamentale tra le classi astratte e le semplici interfacce di altri linguaggi — puoi inserire la logica condivisa nella classe astratta:
Condividere logica comune tramite un metodo concreto
from abc import ABC, abstractmethod
class Logger(ABC):
def log(self, message):
"""Concrete method — shared by all subclasses."""
formatted = self.format_message(message)
self.write(formatted)
def format_message(self, message):
return f"[LOG] {message}"
@abstractmethod
def write(self, message):
"""Abstract — each subclass decides how to output."""
pass
class ConsoleLogger(Logger):
def write(self, message):
print(message)
class FileLogger(Logger):
def __init__(self):
self.entries = []
def write(self, message):
self.entries.append(message)
console = ConsoleLogger()
console.log("Server started") # prints: [LOG] Server started
file_log = FileLogger()
file_log.log("Database connected")
print(file_log.entries) # ['[LOG] Database connected']Qui log() e format_message() sono metodi concreti condivisi. Solo write() — la parte che differisce tra i backend — è astratta. Le sottoclassi non devono mai reimplementare la logica di formattazione.
isinstance() e le Classi Astratte
Un oggetto che è un'istanza di una sottoclasse concreta è considerato anche un'istanza della classe base astratta. Questo ti consente di scrivere funzioni che accettano qualsiasi oggetto conforme all'interfaccia, senza preoccuparsi del tipo specifico:
from abc import ABC, abstractmethod
import math
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return math.pi * self.radius ** 2
def perimeter(self):
return 2 * math.pi * self.radius
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
shapes = [Circle(3), Rectangle(4, 5)]
for shape in shapes:
print(isinstance(shape, Shape)) # True for both
def total_area(shapes):
return sum(s.area() for s in shapes)
print(round(total_area(shapes), 4)) # 48.2743Questo è il vantaggio principale: total_area() funziona con qualsiasi Shape — passata, presente o futura — purché l'oggetto implementi l'interfaccia.
Esempio dal Mondo Reale: Processori di Pagamento
Le classi astratte brillano quando hai bisogno di più implementazioni dello stesso concetto. Considera un sistema di pagamento che deve supportare diversi provider:
Classe astratta per l'elaborazione dei pagamenti
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
@abstractmethod
def charge(self, amount):
pass
@abstractmethod
def refund(self, amount):
pass
def process(self, amount):
"""Concrete method — shared processing logic."""
print(f"Processing payment of ${amount}")
self.charge(amount)
class StripeProcessor(PaymentProcessor):
def charge(self, amount):
print(f"Stripe: charged ${amount}")
def refund(self, amount):
print(f"Stripe: refunded ${amount}")
class PayPalProcessor(PaymentProcessor):
def charge(self, amount):
print(f"PayPal: charged ${amount}")
def refund(self, amount):
print(f"PayPal: refunded ${amount}")
processors = [StripeProcessor(), PayPalProcessor()]
for p in processors:
p.process(100)
# Processing payment of $100
# Stripe: charged $100
# Processing payment of $100
# PayPal: charged $100Aggiungere un terzo provider di pagamento — ad esempio BraintreeProcessor — richiede solo di implementare charge() e refund(). La logica di process() e qualsiasi controllo isinstance continueranno a funzionare senza modifiche.
Quando Usare le Classi Astratte
Usa una classe astratta quando:
- Hai una famiglia di classi correlate che devono tutte fornire determinati metodi.
- Vuoi condividere del codice (metodi concreti) imponendo al contempo che altri metodi vengano sovrascritti.
- Vuoi che Python sollevi un errore immediatamente se una sottoclasse è incompleta, invece di aspettare un crash al momento della chiamata.
Probabilmente non hai bisogno di una classe astratta quando:
- Hai solo una o due sottoclassi e la gerarchia difficilmente crescerà.
- L'interfaccia condivisa è già imposta con altri mezzi (ad esempio, Python dataclasses o il duck typing).
- Vuoi semplicemente impedire l'istanziazione diretta — una soluzione più semplice è un hook
__init_subclass__o un semplice commento.
Per concetti strettamente correlati, vedi ereditarietà Python e polimorfismo Python.