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:

1class NomeClasse(classi-base, *, **kw):
2  istruzione(i)
1
Testata della definizione di classe: NomeClasse è un identificatore, cioè una variabile che la definizione class, 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, come Elemento, 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 denominato metaclass 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 valore Valore 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 valore Valore 1.
8
Crea un’istanza della classe MiaClasse con valore Valore 2.
9
Stampa il valore dell’attributo di classe attributo_classe.
10
Stampa il valore dell’attributo di classe attributo_classe dall’istanza istanza1.
11
Stampa il valore dell’attributo di istanza attributo_istanza per istanza1.
12
Stampa il valore dell’attributo di istanza attributo_istanza per istanza2.
13
Chiama il metodo mostra_attributi sull’istanza istanza1.
14
Chiama il metodo mostra_attributi sull’istanza istanza2.

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:

  1. Membro nell’istanza: Se 'nome' è una chiave in x.__dict__, x.nome restituisce il valore associato a quella chiave. Questo è il caso più semplice e veloce.

  2. Membro nella classe o nelle sue basi: Se 'nome' non è una chiave in x.__dict__, la ricerca del membro procede nella classe di x (x.__class__) e nelle sue basi, seguendo l’ordine di risoluzione dei metodi (method resolution order, MRO).

  3. 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’eccezione AttributeError.

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 classe Base è associato al valore 23.
3
Definizione della classe Derivata che eredita da Base.
4
L’attributo b della classe Derivata è associato al valore 45.
5
Creazione di un’istanza della classe Derivata.
6
Associazione dell’attributo c dell’istanza d al valore 67.
7
Stampa del valore dell’attributo a dell’istanza d, trovato nella classe base. Output: 23.
8
Stampa del valore dell’attributo b dell’istanza d, trovato nella classe derivata. Output: 45.
9
Stampa del valore dell’attributo c dell’istanza d, 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’attributo valore 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’istanza c1.
12
Incrementa il valore dell’attributo valore dell’istanza c2.
13
Stampa il valore dell’attributo valore dell’istanza c1. Output: 1.
14
Stampa il valore dell’attributo valore dell’istanza c2. 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 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:

    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:

    1class MiaClasse:
    2  primo_attributo = "esempio"
    
    3print(MiaClasse.primo_attributo)
    1
    Definisce una classe MiaClasse.
    2
    Associa un attributo di classe primo_attributo con valore esempio.
    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 valore esempio.
    3
    Definisce un metodo primo_metodo con un parametro self.
    4
    Il metodo primo_metodo restituisce una stringa Ciao, mondo!.
    5
    Stampa il valore dell’attributo di classe primo_attributo.
    6
    Crea un’istanza della classe MiaClasse e la associa a mia_istanza.
    7
    Chiama il metodo primo_metodo sull’istanza mia_istanza. Python passa automaticamente mia_istanza come argomento per self.
    8
    Chiama il metodo primo_metodo direttamente sulla classe MiaClasse, passando esplicitamente mia_istanza come argomento per self.
  • 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 stringa Questo è 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 valore 10.
    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 da ClasseBase.
    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 a MiaClasse.
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:

1class C1:
2  x = 23

3print(C1.x)
1
Definizione della classe C1.
2
L’attributo x della classe C1 è associato al valore 23.
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:

1class C2:
2  pass

3C2.x = 23

4print(C2.x)
1
Definizione della classe C2.
2
Corpo della classe vuoto.
3
Associazione dell’attributo x della classe C2 al valore 23.
4
Stampa del valore dell’attributo x della classe C2.

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:

1class C3:
2  x = 23
3  y = x + 22

4print(C3.y)
1
Definizione della classe C3.
2
L’attributo x della classe C3 è associato al valore 23.
3
L’attributo y della classe C3 è associato alla somma di x e 22.
4
Stampa del valore dell’attributo y della classe C3.

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:

1class C4:
2  x = 23
  
3  def metodo(self):
4    print(C4.x)

5c = C4()

6c.metodo()
1
Definizione della classe C4.
2
L’attributo x della classe C4 è associato al valore 23.
3
Definizione di un metodo della classe C4.
4
Stampa del valore dell’attributo x della classe C4.
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:

1class C5:
2  def ciao(self):
3    print('Ciao')

4c = C5()
5c.ciao()
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’istanza c.

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:

1class C6:
2  """Questa è una classe di esempio."""
  
3  def metodo(self):
4    pass

5print(C6.__doc__)
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:

1class C6:
2  def __init__(self, n):
3    self.x = n

4un_altra_istanza = C6(42)
1
Definizione della classe C6.
2
Definizione del metodo __init__ con il parametro n.
3
Associazione dell’attributo x al valore del parametro n.
4
Creazione di un’istanza della classe C6 con il valore 42 per il parametro n.

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 ..

1un_istanza.ciao()

2print(un_altra_istanza.x)
1
Chiamata del metodo ciao dell’istanza un_istanza.
2
Stampa del valore dell’attributo x dell’istanza un_altra_istanza.

Possiamo dare a un oggetto istanza un attributo associando un valore a un riferimento di attributo.

1class C7:
2  pass

3z = C7()

4z.x = 23

5print(z.x)
1
Definizione della classe C7.
2
Corpo della classe vuoto.
3
Creazione di un’istanza della classe C7.
4
Associazione dell’attributo x dell’istanza z al valore 23.
5
Stampa del valore dell’attributo x dell’istanza z.

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 chiamato saluta.
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:

1metodo_non_vincolato = C8.saluta

2metodo_non_vincolato(x)
1
Ottenimento di un metodo non vincolato dalla classe C8 e assegnazione a metodo_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 valore 23.
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 valore 45.
7
Creazione di un’istanza della classe Derivata.
8
Stampa del valore dell’attributo a dell’istanza d.
9
Chiamata del metodo saluta dell’istanza d.

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 parametro valore.
3
Associazione dell’attributo valore al valore del parametro valore.
4
Definizione del metodo __str__.
5
Restituzione di una stringa rappresentativa dell’istanza.
6
Creazione di un’istanza della classe C9 con il valore 10.
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 classe contatore_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.
  • 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.

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.

    class Matematica:
      @staticmethod
      def moltiplica(a, b):
        return a * b
    
      @staticmethod
      def dividi(a, b):
        return a / b
    
    print(Matematica.moltiplica(3, 4))
  1. 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 classe X è un descrittore di tipo Const.
12
Creazione di un’istanza della classe X.
13
Stampa del valore dell’attributo c dell’istanza x.
14
Tentativo di impostazione del valore dell’attributo c dell’istanza x (ignorato).
15
Stampa del valore dell’attributo c dell’istanza x.
16
Tentativo di eliminazione dell’attributo c dell’istanza x (ignorato).
17
Stampa del valore dell’attributo c dell’istanza x.

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, chiama DescrittoreNonSovrascrivente.__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, chiama DescrittoreSovrascrivente.__get__ e restituisce "valore iniziale".
5
Chiama DescrittoreSovrascrivente.__set__ per modificare il valore.
6
Trova descrittore nella classe, chiama DescrittoreSovrascrivente.__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 classe Prodotto è un descrittore di tipo PositiveNumber.
12
Creazione di un’istanza della classe Prodotto.
13
Imposta il valore dell’attributo prezzo dell’istanza p.
14
Stampa il valore dell’attributo prezzo dell’istanza p.
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 funzione di_ciao.
8
Definizione della funzione di_ciao.
9
Stampa del messaggio Ciao!.
10
Chiamata della funzione di_ciao decorata. Output:
Qualcosa prima della funzione
Ciao!
Qualcosa dopo la funzione

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().