W3docs

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 abcABC e abstractmethod
  • 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 time

Il 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 area

La 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:

NomeDescrizione
ABCUna classe base di supporto. Ereditala per rendere astratta la tua classe.
abstractmethodUn decoratore che contrassegna un metodo come astratto.

Importali in questo modo:

from abc import ABC, abstractmethod

Creare 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):
        pass

Shape 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, perimeter

Implementare 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())            # 20

Sia 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 perimeter

Il 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: Meow

La 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.2743

Questo è 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 $100

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

Pratica

Pratica
What happens when you try to instantiate a class that inherits from ABC but does not implement all abstract methods?
What happens when you try to instantiate a class that inherits from ABC but does not implement all abstract methods?
Was this page helpful?