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:
Sponsored Protocol
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.
Sponsored Protocol
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.
Sponsored Protocol
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).
Sponsored Protocol
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.
Sponsored Protocol
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.