Classi e OOP
Modella clienti, pratiche e fatture dello studio legale con classi Python: ereditarietà, metodi speciali e composizione.
Classi e OOP
Lo studio legale dell'Avv. Marco Rossi gestisce clienti, pratiche e fatture. Finora abbiamo usato dizionari per rappresentare questi dati, ma quando le relazioni diventano più complesse serve uno strumento migliore. Le classi permettono di raggruppare dati e comportamenti in un unico oggetto, rendendo il codice più organizzato e manutenibile.
La prima classe: Cliente
Partiamo dal dizionario cliente che già conosciamo e trasformiamolo in una classe con gli stessi campi: nome, cognome, codice_fiscale e tipo.
class Cliente:
def __init__(self, nome, cognome, codice_fiscale, tipo="privato"):
self.nome = nome
self.cognome = cognome
self.codice_fiscale = codice_fiscale
self.tipo = tipo # "privato" o "aziendale"
def nome_completo(self):
return f"{self.nome} {self.cognome}"
cliente = Cliente("Laura", "Verdi", "VRDLRA85M41H501Z")
print(cliente.nome_completo()) # Laura Verdi
print(cliente.tipo) # privatoIl metodo __init__ è il costruttore: viene eseguito quando crei un nuovo oggetto. self è il riferimento all'istanza corrente, simile a this in altri linguaggi.
Ereditarietà: ClientePrivato e ClienteAziendale
Un cliente privato e uno aziendale condividono i campi base, ma hanno informazioni aggiuntive diverse. L'ereditarietà evita di duplicare codice.
class ClientePrivato(Cliente):
def __init__(self, nome, cognome, codice_fiscale, telefono):
super().__init__(nome, cognome, codice_fiscale, tipo="privato")
self.telefono = telefono
class ClienteAziendale(Cliente):
def __init__(self, nome, cognome, codice_fiscale, partita_iva, ragione_sociale):
super().__init__(nome, cognome, codice_fiscale, tipo="aziendale")
self.partita_iva = partita_iva
self.ragione_sociale = ragione_sociale
privato = ClientePrivato("Marco", "Rossi", "RSSMRC80A01H501X", "333-1234567")
azienda = ClienteAziendale("Sara", "Conti", "CNTSAR75B42H501Y", "IT01234567890", "Conti & Associati")
print(privato.nome_completo()) # Marco Rossi
print(azienda.ragione_sociale) # Conti & Associatisuper().__init__(...) chiama il costruttore della classe genitore, così non ripetiamo l'inizializzazione dei campi comuni.
Metodi speciali: __str__ e __repr__
I dunder methods (double underscore) personalizzano come Python tratta i tuoi oggetti. __str__ definisce la rappresentazione leggibile, __repr__ quella tecnica utile per il debug.
class Cliente:
def __init__(self, nome, cognome, codice_fiscale, tipo="privato"):
self.nome = nome
self.cognome = cognome
self.codice_fiscale = codice_fiscale
self.tipo = tipo
def __str__(self):
return f"{self.nome} {self.cognome} ({self.tipo})"
def __repr__(self):
return f"Cliente(nome='{self.nome}', cognome='{self.cognome}', cf='{self.codice_fiscale}')"
cliente = Cliente("Laura", "Verdi", "VRDLRA85M41H501Z")
print(cliente) # Laura Verdi (privato)
print(repr(cliente)) # Cliente(nome='Laura', cognome='Verdi', cf='VRDLRA85M41H501Z')Composizione: una Pratica contiene un Cliente
La composizione è il pattern dove un oggetto contiene un altro oggetto. Una pratica legale è legata a un cliente specifico: non ha senso senza.
class Pratica:
def __init__(self, numero, cliente, descrizione, tariffa_oraria=150.0):
self.numero = numero
self.cliente = cliente # oggetto Cliente
self.descrizione = descrizione
self.tariffa_oraria = tariffa_oraria
self.ore_lavorate = 0
self.stato = "aperta"
def registra_ore(self, ore):
self.ore_lavorate += ore
def calcola_importo(self):
return self.ore_lavorate * self.tariffa_oraria
def __str__(self):
return f"Pratica {self.numero}: {self.descrizione} - {self.cliente.nome_completo()}"
# Creiamo cliente e pratica collegati
cliente = Cliente("Laura", "Verdi", "VRDLRA85M41H501Z")
pratica = Pratica("2024-001", cliente, "Contenzioso civile")
pratica.registra_ore(12)
print(pratica) # Pratica 2024-001: Contenzioso civile - Laura Verdi
print(f"Importo: {pratica.calcola_importo():.2f} EUR") # Importo: 1800.00 EUREsempio completo: sistema di fatturazione
Ora colleghiamo tutto. Una Fattura nasce da una Pratica, che a sua volta è collegata a un Cliente. Questo è il vantaggio della OOP: ogni oggetto ha la propria responsabilità.
class Fattura:
_contatore = 0 # variabile di classe, condivisa tra tutte le istanze
def __init__(self, pratica, aliquota_iva=22):
Fattura._contatore += 1
self.numero = f"FT-{Fattura._contatore:04d}"
self.pratica = pratica # oggetto Pratica
self.aliquota_iva = aliquota_iva
self.importo_netto = pratica.calcola_importo()
self.importo_iva = self.importo_netto * aliquota_iva / 100
self.importo_totale = self.importo_netto + self.importo_iva
def __str__(self):
return (
f"Fattura {self.numero}\n"
f"Cliente: {self.pratica.cliente.nome_completo()}\n"
f"Pratica: {self.pratica.descrizione}\n"
f"Netto: {self.importo_netto:.2f} EUR\n"
f"IVA ({self.aliquota_iva}%): {self.importo_iva:.2f} EUR\n"
f"Totale: {self.importo_totale:.2f} EUR"
)
# Pipeline completa: Cliente -> Pratica -> Fattura
cliente = Cliente("Laura", "Verdi", "VRDLRA85M41H501Z")
pratica = Pratica("2024-001", cliente, "Contenzioso civile")
pratica.registra_ore(12)
fattura = Fattura(pratica)
print(fattura)
# Fattura FT-0001
# Cliente: Laura Verdi
# Pratica: Contenzioso civile
# Netto: 1800.00 EUR
# IVA (22%): 396.00 EUR
# Totale: 2196.00 EURDa ricordare
- ●Una classe raggruppa dati (attributi) e comportamenti (metodi) in un unico oggetto
- ●Usa l'ereditarietà quando due classi condividono una base comune, ma non esagerare con la profondità
- ●I metodi speciali come
__str__e__repr__rendono gli oggetti più facili da usare e debuggare - ●Preferisci la composizione (un oggetto contiene un altro) all'ereditarietà profonda
- ●Il pattern Cliente -> Pratica -> Fattura mostra come le classi modellano relazioni reali tra entità