Programmazione a oggetti in Python senza complicare il codice

21 maggio 2026

Copertina libro "Programmazione Oggetti" che illustra OOP in Python, Java, C++ e Php.

Indice

Quando uno script Python cresce, funzioni e dizionari iniziano spesso a diventare difficili da mantenere. L’approccio a oggetti aiuta a riunire dati e comportamenti in strutture coerenti, con esempi utili anche per applicazioni che leggono database, gestiscono API o elaborano dati. In questa guida mostro come funzionano classi, istanze, ereditarietà, polimorfismo e composizione, indicando anche gli errori che vedo più spesso nei progetti reali.

Le idee essenziali per usare bene la programmazione a oggetti

  • Classe e istanza non sono la stessa cosa: la prima definisce una struttura, la seconda rappresenta un oggetto concreto.
  • Incapsulamento significa proteggere lo stato interno e controllare come viene modificato.
  • Ereditarietà e polimorfismo sono utili, ma non vanno introdotti automaticamente in ogni progetto.
  • Composizione e dipendenze ben separate rendono spesso il codice più semplice da testare.
  • Per modelli di dati essenziali, dataclass può ridurre il codice ripetitivo senza sacrificare la chiarezza.

Diagramma OOP Python: Encapsulation, Inheritance, Polymorphism, Abstraction. Concetti chiave per programmare in modo efficiente.

Che cosa significa usare l’OOP in Python

La programmazione orientata agli oggetti organizza il software intorno a oggetti che possiedono dati e operazioni. Un oggetto può rappresentare un cliente, un ordine, una connessione al database o un documento, mentre i suoi metodi descrivono ciò che quell’oggetto sa fare.

In Python una classe è il modello da cui vengono create le istanze. La classe definisce attributi e metodi, ma non rappresenta ancora un elemento concreto. Quando creo un’istanza, invece, ottengo un oggetto con un proprio stato.

class Cliente:
    def __init__(self, nome, email):
        self.nome = nome
        self.email = email

    def descrizione(self):
        return f"{self.nome} <{self.email}>"

cliente = Cliente("Giulia Rossi", "giulia@example.it")
print(cliente.descrizione())

Il parametro self indica l’istanza corrente. Non è una parola riservata, ma è la convenzione obbligatoria da rispettare per rendere leggibile il codice. Il metodo __init__ viene chiamato quando creo l’oggetto e serve a inizializzarne lo stato.

Il vantaggio concreto non è semplicemente avere “un codice più ordinato”. Una classe ben progettata stabilisce quali operazioni sono consentite e mantiene vicini i dati e le regole che li riguardano. Questo diventa particolarmente utile quando la stessa logica viene riutilizzata in più punti dell’applicazione.

I quattro concetti da capire davvero

Incapsulamento

L’incapsulamento raccoglie stato e comportamento nello stesso oggetto e limita l’accesso diretto alle parti delicate. Python non impone una vera visibilità privata come alcuni linguaggi compilati, ma usa convenzioni molto chiare. Un attributo preceduto da un singolo underscore, come _saldo, segnala che dovrebbe essere usato internamente.

class Conto:
    def __init__(self, saldo_iniziale=0):
        self._saldo = saldo_iniziale

    def deposita(self, importo):
        if importo <= 0:
            raise ValueError("L'importo deve essere positivo")
        self._saldo += importo

    @property
    def saldo(self):
        return self._saldo

Qui il saldo non viene modificato liberamente dall’esterno. Il metodo deposita controlla l’input e impedisce di inserire valori non validi. È un esempio piccolo, ma mostra una regola che considero fondamentale: le invarianti, cioè le condizioni che devono rimanere sempre vere, vanno protette vicino ai dati a cui si riferiscono.

Ereditarietà

Con l’ereditarietà una classe può riutilizzare o specializzare il comportamento di un’altra. Una classe derivata può ridefinire un metodo e chiamare l’implementazione originale con super().

class Notifica:
    def invia(self, messaggio):
        raise NotImplementedError

class EmailNotifica(Notifica):
    def invia(self, messaggio):
        return f"Invio email: {messaggio}"

class SmsNotifica(Notifica):
    def invia(self, messaggio):
        return f"Invio SMS: {messaggio}"

L’ereditarietà ha senso quando esiste una relazione stabile di tipo “è un”. Un SMS e un’email possono essere tipi di notifica, ma non conviene creare gerarchie profonde soltanto per evitare qualche riga duplicata. Nella pratica, una o due relazioni semplici sono spesso più sostenibili di una catena con molte classi base.

Polimorfismo

Il polimorfismo permette di usare oggetti diversi attraverso lo stesso comportamento. Una funzione non deve necessariamente sapere se riceve un’email o un SMS, purché l’oggetto abbia un metodo invia compatibile.

def invia_notifica(canale, messaggio):
    return canale.invia(messaggio)

canali = [EmailNotifica(), SmsNotifica()]

for canale in canali:
    print(invia_notifica(canale, "Report pronto"))

