Meteora Web

Programmazione a Oggetti in Python: Classi, Ereditarietà e Dataclass — Guida Operativa

Hai mai scritto decine di variabili sparse per gestire i prodotti di un e-commerce e ti sei ritrovato con un codice che solo tu capisci, e solo se lo leggi entro mezzogiorno? È lo stesso problema che...

Meteora Web Redazione 6 min di lettura Read in English
Programmazione a Oggetti in Python: Classi, Ereditarietà e Dataclass — Guida Operativa

Hai mai scritto decine di variabili sparse per gestire i prodotti di un e-commerce e ti sei ritrovato con un codice che solo tu capisci, e solo se lo leggi entro mezzogiorno? È lo stesso problema che abbiamo affrontato quando abbiamo preso in mano la gestione del sistema ERP di Hibrido Abbigliamento — decine di listini, taglie, stagioni, tutto da incrociare. Senza una struttura solida, ogni modifica diventa un incubo. La programmazione a oggetti in Python è quello scatolame organizzato che trasforma il caos in qualcosa di manutenibile, testabile e, soprattutto, economicamente sensato.

Noi, di Meteora Web, lavoriamo quotidianamente con Python per automatizzare processi, costruire piattaforme e integrare dati. In questa guida non troverai definizioni da manuale universitario: partiamo dal codice sporco e lo ripuliamo insieme, passo dopo passo.

Perché le classi salvano il fatturato (e la sanità mentale)

Un programma che gestisce prodotti, ordini o clienti senza classi assomiglia a un magazzino dove ogni scatola è buttata in mezzo al corridoio. Funziona fino a quando hai tre prodotti. Appena cresci, perdi tutto.

Le classi ti permettono di incapsulare dati e comportamenti in un unico oggetto. Se un prodotto ha nome, prezzo, quantità, e una funzione per calcolare lo sconto, mettili insieme. Il codice diventa auto-documentato, e quando qualcosa cambia — come il calcolo dell'IVA — modifichi un punto solo.

La classe più semplice che ha senso

Ecco come definiamo una classe Prodotto per il nostro e-commerce, senza fronzoli:

class Prodotto:
    def __init__(self, nome: str, prezzo: float, qta: int) -> None:
        self.nome = nome
        self.prezzo = prezzo
        self.qta = qta

    def valore_magazzino(self) -> float:
        return self.prezzo * self.qta

    def applica_sconto(self, percentuale: float) -> None:
        self.prezzo *= (1 - percentuale / 100)

__init__ è il costruttore: si chiama quando crei un nuovo prodotto. self è il riferimento all'istanza corrente — ogni oggetto ha le sue copie dei dati. I metodi valore_magazzino e applica_sconto operano su quei dati.

Proviamo sul campo:

maglietta = Prodotto("T-shirt bianca", 29.90, 50)
print(maglietta.valore_magazzino())  # 1495.0
maglietta.applica_sconto(20)
print(maglietta.prezzo)            # 23.92

Un metodo che aggiorna il prezzo? Pericoloso se non controllato. Nella realtà aggiungiamo validazione — ma per ora il concetto è chiaro: i dati e le operazioni sono accoppiati logicamente.

Errori comuni da evitare subito

  • Non usare variabili globali per lo stato — ogni istanza deve avere i propri attributi.
  • Non dimenticare __init__ — se non definisci il costruttore, puoi comunque aggiungere attributi dopo, ma diventa un caos.
  • Non mischiare logica di business con IO — la classe non deve stampare a schermo, ma restituire valori.

Cosa fare adesso: Apri un editor e crea la tua prima classe con __init__ e almeno un metodo che modifichi uno stato interno. Poi istanzia due oggetti con dati diversi e chiama i metodi.

Ereditarietà: non scrivere due volte la stessa logica

Nel mondo reale hai prodotti con comportamenti diversi: un capo di abbigliamento ha taglia e colore, un corso digitale ha durata e numero di lezioni. Senza ereditarietà, duplichi codice e moltiplichi gli errori.

L'ereditarietà ti permette di definire una classe base (es. Prodotto) e poi specializzare. La classe figlia eredita tutto, e può aggiungere o sovrascrivere metodi.

Esempio concreto: abbigliamento vs digitale

Partiamo dalla classe Prodotto di prima, e creiamo due figlie:

class ProdottoAbbigliamento(Prodotto):
    def __init__(self, nome: str, prezzo: float, qta: int,
                 taglia: str, colore: str) -> None:
        super().__init__(nome, prezzo, qta)
        self.taglia = taglia
        self.colore = colore

    def descrizione(self) -> str:
        return f"{self.nome} - Taglia {self.taglia}, {self.colore}"

class ProdottoDigitale(Prodotto):
    def __init__(self, nome: str, prezzo: float, durata_ore: float) -> None:
        super().__init__(nome, prezzo, qta=99999)  # scorta virtuale
        self.durata_ore = durata_ore

    def valore_magazzino(self) -> float:
        # Per i digitali il magazzino è illimitato, ritorniamo prezzo * 1
        return self.prezzo

Osserva: super().__init__ chiama il costruttore della classe madre. I metodi valore_magazzino sono sovrascritti (override) per adattare la logica al tipo di prodotto.

Ereditarietà in azione:

