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.92Un 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.prezzoOsserva: 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.415Dataclass 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
- 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.
- 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.
- 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. - 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.
- 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.