Python sfrutta spesso il cosiddetto duck typing: conta ciò che un oggetto sa fare, non la classe da cui deriva. È una delle caratteristiche più pratiche del linguaggio, perché evita controlli rigidi come “se l’oggetto è di questo tipo, fai questo”.

Astrazione

L’astrazione nasconde i dettagli non necessari e presenta un’interfaccia più semplice. Le classi astratte del modulo abc sono utili quando voglio imporre un contratto minimo alle implementazioni concrete.

from abc import ABC, abstractmethod

class Repository(ABC):
    @abstractmethod
    def salva(self, elemento):
        pass

    @abstractmethod
    def trova(self, identificativo):
        pass

Non userei una classe astratta per ogni piccola funzione. È una scelta sensata quando più implementazioni devono rispettare gli stessi metodi, per esempio un repository SQLite, uno PostgreSQL e uno fittizio usato nei test.

Un esempio concreto con database e repository

Il collegamento con i database è uno degli impieghi più utili dell’approccio a oggetti. Invece di distribuire query SQL in tutto il programma, posso creare un oggetto che si occupa di persistenza, cioè di salvare e recuperare i dati.

from dataclasses import dataclass
import sqlite3

@dataclass
class Prodotto:
    id: int | None
    nome: str
    prezzo: float

class ProdottoRepository:
    def __init__(self, connessione):
        self.connessione = connessione

    def crea_tabella(self):
        self.connessione.execute("""
            CREATE TABLE IF NOT EXISTS prodotti (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                nome TEXT NOT NULL,
                prezzo REAL NOT NULL
            )
        """)
        self.connessione.commit()

    def salva(self, prodotto):
        cursore = self.connessione.execute(
            "INSERT INTO prodotti (nome, prezzo) VALUES (?, ?)",
            (prodotto.nome, prodotto.prezzo)
        )
        self.connessione.commit()
        return Prodotto(cursore.lastrowid, prodotto.nome, prodotto.prezzo)

    def tutti(self):
        righe = self.connessione.execute(
            "SELECT id, nome, prezzo FROM prodotti ORDER BY id"
        )
        return [Prodotto(*riga) for riga in righe]

In questo esempio la dataclass rappresenta il dato, mentre il repository contiene le operazioni SQL. La separazione è importante perché il resto dell’applicazione non deve conoscere la sintassi delle query o il modo in cui viene aperta la connessione.

La query usa i parametri invece di concatenare stringhe. È una scelta essenziale per evitare l’iniezione SQL, cioè l’inserimento di istruzioni non previste attraverso dati forniti dall’utente.

Per un progetto piccolo SQLite può essere sufficiente. Se l’applicazione deve gestire molte connessioni concorrenti, transazioni complesse o più processi, sarà invece necessario valutare un database server e una gestione più accurata del ciclo di vita delle connessioni.

Perché questo schema è utile

  • Il modello Prodotto contiene i dati senza conoscere il database.
  • Il repository concentra le query e rende più semplice sostituire SQLite.
  • I test possono usare un database temporaneo o un repository fittizio.
  • La logica applicativa resta separata dai dettagli tecnici della persistenza.

Non bisogna però trasformare ogni singola tabella in una gerarchia di classi. Il valore del design sta nei confini chiari, non nel numero di oggetti creati.

Composizione, dataclass e scelta del design

Quando progetto una classe, mi chiedo prima se l’oggetto debba davvero ereditare da un altro. Spesso la soluzione più flessibile è la composizione, cioè costruire un oggetto mettendo al suo interno altri oggetti specializzati.

class RegistroOrdini:
    def __init__(self, repository, servizio_pagamenti):
        self.repository = repository
        self.servizio_pagamenti = servizio_pagamenti

    def registra(self, ordine):
        self.servizio_pagamenti.autorizza(ordine.totale)
        return self.repository.salva(ordine)

Il registro non sa come funziona il pagamento e non sa dove vengono salvati gli ordini. Dipende solo da due interfacce operative. Questa struttura facilita i test, perché posso sostituire il servizio reale con un mock, cioè un oggetto simulato che riproduce il comportamento necessario.

Le dataclass sono invece adatte quando una classe serve soprattutto a trasportare dati. Generano automaticamente metodi come il costruttore e la rappresentazione testuale, riducendo il codice ripetitivo.

from dataclasses import dataclass

@dataclass
class Coordinate:
    latitudine: float
    longitudine: float

punto = Coordinate(41.9028, 12.4964)

Non userei una dataclass per nascondere regole di business complesse. Se l’oggetto deve validare transizioni, coordinare servizi o proteggere uno stato delicato, una classe tradizionale con metodi espliciti comunica meglio l’intenzione.

Scelta Quando usarla Rischio tipico
Funzioni semplici Logica breve e senza stato persistente Distribuire dati correlati in molti argomenti
Dataclass Modelli dati chiari e leggeri Aggiungere logica complessa senza struttura
Ereditarietà Contratto comune e relazione stabile tra tipi Gerarchie profonde e rigide
Composizione Servizi sostituibili e responsabilità separate Creare troppe dipendenze senza un disegno coerente

