18 Programmazione orientata agli oggetti
Python è un linguaggio di programmazione orientato agli oggetti. A differenza di altri linguaggi orientati agli oggetti, tuttavia, Python non costringe ad utilizzare esclusivamente il paradigma orientato agli oggetti: supporta anche la programmazione procedurale, con moduli e funzioni, consentendo di scegliere il miglior paradigma per ogni parte del programma.
Il paradigma orientato agli oggetti aiuta a raggruppare stato (dati) e comportamento (codice) in comode unità di funzionalità. Inoltre, offre alcuni meccanismi specializzati utili, come l’ereditarietà e i metodi speciali. L’approccio procedurale più semplice, basato su moduli e funzioni, può essere più adatto quando non è necessario sfruttare i vantaggi della programmazione orientata agli oggetti. Con Python, possiamo combinare e mescolare paradigmi.
18.1 Classi e istanze
Una classe è un tipo definibile dall’utente, che possiamo istanziare per costruire istanze, ovvero oggetti di quel tipo. Python supporta questo attraverso le sue classi e oggetti istanza.
18.1.1 L’istruzione class
L’istruzione class
è il modo più usuale per definire un oggetto classe. class
è un’istruzione composta a singola clausola con la seguente sintassi:
- 1
-
Testata della definizione di classe:
NomeClasse
è un identificatore, cioè una variabile che la definizioneclass
, quando completata, associa (o riassocia) all’oggetto classe appena creato. Le convenzioni di denominazione Python consigliano di utilizzare la convenzione del cammello per i nomi delle classi, comeElemento
,UtentePrivilegiato
,StrutturaMultiUso
, ecc.classi-base
è una serie di espressioni delimitate da virgole i cui valori sono oggetti classe. Vari linguaggi di programmazione utilizzano nomi diversi per questi oggetti classe: possiamo chiamarli basi, superclasse o genitori della classe. Possiamo dire che la classe creata eredita da, deriva da, estende o sottoclasse le sue classi base; in questo capitolo, generalmente usiamo il termine estendere. Questa classe è una sottoclasse diretta o discendente delle sue classi base.**kw
può includere un argomento denominatometaclass
per stabilire la metaclasse della classe. - 2
- Corpo della definizione della classe composto di istruzioni di definizione di membri della classe. Queste possono includere la definizione di attributi, metodi e altre classi.
La definizione di una classe crea un oggetto classe, proprio come la definizione di una funzione crea un oggetto funzione. L’oggetto classe ha tipo type
, da non confondere con la funzione predefinita type()
. Quando si chiama una classe come se fosse una funzione, avviene l’istanziazione, che crea un nuovo oggetto istanza della classe con un tipo corrispondente alla classe stessa.
Esempio di definizione di una classe:
1class MiaClasse:
2 def __init__(self, valore):
3 self.valore = valore
4istanza = MiaClasse(10)
5print(type(MiaClasse))
6print(type(istanza))
- 1
-
Definisce una classe
MiaClasse
. - 2
-
Definisce il metodo speciale
__init__
per l’inizializzazione. - 3
-
Associa un valore all’attributo
valore
dell’istanza. - 4
-
Crea un’istanza della classe
MiaClasse
con valore 10. - 5
-
Stampa il tipo di
MiaClasse
, che sarà<class 'type'>
. - 6
-
Stampa il tipo di
istanza
, che sarà<class '__main__.MiaClasse'>
.
18.1.2 Il parametro self
In Python, i metodi di istanza delle classi possono includere un parametro speciale self
come primo parametro. Quando presente, self
permette di passare al metodo l’istanza della classe, consentendo l’accesso agli attributi e ai metodi dell’istanza. La differenza fondamentale tra membri della classe e membri delle istanze è che i membri della classe sono condivisi tra tutte le istanze, mentre i membri delle istanze sono specifici per ciascuna istanza.
Esempio di membri della classe e membri delle istanze:
1class MiaClasse:
2 attributo_classe = "Valore di classe"
3 def __init__(self, valore):
4 self.attributo_istanza = valore
5 def mostra_attributi(self):
6 return f"Classe: {MiaClasse.attributo_classe}, Istanza: {self.attributo_istanza}"
# Creazione di due istanze della classe
7istanza1 = MiaClasse("Valore 1")
8istanza2 = MiaClasse("Valore 2")
# Accesso agli attributi della classe e delle istanze
9print(MiaClasse.attributo_classe)
10print(istanza1.attributo_classe)
11print(istanza1.attributo_istanza)
12print(istanza2.attributo_istanza)
# Chiamata ai metodi delle istanze
13print(istanza1.mostra_attributi())
14print(istanza2.mostra_attributi())
- 1
-
Definisce una classe
MiaClasse
. - 2
-
Definisce un attributo di classe
attributo_classe
con valoreValore di classe
. - 3
-
Definisce il metodo speciale
__init__
per l’inizializzazione. - 4
-
Associa un valore all’attributo
attributo_istanza
dell’istanza. - 5
-
Definisce un metodo
mostra_attributi
per mostrare gli attributi. - 6
-
Il metodo
mostra_attributi
restituisce una stringa con gli attributi di classe e di istanza. - 7
-
Crea un’istanza della classe
MiaClasse
con valoreValore 1
. - 8
-
Crea un’istanza della classe
MiaClasse
con valoreValore 2
. - 9
-
Stampa il valore dell’attributo di classe
attributo_classe
. - 10
-
Stampa il valore dell’attributo di classe
attributo_classe
dall’istanzaistanza1
. - 11
-
Stampa il valore dell’attributo di istanza
attributo_istanza
peristanza1
. - 12
-
Stampa il valore dell’attributo di istanza
attributo_istanza
peristanza2
. - 13
-
Chiama il metodo
mostra_attributi
sull’istanzaistanza1
. - 14
-
Chiama il metodo
mostra_attributi
sull’istanzaistanza2
.
18.2 Membri
Un riferimento a un membro è un’espressione della forma x.nome
, dove x
è qualsiasi espressione e nome
è un identificatore chiamato nome del membro. Molti oggetti Python hanno membri, ma un riferimento a un membro ha una semantica speciale e ricca quando x
si riferisce a una classe o a un’istanza. I metodi sono membri, quindi tutto ciò che diciamo sui membri in generale si applica anche a quelli chiamabili (cioè, metodi).
Quando utilizziamo la sintassi x.nome
per riferirci a un membro di un’istanza x
di una classe C
, la ricerca dell’attributo procede in tre passi:
Membro nell’istanza: Se
'nome'
è una chiave inx.__dict__
,x.nome
restituisce il valore associato a quella chiave. Questo è il caso più semplice e veloce.Membro nella classe o nelle sue basi: Se
'nome'
non è una chiave inx.__dict__
, la ricerca del membro procede nella classe dix
(x.__class__
) e nelle sue basi, seguendo l’ordine di risoluzione dei metodi (method resolution order, MRO).Metodo
__getattr__
: Se l’attributo non è trovato né nell’istanza né nella classe e nelle sue basi, viene chiamato il metodo speciale__getattr__
, se definito. Questo metodo può fornire un valore di ritorno per l’attributo o sollevare un’eccezioneAttributeError
.
Esempio di riferimento ai membri:
1class Base:
2 a = 23
3class Derivata(Base):
4 b = 45
5d = Derivata()
6d.c = 67
7print(d.a)
8print(d.b)
9print(d.c)
- 1
-
Definizione della classe
Base
. - 2
-
L’attributo
a
della classeBase
è associato al valore23
. - 3
-
Definizione della classe
Derivata
che eredita daBase
. - 4
-
L’attributo
b
della classeDerivata
è associato al valore45
. - 5
-
Creazione di un’istanza della classe
Derivata
. - 6
-
Associazione dell’attributo
c
dell’istanzad
al valore67
. - 7
-
Stampa del valore dell’attributo
a
dell’istanzad
, trovato nella classe base. Output:23
. - 8
-
Stampa del valore dell’attributo
b
dell’istanzad
, trovato nella classe derivata. Output:45
. - 9
-
Stampa del valore dell’attributo
c
dell’istanzad
, trovato nell’istanza stessa. Output:67
.
In Python, un attributo o un metodo può essere membro di una classe o di un’istanza. La differenza fondamentale tra membri di classe e membri di istanza è che i membri di classe sono condivisi tra tutte le istanze, mentre i membri di istanza sono specifici per ciascuna istanza.
Esempio:
1class Contatore:
2 contatore_comune = 0
3 def __init__(self, valore_iniziale=0):
4 self.valore = valore_iniziale
5 Contatore.contatore_comune += 1
6 def incrementa(self):
7 self.valore += 1
# Membri di classe
8print(Contatore.contatore_comune)
# Creazione di due istanze
9c1 = Contatore()
10c2 = Contatore(10)
# Membri di istanza
11c1.incrementa()
12c2.incrementa()
13print(c1.valore)
14print(c2.valore)
# Membro di classe aggiornato
15print(Contatore.contatore_comune)
- 1
-
Definisce una classe
Contatore
. - 2
-
Definisce un attributo di classe
contatore_comune
inizializzato a 0. - 3
-
Definisce il metodo speciale
__init__
per l’inizializzazione. - 4
-
Associa un valore iniziale all’attributo
valore
dell’istanza. - 5
-
Incrementa l’attributo di classe
contatore_comune
ogni volta che viene creata una nuova istanza. - 6
-
Definisce un metodo
incrementa
per incrementare l’attributovalore
dell’istanza. - 7
-
Incrementa l’attributo
valore
dell’istanza. - 8
-
Stampa il valore dell’attributo di classe
contatore_comune
. - 9
-
Creazione di un’istanza della classe
Contatore
con valore iniziale di default. - 10
-
Creazione di un’istanza della classe
Contatore
con valore iniziale 10. - 11
-
Incrementa il valore dell’attributo
valore
dell’istanzac1
. - 12
-
Incrementa il valore dell’attributo
valore
dell’istanzac2
. - 13
-
Stampa il valore dell’attributo
valore
dell’istanzac1
. Output:1
. - 14
-
Stampa il valore dell’attributo
valore
dell’istanzac2
. Output:11
. - 15
-
Stampa il valore dell’attributo di classe
contatore_comune
. Output:2
.
Questo esempio illustra come i membri di classe siano condivisi tra tutte le istanze, mentre i membri di istanza siano specifici per ciascuna istanza.
18.3 Oggetti
Una classe è un oggetto Python con le seguenti caratteristiche:
La definizione di una classe crea un oggetto classe, proprio come la definizione di una funzione crea un oggetto funzione. L’oggetto classe ha tipo
type
, da non confondere con la funzione predefinitatype()
. Quando si chiama una classe come se fosse una funzione, avviene l’istanziazione, che crea un nuovo oggetto istanza della classe con un tipo corrispondente alla classe stessa.Esempio:
1class MiaClasse: 2 def __init__(self, valore): 3 self.valore = valore 4istanza = MiaClasse(10) 5print(type(MiaClasse)) 6print(type(istanza))
- 1
-
Definisce una classe
MiaClasse
. - 2
-
Definisce il metodo speciale
__init__
per l’inizializzazione. - 3
-
Associa un valore all’attributo
valore
dell’istanza. - 4
-
Crea un’istanza della classe
MiaClasse
con valore 10. - 5
-
Stampa il tipo di
MiaClasse
, che sarà<class 'type'>
. - 6
-
Stampa il tipo di
istanza
, che sarà<class '__main__.MiaClasse'>
.
Una classe ha membri con nomi arbitrari che possiamo associare e referenziare:
- 1
-
Definisce una classe
MiaClasse
. - 2
-
Associa un attributo di classe
primo_attributo
con valoreesempio
. - 3
-
Stampa il valore dell’attributo di classe
primo_attributo
.
I membri della classe possono essere metodi (funzioni) o dati ordinari (variabili):
1class MiaClasse: 2 primo_attributo = "esempio" 3 def primo_metodo(self): 4 return "Ciao, mondo!" 5print(MiaClasse.primo_attributo) 6mia_istanza = MiaClasse() 7print(mia_istanza.primo_metodo()) 8print(MiaClasse.primo_metodo(mia_istanza))
- 1
-
Definisce una classe
MiaClasse
. - 2
-
Definisce un attributo di classe
primo_attributo
con valoreesempio
. - 3
-
Definisce un metodo
primo_metodo
con un parametroself
. - 4
-
Il metodo
primo_metodo
restituisce una stringaCiao, mondo!
. - 5
-
Stampa il valore dell’attributo di classe
primo_attributo
. - 6
-
Crea un’istanza della classe
MiaClasse
e la associa amia_istanza
. - 7
-
Chiama il metodo
primo_metodo
sull’istanzamia_istanza
. Python passa automaticamentemia_istanza
come argomento perself
. - 8
-
Chiama il metodo
primo_metodo
direttamente sulla classeMiaClasse
, passando esplicitamentemia_istanza
come argomento perself
.
Gli attributi della classe associati a funzioni sono noti anche come metodi della classe:
1class MiaClasse: 2 def primo_metodo(self): 3 return "Questo è un metodo" 4istanza = MiaClasse() 5print(istanza.primo_metodo())
- 1
-
Definisce una classe
MiaClasse
. - 2
-
Definisce un metodo
primo_metodo
. - 3
-
Il metodo
primo_metodo
restituisce una stringaQuesto è un metodo
. - 4
-
Crea un’istanza della classe
MiaClasse
. - 5
-
Chiama il metodo
primo_metodo
sull’istanza e stampa il risultato.
Un metodo può avere uno dei tanti nomi definiti da Python con due trattini bassi all’inizio e alla fine (noti come nomi dunder, abbreviazione di “double-underscore names” - ad esempio, il nome
__init__
è pronunciato “dunder init”). Python chiama implicitamente questi metodi speciali, quando una classe li fornisce, quando si verificano vari tipi di operazioni su quella classe o sulle sue istanze.1class MiaClasse: 2 def __init__(self, valore): 3 self.valore = valore 4 def __str__(self): 5 return f"MiaClasse con valore {self.valore}" 6istanza = MiaClasse(10) 7print(istanza)
- 1
-
Definisce una classe
MiaClasse
. - 2
-
Definisce il metodo speciale
__init__
per l’inizializzazione. - 3
-
Associa un valore all’attributo
valore
dell’istanza. - 4
-
Definisce il metodo speciale
__str__
per la rappresentazione stringa. - 5
-
__str__
restituisce una stringa rappresentativa dell’istanza. - 6
-
Crea un’istanza della classe
MiaClasse
con valore10
. - 7
-
Stampa la rappresentazione stringa dell’istanza, chiamando implicitamente
__str__
.
Una classe può ereditare da una o più classi, il che significa che delega ad altri oggetti classe la ricerca di alcuni attributi (inclusi metodi regolari e dunder) che non sono nella classe stessa.
1class ClasseBase: 2 def metodo_base(self): 3 return "Metodo base" 4class ClasseDerivata(ClasseBase): 5 def metodo_derivato(self): 6 return "Metodo derivato" 7istanza = ClasseDerivata() 8print(istanza.metodo_base()) 9print(istanza.metodo_derivato())
- 1
-
Definisce una classe
ClasseBase
. - 2
-
Definisce un metodo
metodo_base
. - 3
-
metodo_base
restituisce una stringa “Metodo base”. - 4
-
Definisce una classe
ClasseDerivata
che eredita daClasseBase
. - 5
-
Definisce un metodo
metodo_derivato
. - 6
-
metodo_derivato
restituisce una stringa “Metodo derivato”. - 7
-
Crea un’istanza della classe
ClasseDerivata
. - 8
-
Chiama il metodo
metodo_base
ereditato dall’istanza e stampa il risultato. - 9
-
Chiama il metodo
metodo_derivato
dell’istanza e stampa il risultato.
Un’istanza di una classe è un oggetto Python con attributi con nomi arbitrari che possiamo associare e referenziare. Ogni oggetto istanza delega la ricerca degli attributi alla sua classe per qualsiasi attributo non trovato nell’istanza stessa. La classe, a sua volta, può delegare la ricerca alle classi da cui eredita, se presenti.
1class MiaClasse:
2 def __init__(self, valore):
3 self.valore = valore
4istanza = MiaClasse(10)
5print(istanza.valore)
- 1
-
Definisce una classe
MiaClasse
. - 2
-
Definisce il metodo speciale
__init__
per l’inizializzazione. - 3
-
Associa un valore all’attributo
valore
dell’istanza. - 4
-
Crea un’istanza della classe
MiaClasse
con valore 10. - 5
-
Stampa il valore dell’attributo
valore
dell’istanza.
In Python, le classi sono oggetti (valori), gestiti proprio come altri oggetti. Possiamo passare una classe come argomento in una chiamata a una funzione e una funzione può restituire una classe come risultato di una chiamata. Possiamo associare una classe a una variabile, a un elemento in un contenitore o a un attributo di un oggetto. Le classi possono anche essere chiavi in un dizionario. Poiché le classi sono oggetti perfettamente ordinari in Python, spesso diciamo che le classi sono oggetti di prima classe.
1def crea_classe():
2 class NuovaClasse:
3 pass
4 return NuovaClasse
5MiaClasse = crea_classe()
6istanza = MiaClasse()
7print(type(istanza))
- 1
-
Definisce una funzione
crea_classe
. - 2
-
Definisce una classe
NuovaClasse
all’interno della funzione. - 3
-
pass
indica che il corpo della classe è vuoto. - 4
-
Restituisce la classe
NuovaClasse
. - 5
-
Chiama la funzione
crea_classe
e associa la classe risultante aMiaClasse
. - 6
-
Crea un’istanza della classe
MiaClasse
. - 7
-
Stampa il tipo dell’istanza, che sarà
<class '__main__.crea_classe.<locals>.NuovaClasse'>
.
18.4 Il corpo della classe
Il corpo di una classe è dove normalmente specifichiamo i membri della classe; questi membri possono essere funzioni o oggetti dati ordinari di qualsiasi tipo. Un membro di una classe può essere un’altra classe, quindi, ad esempio, possiamo avere una dichiarazione di classe annidata all’interno di un’altra definizione di classe.
18.4.1 Attributi degli oggetti classe
Di solito specifichiamo un membro di un oggetto classe associando un valore a un identificatore all’interno del corpo della classe. Per esempio:
- 1
-
Definizione della classe
C1
. - 2
-
L’attributo
x
della classeC1
è associato al valore23
. - 3
-
Stampa del valore dell’attributo x della classe
C1
.
Possiamo anche associare o disassociare membri della classe al di fuori del corpo della classe. Per esempio:
- 1
-
Definizione della classe
C2
. - 2
- Corpo della classe vuoto.
- 3
-
Associazione dell’attributo x della classe
C2
al valore23
. - 4
-
Stampa del valore dell’attributo
x
della classeC2
.
Il nostro programma è solitamente più leggibile se associamo attributi della classe solo con istruzioni all’interno del corpo della classe. Tuttavia, riassociarli altrove può essere necessario se vogliamo mantenere informazioni di stato a livello di classe, piuttosto che a livello di istanza; Python ci permette di farlo, se lo desideriamo. Non c’è differenza tra un attributo di classe associato nel corpo della classe e uno associato o riassociato al di fuori del corpo associando un attributo. Come discuteremo a breve, tutte le istanze della classe condividono tutti gli attributi della classe.
L’istruzione della classe implicitamente imposta alcuni attributi della classe. L’attributo __name__
è la stringa dell’identificatore NomeClasse utilizzato nella dichiarazione della classe. L’attributo __bases__
è la tupla di oggetti classe forniti (o impliciti) come classi base nella dichiarazione della classe. Per esempio, utilizzando la classe C1 appena creata:
1print(C1.__name__, C1.__bases__)
- 1
- Stampa del nome della classe e delle classi base.
Una classe ha anche un attributo chiamato __dict__
, che è la mappatura di sola lettura che la classe utilizza per mantenere altri attributi (noto anche, informalmente, come spazio dei nomi della classe).
Nelle istruzioni direttamente nel corpo di una classe, i riferimenti ai membri della classe devono utilizzare un nome semplice, non un nome completamente qualificato. Per esempio:
- 1
-
Definizione della classe
C3
. - 2
-
L’attributo x della classe
C3
è associato al valore23
. - 3
-
L’attributo y della classe
C3
è associato alla somma dix
e22
. - 4
-
Stampa del valore dell’attributo
y
della classeC3
.
Tuttavia, nelle istruzioni all’interno dei metodi definiti in un corpo di classe, i riferimenti agli attributi della classe devono utilizzare un nome completamente qualificato, non un nome semplice. Per esempio:
- 1
-
Definizione della classe
C4
. - 2
-
L’attributo x della classe
C4
è associato al valore23
. - 3
-
Definizione di un metodo della classe
C4
. - 4
-
Stampa del valore dell’attributo
x
della classeC4
. - 5
-
Creazione di un’istanza della classe
C4
. - 6
-
Chiamata del metodo dell’istanza
c
.
18.4.2 Definizioni di funzioni nel corpo di una classe
La maggior parte dei corpi delle classi include alcune istruzioni def
, poiché le funzioni (note come metodi in questo contesto) sono importanti membri per la maggior parte delle istanze delle classi. Un’istruzione def
in un corpo di classe obbedisce alle regole viste per le funzioni ordinarie.
Ecco un esempio di una classe che include una definizione di metodo:
- 1
-
Definizione della classe
C5
. - 2
-
Definizione di un metodo della classe
C5
. - 3
-
Stampa del messaggio
Ciao
. - 4
-
Creazione di un’istanza della classe
C5
. - 5
-
Chiamata del metodo
ciao()
dell’istanzac
.
18.4.3 Variabili private della classe
Quando un’istruzione in un corpo di classe (o in un metodo nel corpo) utilizza un identificatore che inizia (ma non termina) con due trattini bassi, come __ident
, Python cambia implicitamente l’identificatore in _NomeClasse__ident
, dove NomeClasse
è il nome della classe. Questo cambiamento implicito consente a una classe di utilizzare nomi privati per attributi, metodi, variabili globali e altri scopi, riducendo il rischio di duplicare accidentalmente i nomi utilizzati altrove (particolarmente nelle sottoclassi).
Per convenzione, gli identificatori che iniziano con un trattino basso sono considerati privati all’ambito che li associa, che tale ambito sia o meno una classe. Il compilatore Python non impone questa convenzione di privacy: è responsabilità dei programmatori rispettarla.
Esempio:
1class MiaClasse:
2 def __init__(self, valore):
3 self.__valore = valore
4 def visualizza_valore(self):
5 return self.__valore
6 def __metodo_privato(self):
7 return "Questo è un metodo privato"
8istanza = MiaClasse(10)
9print(istanza.visualizza_valore())
try:
10 print(istanza.__valore)
except Exception as e:
print(e)
11print(istanza._MiaClasse__valore)
try:
12 print(istanza.__metodo_privato())
except Exception as e:
print(e)
13print(istanza._MiaClasse__metodo_privato())
- 1
-
Definisce una classe
MiaClasse
. - 2
-
Definisce il metodo speciale
__init__
per l’inizializzazione. - 3
-
Utilizza un attributo con due trattini bassi all’inizio, che verrà trasformato in
_MiaClasse__valore
. - 4
-
Definisce un metodo
visualizza_valore
. - 5
-
Il metodo
visualizza_valore
restituisce l’attributo__valore
. - 6
-
Definisce un metodo privato
__metodo_privato
. - 7
-
Il metodo
__metodo_privato
restituisce una stringa. - 8
-
Crea un’istanza della classe
MiaClasse
con valore 10. - 9
-
Chiama il metodo
visualizza_valore
sull’istanza e stampa il risultato. Output:10
. - 10
-
Il tentativo di accedere direttamente all’attributo
__valore
genera un errore. Output:'MiaClasse' object has no attribute '__valore'
. - 11
-
Accede all’attributo
__valore
utilizzando il nome modificato_MiaClasse__valore
. Output:10
. - 12
-
Il tentativo di chiamare direttamente il metodo
__metodo_privato
genera un errore. Output:'MiaClasse' object has no attribute '__metodo_privato'
. - 13
-
Chiama il metodo privato
__metodo_privato
utilizzando il nome modificato_MiaClasse__metodo_privato
. Output:Questo è un metodo privato
.
In questo esempio, l’attributo __valore
e il metodo __metodo_privato
sono “rinominati” da Python per evitare conflitti di nomi, rendendoli più difficili da accedere accidentalmente. Tuttavia, questo non rende gli attributi o i metodi veramente privati, poiché possono ancora essere accessibili utilizzando il nome modificato. Quindi, l’uso del doppio trattino basso è una convenzione per indicare che un attributo o metodo è destinato all’uso interno della classe, non un meccanismo di sicurezza.
18.4.4 Stringhe di documentazione della classe
Se la prima istruzione nel corpo della classe è un letterale stringa, il compilatore associa quella stringa come stringa di documentazione (o docstring) per la classe. La docstring per la classe è disponibile nell’attributo __doc__
; se la prima istruzione nel corpo della classe non è un letterale stringa, il suo valore è None
.
Esempio di docstring di una classe:
- 1
-
Definizione della classe
C6
. - 2
-
Stringa di documentazione della classe
C6
. - 3
-
Definizione di un metodo della classe
C6
. - 4
- Corpo del metodo vuoto.
- 5
-
Stampa della docstring della classe
C6
.
18.5 Istanze
Per creare un’istanza di una classe, chiamiamo l’oggetto classe come se fosse una funzione. Ogni chiamata restituisce una nuova istanza il cui tipo è quella classe:
1un_istanza = C5()
- 1
-
Creazione di un’istanza della classe
C5
.
La funzione predefinita isinstance(i, C)
, con una classe come argomento C
, restituisce True
quando i
è un’istanza della classe C
o di qualsiasi sottoclasse di C
. Altrimenti, isinstance
restituisce False
. Se C
è una tupla di tipi (o più tipi uniti utilizzando l’operatore |
), isinstance
restituisce True
se i
è un’istanza o sottoclasse di uno dei tipi dati, e False
altrimenti.
18.5.1 __init__
Quando una classe definisce o eredita un metodo chiamato __init__
, chiamare l’oggetto classe esegue __init__
sulla nuova istanza per eseguire l’inizializzazione per istanza. Gli argomenti passati nella chiamata devono corrispondere ai parametri di __init__
, eccetto per il parametro self
. Per esempio, consideriamo la seguente definizione di classe:
- 1
-
Definizione della classe
C6
. - 2
-
Definizione del metodo
__init__
con il parametron
. - 3
-
Associazione dell’attributo
x
al valore del parametron
. - 4
-
Creazione di un’istanza della classe
C6
con il valore42
per il parametron
.
Il metodo __init__
di solito contiene istruzioni che associano attributi di istanza. Un metodo __init__
non deve restituire un valore diverso da None
; se lo fa, Python solleva un’eccezione TypeError
.
18.5.2 Membri degli oggetti istanza
Una volta creata un’istanza, possiamo accedere ai suoi membri (dati e metodi) utilizzando l’operatore punto .
.
- 1
-
Chiamata del metodo
ciao
dell’istanzaun_istanza
. - 2
-
Stampa del valore dell’attributo
x
dell’istanzaun_altra_istanza
.
Possiamo dare a un oggetto istanza un attributo associando un valore a un riferimento di attributo.
- 1
-
Definizione della classe
C7
. - 2
- Corpo della classe vuoto.
- 3
-
Creazione di un’istanza della classe
C7
. - 4
-
Associazione dell’attributo
x
dell’istanzaz
al valore23
. - 5
-
Stampa del valore dell’attributo
x
dell’istanzaz
.
18.6 Metodi vincolati e non vincolati
Il metodo __get__
di un oggetto funzione può restituire l’oggetto funzione stesso o un oggetto metodo vincolato che avvolge la funzione; un metodo vincolato è associato all’istanza specifica da cui è ottenuto. Un metodo vincolato è un’istanza di un metodo che è legato a un oggetto particolare, il che significa che può essere chiamato senza dover passare l’oggetto come parametro. Al contrario, un metodo non vincolato non è legato a un’istanza e deve essere esplicitamente passato un oggetto come parametro.
Quando un metodo è chiamato su un’istanza di una classe, Python crea un metodo vincolato, che ha un riferimento implicito all’istanza, passato come il primo argomento self
. Questo permette al metodo di accedere agli attributi e ad altri metodi della classe tramite self
.
Esempio di metodo vincolato:
1class C8:
2 def saluta(self):
3 print("Ciao!")
x = C8()
metodo_vincolato = x.saluta
metodo_vincolato()
- 1
-
Definizione della classe
C8
. - 2
-
Definizione di un metodo della classe
C8
chiamatosaluta
. - 3
-
Il metodo
saluta
stampa il
messaggio "Ciao!"
. 4. Creazione di un’istanza della classe C8
e assegnazione a x
. 5. Ottenimento di un metodo vincolato dall’istanza x
e assegnazione a metodo_vincolato
. Questo passo associa il metodo saluta
all’istanza x
, creando un metodo vincolato. 6. Chiamata del metodo vincolato. Questa chiamata è equivalente a x.saluta()
e stampa "Ciao!"
.
Quando metodo_vincolato
è chiamato, non c’è bisogno di passare x
come argomento perché x
è già legato al metodo vincolato. Questo è ciò che rende i metodi vincolati potenti e comodi da usare.
In contrasto, un metodo non vincolato può essere ottenuto dalla classe stessa. In tal caso, è necessario passare esplicitamente l’istanza come primo argomento.
Esempio di metodo non vincolato:
- 1
-
Ottenimento di un metodo non vincolato dalla classe
C8
e assegnazione ametodo_non_vincolato
. - 2
-
Chiamata del metodo non vincolato, passando esplicitamente l’istanza
x
come argomento. Questo passo è necessario per fornire il contesto (self
) per il metodo, poichémetodo_non_vincolato
non è legato a nessuna istanza.
In sintesi, i metodi vincolati consentono di chiamare metodi di istanza senza dover passare esplicitamente l’istanza, rendendo il codice più pulito e intuitivo.
18.7 Ereditarietà
Quando utilizziamo un riferimento a un attributo C.nome
su un oggetto classe C
, e nome
non è una chiave in C.__dict__
, la ricerca procede implicitamente su ogni oggetto classe che è in C.__bases__
in un ordine specifico (noto storicamente come ordine di risoluzione dei metodi, o MRO, ma che in realtà si applica a tutti gli attributi, non solo ai metodi). Le classi base di C
possono a loro volta avere le proprie basi. La ricerca controlla gli antenati diretti e indiretti, uno per uno, nell’MRO, fermandosi quando nome
viene trovato.
Esempio di ereditarietà:
1class Base:
2 a = 23
3 def saluta(self):
4 print("Ciao dal Base")
5class Derivata(Base):
6 b = 45
7d = Derivata()
8print(d.a)
9d.saluta()
- 1
-
Definizione della classe
Base
. - 2
-
L’attributo
a
della classe Base è associato al valore23
. - 3
-
Definizione di un metodo della classe
Base
. - 4
-
Stampa del messaggio
Ciao dal Base
. - 5
-
Definizione della classe Derivata che eredita da
Base
. - 6
-
L’attributo
b
della classe Derivata è associato al valore45
. - 7
-
Creazione di un’istanza della classe
Derivata
. - 8
-
Stampa del valore dell’attributo
a
dell’istanzad
. - 9
-
Chiamata del metodo
saluta
dell’istanzad
.
18.8 Metodi speciali
I metodi speciali in Python sono metodi con due trattini bassi all’inizio e alla fine del loro nome (dunder). Questi metodi vengono chiamati implicitamente in determinate situazioni. Ad esempio, il metodo __init__
viene chiamato quando viene creata una nuova istanza di una classe.
Esempio di metodo speciale:
1class C9:
2 def __init__(self, valore):
3 self.valore = valore
4 def __str__(self):
5 return f"C9 con valore: {self.valore}"
6c = C9(10)
7print(c)
- 1
-
Definizione della classe
C9
. - 2
-
Definizione del metodo
__init__
con il parametrovalore
. - 3
-
Associazione dell’attributo
valore
al valore del parametrovalore
. - 4
-
Definizione del metodo
__str__
. - 5
- Restituzione di una stringa rappresentativa dell’istanza.
- 6
-
Creazione di un’istanza della classe
C9
con il valore10
. - 7
-
Stampa della rappresentazione dell’istanza
c
.
18.9 Metodi di classe e metodi statici
In Python, i metodi possono essere definiti a livello di classe in due modi principali: come metodi statici e come metodi di classe. Entrambi offrono funzionalità diverse rispetto ai metodi di istanza.
18.9.1 Metodi statici
I metodi statici sono definiti utilizzando il decoratore @staticmethod
. Questi metodi non ricevono automaticamente né il riferimento all’istanza (self
) né alla classe (cls
) come primo argomento. Sono simili alle funzioni normali, ma sono chiamati come membri della classe. Sono utili quando non è necessario accedere né alle variabili di istanza né alle variabili di classe all’interno del metodo.
Esempio di metodo statico:
1class Utilita:
2 @staticmethod
3 def somma(a, b):
4 return a + b
# Chiamata al metodo statico senza creare un'istanza
5print(Utilita.somma(10, 5))
- 1
-
Definizione della classe
Utilita
. - 2
-
Decoratore
@staticmethod
per definire un metodo statico. - 3
-
Definizione del metodo statico
somma
. - 4
-
Il metodo
somma
prende due argomenti e restituisce la loro somma. - 5
-
Chiamata al metodo statico
somma
senza creare un’istanza della classe.
18.9.2 Metodi di classe
I metodi di classe sono definiti utilizzando il decoratore @classmethod
. Questi metodi ricevono automaticamente un riferimento alla classe (cls
) come primo argomento. Sono utili quando si ha bisogno di accedere o modificare lo stato della classe piuttosto che quello dell’istanza.
Esempio di metodo di classe:
1class Contatore:
2 contatore_comune = 0
3 def __init__(self):
4 Contatore.contatore_comune += 1
5 @classmethod
6 def ottieni_contatore(cls):
7 return cls.contatore_comune
# Creazione di istanze
8c1 = Contatore()
9c2 = Contatore()
# Chiamata al metodo di classe
10print(Contatore.ottieni_contatore())
- 1
-
Definizione della classe
Contatore
. - 2
-
Attributo di classe
contatore_comune
. - 3
-
Metodo speciale
__init__
per l’inizializzazione. - 4
-
Incrementa l’attributo di classe
contatore_comune
ogni volta che viene creata una nuova istanza. - 5
-
Decoratore
@classmethod
per definire un metodo di classe. - 6
-
Definizione del metodo di classe
ottieni_contatore
. - 7
-
Il metodo
ottieni_contatore
restituisce il valore dell’attributo di classecontatore_comune
. - 8
-
Creazione di un’istanza della classe
Contatore
. - 9
-
Creazione di un’altra istanza della classe
Contatore
. - 10
-
Chiamata al metodo di classe
ottieni_contatore
senza creare un’istanza della classe.
18.9.3 Differenza tra metodi statici e metodi di classe
Metodi statici:
- Non ricevono né l’istanza (
self
) né la classe (cls
) come primo argomento. - Sono simili alle funzioni normali, ma possono essere chiamati attraverso il nome della classe.
- Utili per operazioni che non dipendono dallo stato della classe o dell’istanza.
- Non ricevono né l’istanza (
Metodi di classe:
- Ricevono il riferimento alla classe (
cls
) come primo argomento. - Possono accedere e modificare lo stato della classe.
- Utili per operazioni che riguardano lo stato globale della classe.
- Ricevono il riferimento alla classe (
18.9.4 Casi d’uso
Utilizzare un metodo statico per una funzione di utilità che non necessita di accedere o modificare lo stato della classe o dell’istanza.
- Output:
12
.
Utilizzare un metodo di classe per mantenere o ottenere lo stato della classe, come contare il numero di istanze create.
class Giocatore: numero_giocatori = 0 def __init__(self, nome): self.nome = nome Giocatore.numero_giocatori += 1 @classmethod def ottieni_numero_giocatori(cls): return cls.numero_giocatori g1 = Giocatore("Mario") g2 = Giocatore("Luigi") 1print(Giocatore.ottieni_numero_giocatori())
- 1
-
Output:
2
.
18.10 Descrittori
Un descrittore è un oggetto la cui classe fornisce uno o più metodi speciali chiamati __get__
, __set__
o __delete__
. I descrittori che sono attributi di classe controllano la semantica di accesso e impostazione degli attributi sulle istanze di quella classe.
Esempio di un descrittore:
1class Const:
2 def __init__(self, value):
3 self.__dict__['value'] = value
4 def __set__(self, *_):
5 pass
6 def __get__(self, *_):
7 return self.__dict__['value']
8 def __delete__(self, *_):
9 pass
10class X:
11 c = Const(23)
12x = X()
13print(x.c)
14x.c = 42
15print(x.c)
16del x.c
17print(x.c)
- 1
-
Definizione della classe
Const
. - 2
- Inizializzazione del descrittore con un valore.
- 3
- Memorizzazione del valore nel dizionario dell’istanza.
- 4
-
Metodo
__set__
del descrittore. - 5
- Ignora qualsiasi tentativo di impostazione del valore.
- 6
-
Metodo
__get__
del descrittore. - 7
- Restituisce il valore memorizzato nel dizionario dell’istanza.
- 8
-
Metodo
__delete__
del descrittore. - 9
- Ignora qualsiasi tentativo di eliminazione del valore.
- 10
-
Definizione della classe
X
. - 11
-
L’attributo
c
della classeX
è un descrittore di tipoConst
. - 12
-
Creazione di un’istanza della classe
X
. - 13
-
Stampa del valore dell’attributo
c
dell’istanzax
. - 14
-
Tentativo di impostazione del valore dell’attributo
c
dell’istanzax
(ignorato). - 15
-
Stampa del valore dell’attributo
c
dell’istanzax
. - 16
-
Tentativo di eliminazione dell’attributo
c
dell’istanzax
(ignorato). - 17
-
Stampa del valore dell’attributo
c
dell’istanzax
.
18.10.1 Accesso
Nel caso di accesso ad un membro di una classe sia un descrittore, le regole viste in precedenza si qualificano come di seguito:
- Se il membro è un descrittore non sovrascrivente (ovvero, un descrittore che implementa solo il metodo
__get__
), viene restituito il risultato del metodo__get__
del descrittore. Esempio:
class DescrittoreNonSovrascrivente:
def __init__(self, valore):
self.valore = valore
def __get__(self, istanza, proprietario):
1 return self.valore
class MiaClasse:
2 descrittore = DescrittoreNonSovrascrivente("valore descrittore")
istanza = MiaClasse()
3print(istanza.descrittore)
- 1
-
Il metodo
__get__
restituisce il valore del descrittore. - 2
- Definisce un descrittore non sovrascrivente come attributo di classe.
- 3
-
Trova
descrittore
nella classe, chiamaDescrittoreNonSovrascrivente.__get__
e restituisce"valore descrittore"
.
- Se il membro è un descrittore sovrascrivente (ovvero, un descrittore che implementa i metodi
__get__
e__set__
), viene chiamato il metodo__get__
del descrittore, che restituisce il valore dell’attributo. Esempio:
class DescrittoreSovrascrivente:
def __init__(self, valore):
self.valore = valore
def __get__(self, istanza, proprietario):
1 return self.valore
def __set__(self, istanza, valore):
2 self.valore = valore
class MiaClasse:
3 descrittore = DescrittoreSovrascrivente("valore iniziale")
istanza = MiaClasse()
4print(istanza.descrittore)
5istanza.descrittore = "nuovo valore"
6print(istanza.descrittore)
- 1
-
Il metodo
__get__
restituisce il valore del descrittore. - 2
-
Il metodo
__set__
permette di modificare il valore del descrittore. - 3
- Definisce un descrittore sovrascrivente come attributo di classe.
- 4
-
Trova
descrittore
nella classe, chiamaDescrittoreSovrascrivente.__get__
e restituisce"valore iniziale"
. - 5
-
Chiama
DescrittoreSovrascrivente.__set__
per modificare il valore. - 6
-
Trova
descrittore
nella classe, chiamaDescrittoreSovrascrivente.__get__
e restituisce"nuovo valore"
.
- Se il membro non è un descrittore, viene restituito il valore associato al membro. Esempio:
class ClasseBase:
1 attributo_base = "valore base"
class MiaClasse(ClasseBase):
pass
istanza = MiaClasse()
2print(istanza.attributo_base)
- 1
- Definisce un attributo di classe non descrittore.
- 2
-
Trova
attributo_base
nella classe base e restituisce"valore base"
.
18.10.2 Usi comuni
Un uso comune dei descrittori è la validazione degli attributi. Ad esempio, possiamo creare un descrittore per assicurare che un attributo sia sempre un numero positivo.
1class PositiveNumber:
2 def __init__(self):
3 self.value = None
4 def __get__(self, instance, owner):
5 return self.value
6 def __set__(self, instance, value):
7 if value < 0:
8 raise ValueError("Il valore deve essere positivo")
9 self.value = value
10class Prodotto:
11 prezzo = PositiveNumber()
12p = Prodotto()
13p.prezzo = 10
14print(p.prezzo)
15p.prezzo = -5
- 1
-
Definisce un descrittore
PositiveNumber
. - 2
- Inizializzazione del descrittore.
- 3
- Memorizzazione del valore.
- 4
-
Metodo
__get__
del descrittore per ottenere il valore. - 5
- Restituisce il valore memorizzato.
- 6
-
Metodo
__set__
del descrittore per impostare il valore. - 7
- Controlla se il valore è negativo.
- 8
- Solleva un’eccezione se il valore è negativo.
- 9
- Imposta il valore se è positivo.
- 10
-
Definisce una classe
Prodotto
. - 11
-
L’attributo
prezzo
della classeProdotto
è un descrittore di tipoPositiveNumber
. - 12
-
Creazione di un’istanza della classe
Prodotto
. - 13
-
Imposta il valore dell’attributo
prezzo
dell’istanzap
. - 14
-
Stampa il valore dell’attributo
prezzo
dell’istanzap
. - 15
-
Tentativo di impostazione di un valore negativo per l’attributo
prezzo
(solleva un’eccezione).
Un altro uso comune dei descrittori è la memorizzazione nella cache dei risultati di calcoli costosi.
1class CachedProperty:
2 def __init__(self, func):
3 self.func = func
4 self.value = None
5 self.is_cached = False
6 def __get__(self, instance, owner):
7 if not self.is_cached:
8 self.value = self.func(instance)
9 self.is_cached = True
10 return self.value
11class DatiComplessi:
12 @CachedProperty
13 def calcolo_costoso(self):
14 print("Calcolo in corso...")
15 return 42
16d = DatiComplessi()
17print(d.calcolo_costoso)
18print(d.calcolo_costoso)
- 1
-
Definisce un descrittore
CachedProperty
. - 2
- Inizializzazione del descrittore con una funzione.
- 3
- Memorizza la funzione.
- 4
- Memorizza il valore.
- 5
- Flag per indicare se il valore è memorizzato nella cache.
- 6
-
Metodo
__get__
del descrittore per ottenere il valore. - 7
- Controlla se il valore è memorizzato nella cache.
- 8
- Calcola il valore e lo memorizza nella cache se non è presente.
- 9
- Imposta il flag di cache.
- 10
- Restituisce il valore memorizzato nella cache.
- 11
-
Definisce una classe
DatiComplessi
. - 12
-
Utilizza
CachedProperty
per decorare un metodo. - 13
-
Definisce un metodo
calcolo_costoso
. - 14
- Stampa un messaggio durante il calcolo.
- 15
- Restituisce un valore.
- 16
-
Creazione di un’istanza della classe
DatiComplessi
. - 17
-
Chiama
calcolo_costoso
e stampa il risultato (calcola e memorizza nella cache). - 18
-
Chiama
calcolo_costoso
e stampa il risultato (usa il valore memorizzato nella cache).
18.11 Decoratori
I decoratori in Python sono funzioni che modificano il comportamento di altre funzioni o metodi. Sono utili per estendere la funzionalità di funzioni o metodi senza modificarne il codice.
Esempio di decoratore:
1def mio_decoratore(f):
2 def wrapper():
3 print("Qualcosa prima della funzione")
4 f()
5 print("Qualcosa dopo la funzione")
6 return wrapper
7@mio_decoratore
8def di_ciao():
9 print("Ciao!")
10di_ciao()
- 1
-
Definizione del decoratore
mio_decoratore
. - 2
-
Definizione della funzione
wrapper
interna. - 3
- Stampa di un messaggio prima della chiamata della funzione decorata.
- 4
- Chiamata della funzione decorata.
- 5
- Stampa di un messaggio dopo la chiamata della funzione decorata.
- 6
-
Restituzione della funzione
wrapper
. - 7
-
Applicazione del decoratore
mio_decoratore
alla funzionedi_ciao
. - 8
-
Definizione della funzione
di_ciao
. - 9
-
Stampa del messaggio
Ciao!
. - 10
-
Chiamata della funzione
di_ciao
decorata. Output:
L’analogo del codice precedente senza l’uso della sintassi di decorazione @
, esplicita ciò che avviene dietro le quinte:
1def mio_decoratore(f):
2 def wrapper():
3 print("Qualcosa prima della funzione")
4 f()
5 print("Qualcosa dopo la funzione")
6 return wrapper
7def di_ciao():
8 print("Ciao!")
9di_ciao = mio_decoratore(di_ciao)
di_ciao()
- 1
-
Definizione del decoratore
mio_decoratore
. - 2
-
Definizione della funzione
wrapper
interna. - 3
- Stampa di un messaggio prima della chiamata della funzione decorata.
- 4
- Chiamata della funzione decorata.
- 5
- Stampa di un messaggio dopo la chiamata della funzione decorata.
- 6
-
Restituzione della funzione
wrapper
. - 7
-
Definizione della funzione
di_ciao
. - 8
-
Stampa del messaggio
Ciao!
. - 9
- Applicazione
del decoratore mio_decoratore
alla funzione di_ciao
assegnando di_ciao
alla funzione wrapper
restituita. 10. Chiamata della funzione di_ciao
decorata. Output: python Qualcosa prima della funzione Ciao! Qualcosa dopo la funzione
Nel primo esempio, la sintassi @mio_decoratore
applica il decoratore alla funzione di_ciao
direttamente sopra la definizione della funzione. Nel secondo esempio, il decoratore mio_decoratore
viene applicato esplicitamente assegnando di_ciao
alla funzione wrapper
restituita dal decoratore. Entrambi gli approcci producono lo stesso risultato, ma il secondo esempio mostra chiaramente come tutte le volte che chiamo la funzione devo modificare la sintassi mentre usando il decoratore nella definizione della funzione, ho una sola variazione che non impatta i codici che utilizzano la di_ciao()
.