La Programmazione Orientata agli Oggetti (OOP, Object-Oriented Programming) è uno dei principali paradigmi utilizzati per progettare software complessi.
In Python le classi permettono di creare nuovi tipi di oggetti che combinano:
L'obiettivo non è semplicemente imparare la sintassi di class, ma
imparare a modellare un problema attraverso oggetti che collaborano
tra loro.
Questo capitolo parte dalle classi più semplici e arriva a:
super();dataclass;Prerequisiti: funzioni, strutture dati,
*args,**kwargs, eccezioni di base e type hints.
Una classe è una definizione che descrive come saranno fatti gli oggetti di un determinato tipo.
Un oggetto creato a partire da una classe è un'istanza.
class Persona:
pass
Possiamo creare due istanze:
p1 = Persona()
p2 = Persona()
p1 e p2 appartengono alla stessa classe, ma sono due oggetti
distinti.
Possiamo verificare il tipo:
print(type(p1))
e verificare se un oggetto è istanza di una classe:
print(isinstance(p1, Persona))
È utile distinguere immediatamente:
Un'analogia semplice:
La classe è il progetto di una casa; l'istanza è una casa costruita a partire da quel progetto.
Definisci una classe Libro vuota e crea due istanze libro1 e
libro2.
Verifica con isinstance() che entrambe siano istanze di Libro.
La sintassi fondamentale è:
class NomeClasse:
# corpo della classe
pass
Per convenzione i nomi delle classi utilizzano il PascalCase:
class Studente:
pass
class ContoBancario:
pass
class ProdottoDigitale:
pass
All'interno di una classe possiamo definire attributi e metodi.
class Studente:
def saluta(self):
print("Ciao, sono uno studente")
Creiamo un'istanza:
s = Studente()
s.saluta()
Output:
Ciao, sono uno studente
selfNei metodi di istanza il primo parametro è normalmente chiamato self.
class Persona:
def saluta(self):
print("Ciao!")
Quando scriviamo:
persona.saluta()
Python passa automaticamente l'istanza come primo argomento.
Concettualmente possiamo immaginare:
Persona.saluta(persona)
self rappresenta quindi l'istanza corrente.
È importante capire che self non è una parola chiave riservata: è una
convenzione obbligatoria nella pratica Python. Il nome potrebbe
tecnicamente essere diverso, ma usare self è lo standard.
self permette di accedere allo stato dell'oggettoclass Persona:
def __init__(self, nome):
self.nome = nome
def saluta(self):
print(f"Ciao, sono {self.nome}")
Ogni istanza possiede il proprio nome.
p1 = Persona("Anna")
p2 = Persona("Marco")
p1.saluta()
p2.saluta()
Output:
Ciao, sono Anna
Ciao, sono Marco
Crea una classe Lampada con un metodo accendi() che stampi:
La lampada è accesa
Crea un'istanza e richiama il metodo.
__init__Quando si crea un'istanza è spesso necessario inizializzarla con alcuni dati.
Python mette a disposizione il metodo speciale __init__().
class Persona:
def __init__(self, nome, eta):
self.nome = nome
self.eta = eta
Ora:
p = Persona("Mario", 30)
Durante la creazione dell'istanza Python esegue __init__() per
inizializzare l'oggetto.
Possiamo poi usare gli attributi:
print(p.nome)
print(p.eta)
Questa istruzione:
self.nome = nome
contiene due elementi diversi:
nome a destra → parametro del metodo;self.nome a sinistra → attributo dell'istanza.Un esempio più completo:
class Studente:
def __init__(self, nome, eta):
self.nome = nome
self.eta = eta
def presenta(self):
print(f"Sono {self.nome} e ho {self.eta} anni")
Uso:
s = Studente("Anna", 18)
s.presenta()
Come nelle normali funzioni, possiamo utilizzare valori di default:
class Studente:
def __init__(self, nome, eta=18):
self.nome = nome
self.eta = eta
Quindi entrambe sono valide:
s1 = Studente("Anna")
s2 = Studente("Marco", 21)
__init__ non è il vero metodo di costruzioneIn termini tecnici, __init__() inizializza un'istanza già creata;
non è il metodo che alloca l'oggetto. Per la maggior parte dei programmi
didattici e applicativi è comunque corretto chiamarlo comunemente
"costruttore".
Crea una classe Prodotto con:
nome;prezzo;descrizione().Esempio:
p = Prodotto("Mouse", 25.90)
p.descrizione()
dovrebbe stampare una descrizione leggibile del prodotto.
Gli attributi preceduti da self. appartengono normalmente alla singola
istanza.
class Studente:
def __init__(self, nome, voto):
self.nome = nome
self.voto = voto
Possiamo creare:
s1 = Studente("Anna", 8)
s2 = Studente("Marco", 6)
Gli oggetti hanno valori indipendenti:
print(s1.voto) # 8
print(s2.voto) # 6
Modificare s1.voto non modifica s2.voto.
s1.voto = 10
print(s1.voto) # 10
print(s2.voto) # 6
Un attributo definito direttamente nel corpo della classe è un attributo di classe.
class Studente:
scuola = "Istituto Tecnico"
def __init__(self, nome):
self.nome = nome
Possiamo accedervi attraverso la classe:
print(Studente.scuola)
oppure attraverso un'istanza:
s = Studente("Anna")
print(s.scuola)
Se non esiste un attributo scuola nell'istanza, Python cerca quello
definito nella classe.
Studente.scuola = "Liceo"
Le istanze che non hanno un proprio scuola vedranno il nuovo valore.
Questa istruzione:
s.scuola = "Università"
non modifica necessariamente l'attributo di classe. Crea invece un attributo di istanza che nasconde quello di classe per quell'oggetto.
s1 = Studente("Anna")
s2 = Studente("Marco")
s1.scuola = "Università"
print(s1.scuola) # Università
print(s2.scuola) # Liceo
print(Studente.scuola) # Liceo
Sono adatti quando un'informazione è effettivamente comune alla classe:
class Dipendente:
azienda = "ACME"
Non sono invece adatti per dati che devono essere indipendenti per ogni oggetto.
Crea una classe Auto con:
numero_ruote = 4;marca e modello.Crea due automobili e verifica la differenza tra i due tipi di attributo.
I metodi di istanza lavorano normalmente sullo stato di una specifica
istanza e ricevono self.
class Conto:
def __init__(self, intestatario, saldo=0):
self.intestatario = intestatario
self.saldo = saldo
def deposita(self, importo):
self.saldo += importo
def visualizza_saldo(self):
print(f"Saldo: {self.saldo:.2f} €")
Uso:
conto = Conto("Anna", 100)
conto.deposita(50)
conto.visualizza_saldo()
I metodi permettono quindi di associare comportamento e dati.
@classmethodUn metodo di classe riceve la classe attraverso cls.
class Studente:
scuola = "ITIS"
@classmethod
def cambia_scuola(cls, nuova_scuola):
cls.scuola = nuova_scuola
Possiamo chiamarlo senza creare un'istanza:
Studente.cambia_scuola("Liceo")
Un uso particolarmente interessante di @classmethod è creare
costruttori alternativi.
class Studente:
def __init__(self, nome, eta):
self.nome = nome
self.eta = eta
@classmethod
def da_stringa(cls, testo):
nome, eta = testo.split("-")
return cls(nome, int(eta))
Uso:
s = Studente.da_stringa("Marco-18")
print(s.nome)
print(s.eta)
cls(...) è preferibile a Studente(...) perché permette alla factory
di funzionare correttamente anche con eventuali sottoclassi.
Crea una classe Data con giorno, mese e anno.
Aggiungi un metodo di classe:
da_stringa("25/12/2026")
che restituisca una nuova istanza.
@staticmethodUn metodo statico non riceve automaticamente né self né cls.
class Matematica:
@staticmethod
def somma(a, b):
return a + b
Uso:
print(Matematica.somma(3, 5))
La funzione non dipende dallo stato di una specifica istanza né dalla classe.
È quindi una normale funzione collocata nella classe perché è concettualmente collegata a quella classe.
Tipo Primo parametro automatico Usa lo stato dell'istanza?
metodo di istanza self sì
metodo di classe cls no, usa la classe
metodo statico nessuno no
@propertyUna property permette di esporre un valore attraverso una sintassi simile a quella di un attributo, mantenendo però la possibilità di eseguire logica.
Esempio:
class Studente:
def __init__(self, nome, eta):
self.nome = nome
self.eta = eta
Se vogliamo impedire età negative:
class Studente:
def __init__(self, nome, eta):
self.nome = nome
self.eta = eta
@property
def eta(self):
return self._eta
@eta.setter
def eta(self, valore):
if valore < 0:
raise ValueError("L'età non può essere negativa")
self._eta = valore
Uso:
s = Studente("Marco", 18)
print(s.eta)
s.eta = 20
La sintassi rimane semplice:
s.eta
s.eta = 20
ma dietro le quinte viene eseguita la logica della property.
_eta?Nel setter non possiamo fare:
self.eta = valore
perché richiameremmo nuovamente il setter, creando una ricorsione infinita.
Utilizziamo quindi un attributo interno:
self._eta = valore
Se definiamo solo il getter:
class Persona:
def __init__(self, nome):
self._nome = nome
@property
def nome(self):
return self._nome
possiamo leggere:
print(p.nome)
ma non assegnare:
p.nome = "Marco"
Una property non deve necessariamente rappresentare un dato memorizzato.
class Rettangolo:
def __init__(self, base, altezza):
self.base = base
self.altezza = altezza
@property
def area(self):
return self.base * self.altezza
Uso:
r = Rettangolo(5, 3)
print(r.area)
area viene calcolata quando viene richiesta.
Crea una classe Prodotto con una property prezzo che impedisca
valori minori di zero.
Aggiungi una property prezzo_scontato che calcoli il prezzo applicando
una percentuale di sconto memorizzata nell'oggetto.
Python non implementa il concetto di private nello stesso modo di Java
o C++.
Esistono però due convenzioni importanti.
_nomeclass Conto:
def __init__(self, saldo):
self._saldo = saldo
L'underscore iniziale comunica:
Questo membro è destinato all'uso interno della classe o del modulo.
Non è un vero blocco all'accesso.
__nomeclass Conto:
def __init__(self, saldo):
self.__saldo = saldo
Python applica il name mangling.
Il nome viene internamente trasformato in qualcosa di simile a:
_Conto__saldo
Quindi:
conto.__saldo
produce un AttributeError, mentre tecnicamente l'attributo può ancora
essere raggiunto tramite il nome mangled.
Questo meccanismo serve soprattutto a evitare collisioni accidentali con nomi definiti da sottoclassi, non a creare una barriera di sicurezza.
Usa:
nome → membro pubblico;_nome → dettaglio interno/convenzione;__nome → name mangling, quando serve evitare collisioni o accessi
accidentali.Per controllare realmente lettura e modifica, spesso è preferibile usare
@property.
Crea una classe ContoBancario con _saldo.
Non permettere di impostare direttamente un saldo negativo: usa un
metodo preleva() che controlli la disponibilità.
I metodi speciali, spesso chiamati dunder methods ("double underscore"), permettono di definire come un oggetto interagisce con il linguaggio Python.
Alcuni esempi:
__init__ → inizializzazione;__str__ → rappresentazione leggibile;__repr__ → rappresentazione tecnica;__len__ → comportamento di len();__eq__ → ==;__lt__ → <;__add__ → +;__getitem__ → obj[indice];__iter__ → iterazione.Questi metodi non vengono normalmente chiamati direttamente. È il linguaggio a utilizzarli quando eseguiamo determinate operazioni.
__str__ e __repr__Senza __str__:
class Persona:
def __init__(self, nome):
self.nome = nome
potremmo ottenere una rappresentazione poco utile:
<__main__.Persona object at 0x...>
Possiamo definire:
class Persona:
def __init__(self, nome):
self.nome = nome
def __str__(self):
return f"Persona: {self.nome}"
Ora:
p = Persona("Marco")
print(p)
produce:
Persona: Marco
__repr____repr__ dovrebbe fornire una rappresentazione utile soprattutto per
debug e sviluppo.
class Persona:
def __init__(self, nome):
self.nome = nome
def __repr__(self):
return f"Persona(nome={self.nome!r})"
Uso:
p = Persona("Marco")
print(repr(p))
In modo semplificato:
__str__ → rappresentazione orientata all'utente;__repr__ → rappresentazione orientata allo sviluppatore.Una buona __repr__ può essere molto utile durante il debug.
Aggiungi __str__ e __repr__ alla classe Prodotto.
Fai in modo che:
print(prodotto)
produca una descrizione leggibile.
== e isQuesti due operatori hanno significati diversi.
isControlla l'identità:
a is b
significa, in sostanza:
aebsono la stessa istanza?
==Controlla l'uguaglianza:
a == b
Il significato concreto dipende dal tipo dell'oggetto e può essere
personalizzato tramite __eq__.
Esempio:
class Studente:
def __init__(self, nome):
self.nome = nome
s1 = Studente("Marco")
s2 = Studente("Marco")
print(s1 is s2) # False
Le istanze sono diverse.
Senza ridefinire __eq__, due istanze di una classe normale non vengono
considerate uguali solo perché hanno gli stessi attributi.
__eq__class Studente:
def __init__(self, nome):
self.nome = nome
def __eq__(self, other):
if not isinstance(other, Studente):
return NotImplemented
return self.nome == other.nome
Ora:
s1 = Studente("Marco")
s2 = Studente("Marco")
print(s1 == s2) # True
Restituiamo NotImplemented quando il confronto con l'altro tipo non è
supportato: permette a Python di gestire correttamente il confronto.
Regola fondamentale:
isè per l'identità;==è per l'uguaglianza.
Implementa __eq__ nella classe Prodotto, considerando uguali due
prodotti con lo stesso codice prodotto.
Possiamo definire anche confronti ordinati.
I principali metodi sono:
__lt__ <
__le__ <=
__eq__ ==
__ne__ !=
__gt__ >
__ge__ >=
Esempio:
class Studente:
def __init__(self, nome, voto):
self.nome = nome
self.voto = voto
def __lt__(self, other):
if not isinstance(other, Studente):
return NotImplemented
return self.voto < other.voto
Ora:
a = Studente("Anna", 7)
b = Studente("Marco", 9)
print(a < b) # True
Per confronti più articolati può essere utile il modulo functools,
oppure definire esplicitamente gli operatori necessari.
Possiamo decidere cosa significa + per una nostra classe.
class Money:
def __init__(self, euro):
self.euro = euro
def __add__(self, other):
if not isinstance(other, Money):
return NotImplemented
return Money(self.euro + other.euro)
def __str__(self):
return f"{self.euro:.2f} €"
Uso:
m1 = Money(10)
m2 = Money(25)
m3 = m1 + m2
print(m3)
Output:
35.00 €
L'espressione:
m1 + m2
richiede a Python di utilizzare il comportamento definito da __add__.
__sub__ -
__mul__ *
__truediv__ /
__floordiv__ //
__mod__ %
__pow__ **
Possiamo anche supportare l'operazione inversa.
class Money:
def __init__(self, euro):
self.euro = euro
def __mul__(self, n):
return Money(self.euro * n)
def __rmul__(self, n):
return self.__mul__(n)
Così funzionano entrambe:
m = Money(10)
print(m * 3)
print(3 * m)
L'operator overloading deve mantenere un significato intuitivo.
Creare:
prodotto + studente
solo perché Python lo permette non è una buona progettazione se l'operazione non ha un significato naturale.
Una classe può comportarsi come una collezione.
Supponiamo di voler rappresentare una classe scolastica:
class Classe:
def __init__(self):
self.studenti = []
def aggiungi(self, studente):
self.studenti.append(studente)
Possiamo aggiungere il supporto a len():
class Classe:
def __init__(self):
self.studenti = []
def aggiungi(self, studente):
self.studenti.append(studente)
def __len__(self):
return len(self.studenti)
Ora:
c = Classe()
c.aggiungi("Anna")
c.aggiungi("Marco")
print(len(c))
__getitem__Possiamo supportare:
c[0]
definendo:
def __getitem__(self, indice):
return self.studenti[indice]
__iter__Per rendere naturale:
for studente in c:
...
possiamo scrivere:
def __iter__(self):
return iter(self.studenti)
__contains__Per supportare:
"Anna" in c
possiamo definire:
def __contains__(self, studente):
return studente in self.studenti
Una versione completa:
class Classe:
def __init__(self):
self.studenti = []
def aggiungi(self, studente):
self.studenti.append(studente)
def __len__(self):
return len(self.studenti)
def __getitem__(self, indice):
return self.studenti[indice]
def __iter__(self):
return iter(self.studenti)
def __contains__(self, studente):
return studente in self.studenti
Uso:
c = Classe()
c.aggiungi("Anna")
c.aggiungi("Marco")
print(len(c))
print(c[0])
print("Marco" in c)
for studente in c:
print(studente)
Crea una classe Playlist che contenga una lista di brani e supporti:
len(playlist);playlist[0];for brano in playlist;"titolo" in playlist.L'ereditarietà permette di creare una nuova classe a partire da una classe esistente.
class Persona:
def __init__(self, nome):
self.nome = nome
def saluta(self):
return f"Ciao, sono {self.nome}"
class Studente(Persona):
pass
Studente eredita da Persona.
s = Studente("Marco")
print(s.nome)
print(s.saluta())
Persona → superclasse / classe base;Studente → sottoclasse / classe derivata.L'ereditarietà ha senso quando esiste una relazione logica del tipo:
Studente è una Persona.
Ha meno senso quando la relazione è semplicemente:
Auto possiede un Motore.
In questo secondo caso è generalmente più naturale la composizione.
Crea:
Veicolo
├── Auto
└── Moto
con un metodo muovi() nella classe base e un comportamento specifico
nelle sottoclassi.
super()Se una sottoclasse definisce un proprio __init__, il costruttore della
classe base non viene eseguito automaticamente.
Esempio:
class Persona:
def __init__(self, nome):
self.nome = nome
class Studente(Persona):
def __init__(self, nome, matricola):
self.matricola = matricola
In questo caso nome non viene inizializzato dalla classe base.
Possiamo usare super():
class Studente(Persona):
def __init__(self, nome, matricola):
super().__init__(nome)
self.matricola = matricola
Ora:
s = Studente("Marco", 12345)
print(s.nome)
print(s.matricola)
super()?Per:
Una sottoclasse può ridefinire un metodo ereditato.
class Persona:
def saluta(self):
return "Ciao, sono una persona"
class Studente(Persona):
def saluta(self):
return "Ciao, sono uno studente"
Ora:
p = Persona()
s = Studente()
print(p.saluta())
print(s.saluta())
Il metodo utilizzato dipende dal tipo reale dell'oggetto.
Possiamo usare super():
class Studente(Persona):
def saluta(self):
messaggio = super().saluta()
return messaggio + " e studio informatica"
Questo permette alla sottoclasse di aggiungere comportamento senza duplicare quello della classe base.
Crea:
Dipendente
├── Docente
└── Tecnico
Ogni classe deve implementare descrizione() in modo diverso.
Usa super() nei costruttori per inizializzare gli attributi comuni.
Un esempio coerente può essere:
Persona
│
└── Dipendente
├── Docente
└── Tecnico
Implementazione:
class Persona:
def __init__(self, nome):
self.nome = nome
def descrizione(self):
return f"Persona: {self.nome}"
class Dipendente(Persona):
def __init__(self, nome, stipendio):
super().__init__(nome)
self.stipendio = stipendio
def descrizione(self):
return f"Dipendente: {self.nome}, stipendio={self.stipendio}"
class Docente(Dipendente):
def __init__(self, nome, stipendio, materia):
super().__init__(nome, stipendio)
self.materia = materia
def descrizione(self):
return (
f"Docente: {self.nome}, "
f"materia={self.materia}, "
f"stipendio={self.stipendio}"
)
class Tecnico(Dipendente):
def __init__(self, nome, stipendio, reparto):
super().__init__(nome, stipendio)
self.reparto = reparto
def descrizione(self):
return (
f"Tecnico: {self.nome}, "
f"reparto={self.reparto}, "
f"stipendio={self.stipendio}"
)
Possiamo lavorare con oggetti di tipo diverso:
persone = [
Persona("Anna"),
Docente("Marco", 1800, "Informatica"),
Tecnico("Luca", 1600, "Laboratorio")
]
for persona in persone:
print(persona.descrizione())
Questo è già un esempio di polimorfismo.
objectIn Python tutte le classi derivano, direttamente o indirettamente, da
object.
Quindi:
class Persona:
pass
è concettualmente equivalente a:
class Persona(object):
pass
Possiamo verificare:
print(Persona.__bases__)
e ottenere:
(<class 'object'>,)
Questo spiega anche perché ogni oggetto Python dispone di numerosi comportamenti ereditati.
Python permette a una classe di ereditare da più classi:
class Volante:
def vola(self):
return "Sto volando"
class Nuotatore:
def nuota(self):
return "Sto nuotando"
class Anatra(Volante, Nuotatore):
pass
Ora:
a = Anatra()
print(a.vola())
print(a.nuota())
Se entrambe le classi definiscono lo stesso metodo:
class A:
def info(self):
return "A"
class B:
def info(self):
return "B"
class C(A, B):
pass
allora:
print(C().info())
restituisce:
A
perché A viene prima di B nell'ordine di risoluzione.
Python utilizza un ordine preciso per cercare attributi e metodi: la MRO.
Possiamo visualizzarlo:
print(C.mro())
oppure:
print(C.__mro__)
Per il caso precedente otteniamo concettualmente:
C → A → B → object
L'MRO diventa particolarmente importante con gerarchie complesse e con l'ereditarietà multipla.
super() e MROIn una gerarchia con ereditarietà multipla, super() non significa
semplicemente "chiama il padre".
Significa, più precisamente:
prosegui nella MRO a partire dalla posizione corrente.
Questo è fondamentale per costruire gerarchie cooperative.
Un caso classico è:
A
/ \
B C
\ /
D
D eredita sia da B sia da C, che a loro volta derivano da A.
Python utilizza una MRO basata sull'algoritmo C3 linearization per stabilire un ordine coerente di ricerca.
Nelle gerarchie cooperative è importante usare super() correttamente,
evitando di chiamare direttamente una specifica classe base quando non è
necessario.
class Base:
def saluta(self):
print("Base")
class A(Base):
def saluta(self):
print("A")
super().saluta()
class B(Base):
def saluta(self):
print("B")
super().saluta()
class C(A, B):
pass
Uso:
C().saluta()
produce:
A
B
Base
perché super() segue la MRO.
L'ereditarietà multipla è potente, ma va usata con criterio. In molti casi, soprattutto in applicazioni aziendali, la composizione è più semplice da comprendere e mantenere.
Una Abstract Base Class (ABC) definisce un contratto per le classi derivate.
Utilizziamo il modulo abc:
from abc import ABC, abstractmethod
Esempio:
class Animale(ABC):
@abstractmethod
def verso(self):
pass
Non possiamo istanziare direttamente Animale:
animale = Animale()
Python genera un TypeError.
class Cane(Animale):
def verso(self):
return "Bau!"
Ora:
cane = Cane()
print(cane.verso())
class Pesce(Animale):
pass
Anche Pesce rimane astratta e non può essere istanziata finché non
implementa verso().
Una ABC può contenere entrambi:
from abc import ABC, abstractmethod
class Figura(ABC):
@abstractmethod
def area(self):
pass
def descrizione(self):
return "Sono una figura geometrica"
Le sottoclassi devono implementare area(), ma possono ereditare
descrizione().
Crea una ABC Pagamento con un metodo astratto:
paga(importo)
Crea:
PagamentoCarta;PagamentoPayPal.Ogni sottoclasse deve implementare il proprio comportamento.
Il polimorfismo significa, in termini pratici:
stessa interfaccia, comportamenti diversi.
Esempio:
class Cane:
def verso(self):
return "Bau!"
class Gatto:
def verso(self):
return "Miao!"
Possiamo scrivere:
animali = [Cane(), Gatto()]
for animale in animali:
print(animale.verso())
Il codice chiamante non deve sapere se l'oggetto è un cane o un gatto.
class Figura(ABC):
@abstractmethod
def area(self):
pass
class Rettangolo(Figura):
def __init__(self, base, altezza):
self.base = base
self.altezza = altezza
def area(self):
return self.base * self.altezza
class Cerchio(Figura):
def __init__(self, raggio):
self.raggio = raggio
def area(self):
return 3.141592653589793 * self.raggio ** 2
Ora:
figure = [
Rettangolo(10, 5),
Cerchio(3)
]
for figura in figure:
print(figura.area())
La funzione che utilizza area() non deve distinguere le classi
concrete.
Questo riduce la necessità di costruire grandi catene di if e elif.
Python porta il polimorfismo ancora più avanti con il principio del duck typing.
La metafora è:
"Se cammina come un'anatra e starnazza come un'anatra, allora è un'anatra."
In pratica non importa necessariamente quale sia la classe dell'oggetto. Importa che supporti l'operazione richiesta.
class Cane:
def verso(self):
return "Bau!"
class Gatto:
def verso(self):
return "Miao!"
class Persona:
def verso(self):
return "Ciao!"
Possiamo scrivere:
def fai_parlare(oggetto):
print(oggetto.verso())
e usare:
fai_parlare(Cane())
fai_parlare(Gatto())
fai_parlare(Persona())
La funzione non controlla la classe.
Cane, Gatto e Persona non devono avere una superclasse comune.
È sufficiente che offrano il comportamento necessario.
Se passiamo:
class Pesce:
pass
e poi:
fai_parlare(Pesce())
otteniamo un AttributeError, perché l'oggetto non possiede verso.
Questa è una caratteristica importante del duck typing: molti vincoli vengono verificati quando il codice viene eseguito.
Uno degli aspetti più importanti della progettazione OOP non è sapere come usare l'ereditarietà, ma sapere quando non usarla.
class Auto(Veicolo):
pass
Esprime:
Auto IS-A Veicolo.
class Motore:
def avvia(self):
print("Motore avviato")
class Auto:
def __init__(self):
self.motore = Motore()
Esprime:
Auto HAS-A Motore.
La composizione:
Una regola utile è:
Preferire la composizione all'ereditarietà quando non esiste una relazione IS-A realmente significativa.
Non significa che l'ereditarietà sia "sbagliata": significa che deve essere una scelta progettuale consapevole.
Invece di:
AutoConMotoreBenzina
AutoConMotoreDiesel
AutoConMotoreElettrico
possiamo avere:
Auto → Motore
├── MotoreBenzina
├── MotoreDiesel
└── MotoreElettrico
e scegliere il motore al momento della costruzione.
dataclassMolte classi servono principalmente a rappresentare dati.
Esempio tradizionale:
class Punto:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Punto(x={self.x}, y={self.y})"
def __eq__(self, other):
if not isinstance(other, Punto):
return NotImplemented
return self.x == other.x and self.y == other.y
Python offre dataclass per ridurre questo boilerplate.
from dataclasses import dataclass
@dataclass
class Punto:
x: int
y: int
Ora:
p = Punto(3, 4)
print(p)
produce una rappresentazione utile, e:
print(Punto(3, 4) == Punto(3, 4))
restituisce True.
dataclass?È particolarmente utile per:
dataclassPossiamo definire:
from dataclasses import dataclass
@dataclass
class Studente:
nome: str
eta: int = 18
Uso:
s = Studente("Anna")
Questo è sbagliato:
@dataclass
class Classe:
studenti: list = []
Per un campo mutabile bisogna utilizzare default_factory:
from dataclasses import dataclass, field
@dataclass
class Classe:
studenti: list = field(default_factory=list)
Così ogni istanza riceve una lista distinta.
Lo stesso principio vale per:
field(default_factory=dict)
field(default_factory=set)
Crea una dataclass Prodotto con:
nome: str;prezzo: float;categoria: str = "generale".Crea almeno tre prodotti e stampali.
field() nelle dataclassfield() permette di controllare il comportamento dei campi.
init=FalseUn campo può essere escluso dal costruttore generato:
from dataclasses import dataclass, field
@dataclass
class Persona:
nome: str
cognome: str
codice: str = field(init=False)
def __post_init__(self):
self.codice = self.nome[0] + self.cognome[0]
Uso:
p = Persona("Mario", "Rossi")
print(p.codice)
repr=FalsePossiamo escludere un campo dalla rappresentazione:
@dataclass
class Utente:
username: str
password: str = field(repr=False)
In questo modo la password non compare nel repr generato.
Questo non è un meccanismo di sicurezza: impedisce solo che il campo venga incluso nella rappresentazione automatica.
compare=FalsePossiamo escludere un campo dai confronti:
@dataclass
class Prodotto:
nome: str
prezzo: float
id_interno: int = field(compare=False)
Crea una dataclass Utente con:
username;email;password esclusa dal repr.Verifica la rappresentazione prodotta da Python.
__post_init__()Una dataclass può definire __post_init__() per eseguire logica
subito dopo l'__init__ generato automaticamente.
@dataclass
class Studente:
nome: str
eta: int
def __post_init__(self):
if self.eta < 0:
raise ValueError("Età non valida")
Il controllo viene eseguito automaticamente:
s = Studente("Anna", 18)
mentre:
s = Studente("Anna", -2)
genera un ValueError.
dataclassUna dataclass non è una classe "limitata": può avere metodi e property
normalmente.
from dataclasses import dataclass
@dataclass
class Rettangolo:
base: float
altezza: float
@property
def area(self):
return self.base * self.altezza
Uso:
r = Rettangolo(5, 2)
print(r.area)
Possiamo anche definire metodi:
@dataclass
class Conto:
intestatario: str
saldo: float = 0.0
def deposita(self, importo: float):
self.saldo += importo
def preleva(self, importo: float):
if importo > self.saldo:
raise ValueError("Saldo insufficiente")
self.saldo -= importo
Quindi dataclass significa semplicemente che Python genera
automaticamente molto codice ripetitivo: non significa rinunciare alla
normale programmazione a oggetti.
dataclassCon:
@dataclass(order=True)
class Studente:
matricola: int
nome: str
Python genera anche i metodi necessari per gli ordinamenti.
a = Studente(2, "Luca")
b = Studente(1, "Anna")
print(b < a)
L'ordinamento tiene conto dei campi nell'ordine in cui sono dichiarati.
Per progettazioni più specifiche può essere preferibile usare
sorted(..., key=...), così da rendere esplicito il criterio.
frozen=TruePossiamo creare una dataclass immutabile:
@dataclass(frozen=True)
class Coordinate:
x: float
y: float
Dopo la creazione:
c = Coordinate(10, 20)
non possiamo normalmente fare:
c.x = 50
perché l'istanza è frozen.
Le dataclass frozen sono utili quando vogliamo rappresentare valori che non devono essere modificati dopo la creazione.
dataclassLe dataclass possono partecipare a gerarchie di ereditarietà.
from dataclasses import dataclass
@dataclass
class Persona:
nome: str
@dataclass
class Studente(Persona):
matricola: int
Uso:
s = Studente("Marco", 12345)
print(s)
Python genera automaticamente il costruttore considerando i campi ereditati e quelli della sottoclasse, nel rispetto delle regole delle dataclass.
Una buona classe non dovrebbe essere solo un contenitore di variabili.
Confrontiamo:
conto.saldo = conto.saldo + 100
con:
conto.deposita(100)
La seconda soluzione esprime meglio l'intenzione.
Il metodo:
def deposita(self, importo):
...
può inoltre controllare le regole dell'oggetto.
Questo porta a un principio importante:
L'oggetto dovrebbe proteggere e gestire il proprio stato.
Non significa nascondere tutto, ma concentrare nella classe le regole che riguardano i suoi dati.
Una classe dovrebbe avere una responsabilità comprensibile.
Una classe come:
class SistemaGestioneNegozio:
...
che contemporaneamente:
rischia di diventare difficile da testare e mantenere.
Meglio separare le responsabilità:
Prodotto
Ordine
Cliente
Catalogo
ServizioOrdini
Repository
Report
Le classi possono poi collaborare.
Questo concetto prepara il terreno alla progettazione di applicazioni più grandi e ai pattern architetturali che verranno affrontati più avanti.
Costruiamo un modello semplice.
from dataclasses import dataclass
@dataclass
class Prodotto:
codice: str
nome: str
prezzo: float
class Carrello:
def __init__(self):
self._prodotti = []
def aggiungi(self, prodotto):
self._prodotti.append(prodotto)
@property
def totale(self):
return sum(prodotto.prezzo for prodotto in self._prodotti)
def __len__(self):
return len(self._prodotti)
def __iter__(self):
return iter(self._prodotti)
def __str__(self):
return f"Carrello con {len(self)} prodotti: {self.totale:.2f} €"
Uso:
mouse = Prodotto("M01", "Mouse", 25.90)
tastiera = Prodotto("T01", "Tastiera", 49.90)
carrello = Carrello()
carrello.aggiungi(mouse)
carrello.aggiungi(tastiera)
print(carrello)
print(carrello.totale)
for prodotto in carrello:
print(prodotto.nome)
Qui abbiamo combinato:
dataclass;__len__;__iter__;__str__;Possiamo definire una classe astratta:
from abc import ABC, abstractmethod
class MetodoPagamento(ABC):
@abstractmethod
def paga(self, importo):
pass
Implementazioni:
class Carta(MetodoPagamento):
def paga(self, importo):
return f"Pagamento con carta: {importo:.2f} €"
class PayPal(MetodoPagamento):
def paga(self, importo):
return f"Pagamento con PayPal: {importo:.2f} €"
Una funzione generica:
def completa_pagamento(metodo, importo):
print(metodo.paga(importo))
Uso:
completa_pagamento(Carta(), 100)
completa_pagamento(PayPal(), 100)
La funzione non deve conoscere la classe concreta.
Questo è un esempio di:
Supponiamo di voler rappresentare un computer.
Un computer ha un processore, una memoria e un disco.
class Processore:
def avvia(self):
print("Processore avviato")
class Memoria:
def carica(self):
print("Memoria inizializzata")
class Computer:
def __init__(self):
self.processore = Processore()
self.memoria = Memoria()
def avvia(self):
self.processore.avvia()
self.memoria.carica()
print("Computer avviato")
Uso:
computer = Computer()
computer.avvia()
Non abbiamo creato:
class ComputerConProcessore:
...
La relazione è di composizione.
selfSbagliato:
class Persona:
def saluta():
print("Ciao")
Corretto:
class Persona:
def saluta(self):
print("Ciao")
def __init__(self, nome):
nome = nome
Questa istruzione non salva il nome nell'oggetto.
Corretto:
def __init__(self, nome):
self.nome = nome
super()Se una sottoclasse ridefinisce __init__ ma deve inizializzare anche la
parte ereditata:
super().__init__(...)
Da evitare:
class Classe:
studenti = []
se la lista dovrebbe essere indipendente per ogni istanza.
Meglio:
class Classe:
def __init__(self):
self.studenti = []
oppure, con una dataclass:
@dataclass
class Classe:
studenti: list = field(default_factory=list)
La domanda non dovrebbe essere:
"Posso evitare di riscrivere questo codice?"
ma:
"Esiste davvero una relazione concettuale tra queste classi?"
Se la risposta è no, valuta la composizione.
Una property è utile quando c'è logica da applicare o quando vogliamo un'interfaccia di accesso controllata.
Se non c'è alcun comportamento, un semplice attributo può essere sufficiente.
self?__init__?cls?@staticmethod?@property?is e ==?super()?@dataclass?default_factory con liste e dizionari nelle
dataclass?is confronta l'identità; == l'uguaglianza.Crea una classe Studente con:
Aggiungi:
__str__;promosso() che restituisca True se la media è almeno 6;voto_medio con controllo tra 0 e 10.Crea:
class ContoBancario:
...
con:
deposita(importo);preleva(importo);saldo;__str__.Il prelievo non deve permettere di superare il saldo disponibile.
Crea:
Persona
└── Dipendente
├── Docente
└── Tecnico
Ogni classe deve:
super();descrizione().Poi crea una lista con oggetti di tipi diversi e stampa le descrizioni.
Definisci una ABC:
class Figura(ABC):
@abstractmethod
def area(self):
pass
Implementa:
Rettangolo;Quadrato;Cerchio.Inserisci le figure in una lista e stampa le aree utilizzando polimorfismo.
Crea una classe Playlist che:
len();in;__str__.Realizza un piccolo sistema OOP per gestire una biblioteca.
LibroIl libro deve avere:
Implementa:
__str__;__repr__;__eq__, considerando uguali due libri con lo stesso ISBN.BibliotecaLa biblioteca deve contenere una collezione di libri.
Deve permettere:
biblioteca.aggiungi_libro(libro)
biblioteca.rimuovi_libro(isbn)
biblioteca.cerca_titolo("Python")
biblioteca.presta(isbn)
biblioteca.restituisci(isbn)
Inoltre deve supportare:
len(biblioteca)
e:
for libro in biblioteca:
...
Non deve essere possibile:
Gli errori devono essere segnalati tramite eccezioni appropriate.
Nel prossimo capitolo sulle eccezioni approfondiremo proprio come progettare e gestire questi errori in modo sistematico.
cerca_titolo() deve restituire tutti i libri il cui titolo contiene la
stringa cercata, senza distinguere maiuscole e minuscole.
Esempio:
risultati = biblioteca.cerca_titolo("python")
Aggiungi una classe astratta:
class ElementoBiblioteca(ABC):
@abstractmethod
def descrizione(self):
pass
Fai derivare Libro da ElementoBiblioteca.
Successivamente aggiungi una seconda tipologia, per esempio:
Rivista
anch'essa derivata da ElementoBiblioteca.
La biblioteca deve poter gestire entrambi i tipi.
Una possibile soluzione è la seguente.
from abc import ABC, abstractmethod
class ElementoBiblioteca(ABC):
@abstractmethod
def descrizione(self):
pass
Libroclass Libro(ElementoBiblioteca):
def __init__(self, isbn, titolo, autore, anno):
self.isbn = isbn
self.titolo = titolo
self.autore = autore
self.anno = anno
self.disponibile = True
def descrizione(self):
stato = "disponibile" if self.disponibile else "in prestito"
return (
f"{self.titolo} - {self.autore} "
f"({self.anno}) [{stato}]"
)
def __str__(self):
return self.descrizione()
def __repr__(self):
return (
f"Libro(isbn={self.isbn!r}, "
f"titolo={self.titolo!r}, "
f"autore={self.autore!r}, "
f"anno={self.anno})"
)
def __eq__(self, other):
if not isinstance(other, Libro):
return NotImplemented
return self.isbn == other.isbn
Rivistaclass Rivista(ElementoBiblioteca):
def __init__(self, codice, titolo, numero):
self.codice = codice
self.titolo = titolo
self.numero = numero
def descrizione(self):
return f"Rivista: {self.titolo}, numero {self.numero}"
def __str__(self):
return self.descrizione()
class Biblioteca:
def __init__(self):
self._elementi = []
def aggiungi(self, elemento):
self._elementi.append(elemento)
def __len__(self):
return len(self._elementi)
def __iter__(self):
return iter(self._elementi)
def cerca_titolo(self, testo):
testo = testo.lower()
return [
elemento
for elemento in self._elementi
if testo in elemento.titolo.lower()
]
Possiamo quindi fare:
biblioteca = Biblioteca()
biblioteca.aggiungi(
Libro(
"978-0000000001",
"Python per tutti",
"Mario Rossi",
2026
)
)
biblioteca.aggiungi(
Rivista(
"R001",
"Python Magazine",
12
)
)
for elemento in biblioteca:
print(elemento.descrizione())
Qui il ciclo:
for elemento in biblioteca:
print(elemento.descrizione())
non ha bisogno di sapere se elemento è un Libro o una Rivista.
Abbiamo quindi ottenuto polimorfismo.
La prima soluzione funziona, ma può essere ulteriormente migliorata.
Possiamo:
dataclass dove appropriato;Ad esempio, invece di:
self._elementi = []
potremmo utilizzare:
self._elementi = {}
indicizzando gli elementi tramite un identificativo.
Questo rende alcune ricerche più efficienti.
Nel capitolo successivo sulle eccezioni vedremo anche come trasformare controlli come:
if libro non disponibile:
...
in una gestione degli errori più pulita e strutturata.
Quando un programma cresce, è importante evitare classi gigantesche.
Per esempio, una biblioteca reale potrebbe avere:
Libro
Utente
Prestito
Catalogo
Biblioteca
RepositoryLibri
ServizioPrestiti
Ogni componente ha una responsabilità.
La classe Libro non dovrebbe sapere:
Dovrebbe conoscere soprattutto ciò che riguarda il concetto di libro.
Questo principio diventerà fondamentale quando inizieremo a costruire applicazioni più grandi.
La vera competenza nella programmazione a oggetti non consiste nel creare molte classi.
Consiste nel decidere:
Un buon modello OOP tende a produrre codice:
In questo capitolo abbiamo introdotto la programmazione orientata agli oggetti in Python.
Una classe definisce un tipo:
class Persona:
...
Un'istanza è un oggetto concreto:
p = Persona()
Gli attributi rappresentano lo stato:
self.nome
I metodi rappresentano comportamenti:
def saluta(self):
...
__init__() inizializza l'istanza.
self rappresenta l'istanza corrente.
Python utilizza convenzioni e strumenti come:
_nome
__nome
@property
per controllare l'accesso ai dati.
Abbiamo visto:
def metodo(self):
...
@classmethod
def metodo(cls):
...
@staticmethod
def metodo():
...
Abbiamo imparato a personalizzare il comportamento degli oggetti tramite:
__str__
__repr__
__eq__
__lt__
__add__
__len__
__getitem__
__iter__
Una sottoclasse può estendere una superclasse:
class Studente(Persona):
...
super() permette di riutilizzare la logica ereditata.
Oggetti diversi possono offrire la stessa interfaccia:
for figura in figure:
figura.area()
In Python spesso conta il comportamento dell'oggetto più che la sua classe:
oggetto.verso()
Le ABC permettono di definire contratti:
from abc import ABC, abstractmethod
Un oggetto può contenere altri oggetti:
self.motore = Motore()
e spesso questa soluzione è preferibile a gerarchie di ereditarietà troppo profonde.
Quando una classe rappresenta soprattutto dati:
@dataclass
class Punto:
x: int
y: int
può ridurre notevolmente il codice ripetitivo.
PROGRAMMAZIONE OOP
│
┌────────────────┼────────────────┐
│ │ │
CLASSE ISTANZA METODI
│ │ │
┌────┴────┐ stato proprio ┌───┼────┐
│ │ │ │ │
attributi metodi self cls static
│
┌───┴───────────────┐
│ │
istanza classe
│ │
self.nome Classe.nome
│
├── property
├── incapsulamento
└── dunder methods
│
▼
__str__, __eq__,
__len__, __add__...
│
▼
EREDITARIETÀ
│
┌───┴────┐
│ │
override super()
│
▼
POLIMORFISMO
│
┌───┴─────────┐
│ │
ABC Duck Typing
│
▼
COMPOSIZIONE
│
▼
DATACLASS
Le classi ci permettono di modellare oggetti e regole del dominio. Ma gli oggetti possono anche trovarsi in situazioni non valide.
Per esempio:
conto.preleva(1000)
quando il saldo è solo 100.
Oppure:
biblioteca.presta("ISBN inesistente")
oppure:
int("abc")
In tutti questi casi il programma deve gestire un errore.
Python mette a disposizione un sistema completo per farlo:
try:
...
except:
...
else:
...
finally:
...
Nel prossimo capitolo affronteremo quindi le eccezioni, imparando a:
try e except;else e finally;raise;Obiettivo: non limitarsi a "evitare che il programma vada in errore", ma imparare a progettare programmi che sappiano gestire correttamente le situazioni eccezionali.