Gli errori che rendono fragile il codice a oggetti

Il primo errore è creare una classe per ogni concetto, anche quando una funzione sarebbe più leggibile. L’OOP non è un obbligo: Python supporta bene anche stili procedurali e funzionali, e molti moduli efficaci combinano più approcci.

Un secondo problema è la classe “onnipotente”, quella che apre il database, valida gli input, invia email, calcola prezzi e prepara le risposte HTTP. Quando una classe ha troppe responsabilità, ogni modifica diventa rischiosa. Preferisco dividere il lavoro in componenti piccoli e con uno scopo riconoscibile.

Bisogna poi fare attenzione agli attributi di classe. Se assegno una lista direttamente nel corpo della classe, quella lista può essere condivisa da tutte le istanze.

class Carrello:
    prodotti = []  # errore frequente

class CarrelloCorretto:
    def __init__(self):
        self.prodotti = []

Nel primo caso ogni carrello usa lo stesso elenco. Nel secondo ogni istanza ha il proprio stato. È un dettaglio semplice, ma può generare bug difficili da individuare quando il programma gestisce più utenti.

Anche l’ereditarietà multipla richiede cautela. Python la supporta, ma l’ordine di risoluzione dei metodi può diventare difficile da seguire. Prima di introdurla, valuterei se la composizione o i mixin, classi piccole che aggiungono una capacità ben delimitata, offrano una soluzione più comprensibile.

Leggi anche: Padding in informatica - memoria, crittografia e database

Come verificare una buona progettazione

Un test pratico consiste nel provare a descrivere ogni classe con una sola frase. Se devo usare molti “e”, probabilmente la classe sta facendo troppo. Controllo anche se posso testare la logica senza avviare l’intera applicazione o senza collegarmi a un database reale.

  • Il costruttore dovrebbe ricevere solo dipendenze davvero necessarie.
  • I metodi dovrebbero avere nomi espliciti e produrre effetti prevedibili.
  • Gli errori di input dovrebbero essere segnalati vicino al punto in cui vengono rilevati.
  • Le query e le connessioni non dovrebbero essere sparse nella logica di business.

Un criterio semplice per iniziare senza complicare il progetto

Io partirei da funzioni e strutture dati semplici, osservando dove compaiono ripetizioni o regole che appartengono chiaramente allo stesso concetto. Solo in quel momento trasformerei quella parte in una classe. È un percorso più naturale rispetto al progettare subito una grande gerarchia.

Per esercitarti, puoi modellare un piccolo catalogo, un sistema di prenotazioni o una gestione di ordini. Parti da una dataclass per i dati, aggiungi un repository per il database e introduci il polimorfismo soltanto quando esistono davvero più comportamenti intercambiabili.

La programmazione a oggetti in Python funziona bene quando rende il codice più leggibile, testabile e modificabile. Se invece aggiunge classi, ereditarietà e livelli di astrazione senza risolvere un problema concreto, è probabilmente il design a guidare il programma invece del contrario.

Domande frequenti

La classe definisce attributi e metodi, mentre l’istanza è un oggetto concreto con un proprio stato. Il metodo __init__ viene usato per inizializzare l’istanza e self indica l’oggetto corrente.

La composizione è spesso preferibile quando un oggetto deve usare servizi sostituibili, come un repository o un servizio di pagamento. L’ereditarietà è più adatta quando esiste una relazione stabile di tipo “è un” e più classi devono rispettare lo stesso contratto.

Una dataclass può rappresentare i dati, mentre un repository può contenere le query e le operazioni di persistenza. In questo modo il modello non conosce il database e i test possono usare una connessione temporanea o un repository fittizio.

È meglio non creare una classe per ogni concetto né concentrare database, validazione, email e risposte HTTP in una classe onnipotente. Bisogna inoltre inizializzare le liste nelle istanze, perché una lista definita nel corpo della classe può essere condivisa da tutti gli oggetti.

Valuta l'articolo

Valutazione: 0.00 Numero di voti: 0

Tag:

classi composizione ereditarietà polimorfismo dataclass

Condividi post

Xavier Grassi

Xavier Grassi

Mi chiamo Xavier Grassi e dedico la mia attività professionale al mondo dell'informatica, con un focus particolare sull'intelligenza artificiale e le soluzioni cloud. Ho accumulato quindici anni di esperienza in questo settore, un percorso che mi ha permesso di approfondire le dinamiche di queste tecnologie in continua evoluzione. La mia passione è quella di rendere accessibili concetti complessi, analizzando le tendenze emergenti e verificando le informazioni per offrire contenuti chiari e affidabili. In bartolomeoalberico.it, mi impegno a fornire spunti utili e aggiornati, aiutando i lettori a navigare nel panorama tecnologico attuale con maggiore consapevolezza.

Scrivi un commento