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.**kwpuò includere un argomento denominatometaclassper 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
valoredell’istanza. - 4
-
Crea un’istanza della classe
MiaClassecon 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_classecon valoreValore di classe. - 3
-
Definisce il metodo speciale
__init__per l’inizializzazione. - 4
-
Associa un valore all’attributo
attributo_istanzadell’istanza. - 5
-
Definisce un metodo
mostra_attributiper mostrare gli attributi. - 6
-
Il metodo
mostra_attributirestituisce una stringa con gli attributi di classe e di istanza. - 7
-
Crea un’istanza della classe
MiaClassecon valoreValore 1. - 8
-
Crea un’istanza della classe
MiaClassecon valoreValore 2. - 9
-
Stampa il valore dell’attributo di classe
attributo_classe. - 10
-
Stampa il valore dell’attributo di classe
attributo_classedall’istanzaistanza1. - 11
-
Stampa il valore dell’attributo di istanza
attributo_istanzaperistanza1. - 12
-
Stampa il valore dell’attributo di istanza
attributo_istanzaperistanza2. - 13
-
Chiama il metodo
mostra_attributisull’istanzaistanza1. - 14
-
Chiama il metodo
mostra_attributisull’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.nomerestituisce 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
adella classeBaseè associato al valore23. - 3
-
Definizione della classe
Derivatache eredita daBase. - 4
-
L’attributo
bdella classeDerivataè associato al valore45. - 5
-
Creazione di un’istanza della classe
Derivata. - 6
-
Associazione dell’attributo
cdell’istanzadal valore67. - 7
-
Stampa del valore dell’attributo
adell’istanzad, trovato nella classe base. Output:23. - 8
-
Stampa del valore dell’attributo
bdell’istanzad, trovato nella classe derivata. Output:45. - 9
-
Stampa del valore dell’attributo
cdell’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_comuneinizializzato a 0. - 3
-
Definisce il metodo speciale
__init__per l’inizializzazione. - 4
-
Associa un valore iniziale all’attributo
valoredell’istanza. - 5
-
Incrementa l’attributo di classe
contatore_comuneogni volta che viene creata una nuova istanza. - 6
-
Definisce un metodo
incrementaper incrementare l’attributovaloredell’istanza. - 7
-
Incrementa l’attributo
valoredell’istanza. - 8
-
Stampa il valore dell’attributo di classe
contatore_comune. - 9
-
Creazione di un’istanza della classe
Contatorecon valore iniziale di default. - 10
-
Creazione di un’istanza della classe
Contatorecon valore iniziale 10. - 11
-
Incrementa il valore dell’attributo
valoredell’istanzac1. - 12
-
Incrementa il valore dell’attributo
valoredell’istanzac2. - 13
-
Stampa il valore dell’attributo
valoredell’istanzac1. Output:1. - 14
-
Stampa il valore dell’attributo
valoredell’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
valoredell’istanza. - 4
-
Crea un’istanza della classe
MiaClassecon 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_attributocon 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_attributocon valoreesempio. - 3
-
Definisce un metodo
primo_metodocon un parametroself. - 4
-
Il metodo
primo_metodorestituisce una stringaCiao, mondo!. - 5
-
Stampa il valore dell’attributo di classe
primo_attributo. - 6
-
Crea un’istanza della classe
MiaClassee la associa amia_istanza. - 7
-
Chiama il metodo
primo_metodosull’istanzamia_istanza. Python passa automaticamentemia_istanzacome argomento perself. - 8
-
Chiama il metodo
primo_metododirettamente sulla classeMiaClasse, passando esplicitamentemia_istanzacome 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_metodorestituisce una stringaQuesto è un metodo. - 4
-
Crea un’istanza della classe
MiaClasse. - 5
-
Chiama il metodo
primo_metodosull’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
valoredell’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
MiaClassecon 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_baserestituisce una stringa “Metodo base”. - 4
-
Definisce una classe
ClasseDerivatache eredita daClasseBase. - 5
-
Definisce un metodo
metodo_derivato. - 6
-
metodo_derivatorestituisce una stringa “Metodo derivato”. - 7
-
Crea un’istanza della classe
ClasseDerivata. - 8
-
Chiama il metodo
metodo_baseereditato dall’istanza e stampa il risultato. - 9
-
Chiama il metodo
metodo_derivatodell’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
valoredell’istanza. - 4
-
Crea un’istanza della classe
MiaClassecon valore 10. - 5
-
Stampa il valore dell’attributo
valoredell’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
NuovaClasseall’interno della funzione. - 3
-
passindica che il corpo della classe è vuoto. - 4
-
Restituisce la classe
NuovaClasse. - 5
-
Chiama la funzione
crea_classee 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
xdella 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
C2al valore23. - 4
-
Stampa del valore dell’attributo
xdella 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 dixe22. - 4
-
Stampa del valore dell’attributo
ydella 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
xdella 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_valorerestituisce l’attributo__valore. - 6
-
Definisce un metodo privato
__metodo_privato. - 7
-
Il metodo
__metodo_privatorestituisce una stringa. - 8
-
Crea un’istanza della classe
MiaClassecon valore 10. - 9
-
Chiama il metodo
visualizza_valoresull’istanza e stampa il risultato. Output:10. - 10
-
Il tentativo di accedere direttamente all’attributo
__valoregenera un errore. Output:'MiaClasse' object has no attribute '__valore'. - 11
-
Accede all’attributo
__valoreutilizzando il nome modificato_MiaClasse__valore. Output:10. - 12
-
Il tentativo di chiamare direttamente il metodo
__metodo_privatogenera un errore. Output:'MiaClasse' object has no attribute '__metodo_privato'. - 13
-
Chiama il metodo privato
__metodo_privatoutilizzando 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
xal valore del parametron. - 4
-
Creazione di un’istanza della classe
C6con il valore42per 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
ciaodell’istanzaun_istanza. - 2
-
Stampa del valore dell’attributo
xdell’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
xdell’istanzazal valore23. - 5
-
Stampa del valore dell’attributo
xdell’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
C8chiamatosaluta. - 3
-
Il metodo
salutastampa 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
C8e assegnazione ametodo_non_vincolato. - 2
-
Chiamata del metodo non vincolato, passando esplicitamente l’istanza
xcome argomento. Questo passo è necessario per fornire il contesto (self) per il metodo, poichémetodo_non_vincolatonon è 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
adella 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
bdella classe Derivata è associato al valore45. - 7
-
Creazione di un’istanza della classe
Derivata. - 8
-
Stampa del valore dell’attributo
adell’istanzad. - 9
-
Chiamata del metodo
salutadell’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
valoreal valore del parametrovalore. - 4
-
Definizione del metodo
__str__. - 5
- Restituzione di una stringa rappresentativa dell’istanza.
- 6
-
Creazione di un’istanza della classe
C9con 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
@staticmethodper definire un metodo statico. - 3
-
Definizione del metodo statico
somma. - 4
-
Il metodo
sommaprende due argomenti e restituisce la loro somma. - 5
-
Chiamata al metodo statico
sommasenza 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_comuneogni volta che viene creata una nuova istanza. - 5
-
Decoratore
@classmethodper definire un metodo di classe. - 6
-
Definizione del metodo di classe
ottieni_contatore. - 7
-
Il metodo
ottieni_contatorerestituisce 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_contatoresenza 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
cdella classeXè un descrittore di tipoConst. - 12
-
Creazione di un’istanza della classe
X. - 13
-
Stampa del valore dell’attributo
cdell’istanzax. - 14
-
Tentativo di impostazione del valore dell’attributo
cdell’istanzax(ignorato). - 15
-
Stampa del valore dell’attributo
cdell’istanzax. - 16
-
Tentativo di eliminazione dell’attributo
cdell’istanzax(ignorato). - 17
-
Stampa del valore dell’attributo
cdell’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
descrittorenella 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
descrittorenella classe, chiamaDescrittoreSovrascrivente.__get__e restituisce"valore iniziale". - 5
-
Chiama
DescrittoreSovrascrivente.__set__per modificare il valore. - 6
-
Trova
descrittorenella 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_basenella 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
prezzodella classeProdottoè un descrittore di tipoPositiveNumber. - 12
-
Creazione di un’istanza della classe
Prodotto. - 13
-
Imposta il valore dell’attributo
prezzodell’istanzap. - 14
-
Stampa il valore dell’attributo
prezzodell’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
CachedPropertyper 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_costosoe stampa il risultato (calcola e memorizza nella cache). - 18
-
Chiama
calcolo_costosoe 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
wrapperinterna. - 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_decoratorealla funzionedi_ciao. - 8
-
Definizione della funzione
di_ciao. - 9
-
Stampa del messaggio
Ciao!. - 10
-
Chiamata della funzione
di_ciaodecorata. 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
wrapperinterna. - 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().