camicia = ProdottoAbbigliamento("Camicia azzurra", 45.00, 20, "L", "azzurro")
print(camicia.valore_magazzino())  # 900.0 (usa il metodo ereditato)
print(camicia.descrizione())       # "Camicia azzurra - Taglia L, azzurro"

corso = ProdottoDigitale("Corso Python", 199.00, 40.5)
print(corso.valore_magazzino())    # 199.0 (usa l'override)

Quando l'ereditarietà diventa un problema

Abbiamo visto troppi progetti con eredità a cinque livelli che nessuno capiva più. Regola pratica: se una classe figlia non usa quasi nulla della madre, forse stai forzando una gerarchia sbagliata. Preferisci la composizione (un oggetto che contiene un altro oggetto) all'eredità eccessiva.

Cosa fare adesso: Parti da una classe base e crea due classi derivate con almeno un metodo sovrascritto. Verifica che il polimorfismo funzioni: chiama lo stesso metodo su oggetti di tipo diverso e ottieni risultati differenti.

Dataclass: quando scrivere __init__ è noioso (e rischioso)

Le dataclass sono state introdotte in Python 3.7 per ridurre il boilerplate. Quando una classe serve solo a contenere dati, con pochi metodi, una dataclass ti evita di scrivere manualmente __init__, __repr__, __eq__ e altri metodi speciali.

Noi le usiamo spesso per modellare DTO (Data Transfer Object) — per esempio i dati che escono da un database PostgreSQL e vanno verso una view in Laravel Livewire o Vue.

Riscriviamo Prodotto come dataclass

from dataclasses import dataclass

@dataclass
class ProdottoDC:
    nome: str
    prezzo: float
    qta: int = 0

    def valore_magazzino(self) -> float:
        return self.prezzo * self.qta

    def applica_sconto(self, percentuale: float) -> None:
        self.prezzo *= (1 - percentuale / 100)

Il decoratore @dataclass genera automaticamente __init__ con i campi in ordine, tipi e valori di default. In più ottieni __repr__ (stampa leggibile) e __eq__ (confronto per valore).

Esempio:

p1 = ProdottoDC("Scarpe sportive", 89.90, 10)
p2 = ProdottoDC("Scarpe sportive", 89.90, 10)
print(p1)        # ProdottoDC(nome='Scarpe sportive', prezzo=89.9, qta=10)
print(p1 == p2)  # True

p1.applica_sconto(15)
print(p1.prezzo) # 76.415

Dataclass immutabili con frozen=True

Se non vuoi che i dati vengano modificati dopo la creazione (utile per sicurezza o per evitare effetti collaterali), usa frozen=True. Attenzione: poi non puoi più fare p1.prezzo = ... ma solo creare nuove istanze.

@dataclass(frozen=True)
class ProdottoImmutable:
    nome: str
    prezzo: float
    qta: int

    def valore_magazzino(self) -> float:
        return self.prezzo * self.qta

    # Non puoi avere metodi che modificano self!

Ereditarietà con dataclass

Funziona, ma con qualche accortezza. La classe figlia deve ridefinire tutti i campi della madre, oppure usare super() nella chiamata. Ecco un esempio di estensione:

@dataclass
class ProdottoAbbigliamentoDC(ProdottoDC):
    taglia: str
    colore: str

    def descrizione(self) -> str:
        return f"{self.nome} - {self.taglia}, {self.colore}"

Cosa fare adesso: Converti una delle tue vecchie classi che contengono solo dati in una dataclass. Confronta la lunghezza del codice prima e dopo. Aggiungi frozen=True se i dati non devono mai cambiare dopo l'inizializzazione.

In sintesi — cosa fare adesso

  1. Riscrivi un pezzo di codice esistente — prendi uno script che gestisce prodotti, ordini o clienti e trasformalo in classi. Anche se è un prototipo, inizia con una classe base.
  2. Applica l'ereditarietà solo dove serve — non forzare gerarchie. Se due classi condividono meno del 30% del codice, probabilmente conviene tenerle separate e usare composizione.
  3. Adotta le dataclass per i contenitori di dati — ogni volta che scrivi una classe con solo attributi e nessuna logica complessa, usa @dataclass. Ti risparmi errori typo in __init__ e codice boilerplate.
  4. Metti alla prova il polimorfismo — scrivi una funzione che accetta un oggetto di tipo Prodotto (base o figlio) e chiama un metodo comune. Verifica che il comportamento cambi in base alla classe reale.
  5. Misura il tempo perso in debugging — nota quanto tempo risparmi quando un errore è localizzato in una sola classe invece che in cento righe di codice procedurale. Il ritorno sull'investimento è immediato.

Noi, di Meteora Web, abbiamo costruito piattaforme per clienti del Sud Italia usando questi principi. Il codice ben strutturato non è un lusso estetico: è manutenzione che non paghi, bug che non cercano, e scalabilità che non ti fa buttare via tutto quando cresci. Non serve essere un ingegnere informatico per iniziare — bastano pazienza e un problema reale da risolvere.

Zenithby Meteora Web Il sistema operativo della tua attività. Social, clienti, prenotazioni e fatture in un'unica piattaforma. Palestre, barber, professionisti. Scopri Zenith → Demo gratis · senza carta

Leggi anche

Dal journal

Ultime guide