9. Classi
*********

Le classi forniscono un mezzo per raggruppare dati e funzionalità
insieme. Creare una nuova classe crea un nuovo *tipo* di oggetto,
consentendo la creazione di nuove *istanze* di quel tipo. Ogni istanza
di classe può avere attributi ad essa associati per mantenere il suo
stato. Le istanze di classe possono anche avere metodi (definiti dalla
sua classe) per modificarne lo stato.

Rispetto ad altri linguaggi di programmazione, il meccanismo delle
classi di Python aggiunge classi con un minimo di nuova sintassi e
semantica. È un misto dei meccanismi di classe trovati in C++ e
Modula-3. Le classi Python forniscono tutte le funzionalità standard
della programmazione orientata agli oggetti: il meccanismo di
ereditarietà delle classi consente più classi di base, una classe
derivata può sovrascrivere qualsiasi metodo della sua classe base o
classi, e un metodo può chiamare il metodo di una classe di base con
lo stesso nome. Gli oggetti possono contenere quantità e tipi di dati
arbitrari. Come è vero per i moduli, le classi partecipano alla natura
dinamica di Python: sono create a tempo di esecuzione e possono essere
modificate ulteriormente dopo la creazione.

Nella terminologia di C++, normalmente i membri della classe (compresi
i membri dei dati) sono *pubblici* (tranne vedere sotto Variabili
Private), e tutte le funzioni membro sono *virtuali*. Come in
Modula-3, non ci sono abbreviazioni per fare riferimento ai membri
dell'oggetto dai suoi metodi: la funzione del metodo è dichiarata con
un primo argomento esplicito che rappresenta l'oggetto, che è fornito
implicitamente dalla chiamata. Come in Smalltalk, le classi stesse
sono oggetti. Questo fornisce la semantica per l'importazione e il
ridenominare. A differenza di C++ e Modula-3, i tipi integrati possono
essere utilizzati come classi di base per l'estensione da parte
dell'utente. Inoltre, come in C++, la maggior parte degli operatori
integrati con una sintassi speciale (operatori aritmetici,
indicizzazione ecc.) possono essere ridefiniti per le istanze della
classe.

(Mancando una terminologia universalmente accettata per parlare di
classi, farò un uso occasionale dei termini Smalltalk e C++.
Utilizzerei i termini di Modula-3, poiché la sua semantica orientata
agli oggetti è più vicina a quella di Python rispetto a C++, ma mi
aspetto che pochi lettori ne abbiano sentito parlare.)


9.1. Una parola su nomi e oggetti
=================================

Gli oggetti hanno individualità e più nomi (in più ambiti) possono
essere vincolati allo stesso oggetto. Questo è noto come aliasing in
altri linguaggi. Questo di solito non è apprezzato a una prima
occhiata a Python e può essere tranquillamente ignorato quando si
tratta di tipi di base immutabili (numeri, stringhe, tuple). Tuttavia,
l'aliasing ha un effetto possibilmente sorprendente sulla semantica
del codice Python che coinvolge oggetti mutabili come liste, dizionari
e la maggior parte degli altri tipi. Questo è di solito usato a
vantaggio del programma, poiché gli alias si comportano come puntatori
in alcuni aspetti. Ad esempio, passare un oggetto è economico poiché
viene passato solo un puntatore dall'implementazione; e se una
funzione modifica un oggetto passato come argomento, il chiamante
vedrà il cambiamento --- questo elimina la necessità di due diversi
meccanismi di passaggio degli argomenti come in Pascal.


9.2. Visibilità e spazi dei nomi in Python
==========================================

Prima di introdurre le classi, devo prima dirti qualcosa sulle regole
di visibilità di Python. Le definizioni di classe fanno alcuni trucchi
interessanti con gli spazi dei nomi e devi sapere come funzionano le
visibilità e gli spazi dei nomi per capire appieno cosa sta
succedendo. Per inciso, la conoscenza su questo argomento è utile per
qualsiasi programmatore Python avanzato.

Iniziamo con alcune definizioni.

Uno *spazio dei nomi* è una mappatura da nomi a oggetti. La maggior
parte degli spazi dei nomi è attualmente implementata come dizionari
Python, ma normalmente non è percepibile in alcun modo (tranne per le
prestazioni) e potrebbe cambiare in futuro. Esempi di spazi dei nomi
sono: l'insieme di nomi integrati (contenente funzioni come "abs()" e
nomi di eccezioni integrate); i nomi globali in un modulo; e i nomi
locali in una chiamata di funzione. In un certo senso, l'insieme di
attributi di un oggetto forma anche uno spazio dei nomi. La cosa
importante da sapere sugli spazi dei nomi è che non c'è assolutamente
alcuna relazione tra nomi in spazi dei nomi diversi; ad esempio, due
moduli diversi possono entrambi definire una funzione "massimizza"
senza confusione --- gli utenti dei moduli devono prefissarlo con il
nome del modulo.

A proposito, uso la parola *attributo* per qualsiasi nome che segue un
punto --- ad esempio, nell'espressione "z.reale", "reale" è un
attributo dell'oggetto "z". Più precisamente, i riferimenti ai nomi
nei moduli sono riferimenti agli attributi: nell'espressione
"nomemodulo.nomefunzione", "nomemodulo" è un oggetto modulo e
"nomefunzione" è un suo attributo. In questo caso c'è una
corrispondenza diretta tra gli attributi del modulo e i nomi globali
definiti nel modulo: condividono lo stesso spazio dei nomi! [1]

Gli attributi possono essere di sola lettura o scrivibili. In
quest'ultimo caso, l'assegnazione agli attributi è possibile. Gli
attributi del modulo sono scrivibili: puoi scrivere
"nomemodulo.larisposta = 42". Gli attributi scrivibili possono anche
essere eliminati con l'istruzione "del". Ad esempio, "del
nomemodulo.larisposta" rimuoverà l'attributo "larisposta" dall'oggetto
denominato da "nomemodulo".

Gli spazi dei nomi sono creati in momenti diversi e hanno durate
diverse. Lo spazio dei nomi che contiene i nomi integrati è creato
quando l'interprete Python si avvia e non viene mai eliminato. Lo
spazio dei nomi globale per un modulo è creato quando la definizione
del modulo viene letta; normalmente, gli spazi dei nomi dei moduli
durano anche fino a quando l'interprete non si arresta. Le istruzioni
eseguite dall'invocazione di livello superiore dell'interprete, lette
da un file di script o interattivamente, sono considerate parte di un
modulo chiamato "__main__", quindi hanno il loro proprio spazio dei
nomi globale. (I nomi integrati in realtà vivono anche in un modulo;
questo si chiama "builtins".)

Lo spazio dei nomi locale per una funzione è creato quando la funzione
viene chiamata e eliminato quando la funzione restituisce o genera
un'eccezione che non viene gestita all'interno della funzione. (In
realtà, dimenticare sarebbe un modo migliore per descrivere ciò che
accade effettivamente.) Naturalmente, le invocazioni ricorsive hanno
ciascuna il proprio spazio dei nomi locale.

Uno *scope* è una regione testuale di un programma Python in cui uno
spazio dei nomi è direttamente accessibile. "Direttamente accessibile"
qui significa che un riferimento non qualificato a un nome tenta di
trovare il nome nello spazio dei nomi.

Anche se gli scope sono determinati staticamente, vengono utilizzati
dinamicamente. In qualsiasi momento durante l'esecuzione, ci sono 3 o
4 scope annidati i cui spazi dei nomi sono direttamente accessibili:

* lo scope più interno, che viene cercato per primo, contiene i nomi
  locali

* gli scope di eventuali funzioni contenenti, che vengono cercati a
  partire dallo scope contenente più vicino, contengono nomi non
  locali, ma anche non globali

* il penultimo scope contiene i nomi globali del modulo corrente

* lo scope più esterno (cercato per ultimo) è lo spazio dei nomi che
  contiene i nomi integrati

Se un nome è dichiarato globale, allora tutti i riferimenti e le
assegnazioni vanno direttamente allo scope penultimo contenente i nomi
globali del modulo. Per rilegare le variabili trovate al di fuori
dello scope più interno, può essere utilizzata l'istruzione
"nonlocal"; se non dichiarate nonlocal, quelle variabili sono di sola
lettura (un tentativo di scrivere su una variabile del genere creerà
semplicemente una *nuova* variabile locale nello scope più interno,
lasciando invariata la variabile esterna con lo stesso nome).

Di solito, lo scope locale fa riferimento ai nomi locali della
funzione (testualmente) corrente. Al di fuori delle funzioni, lo scope
locale fa riferimento allo stesso spazio dei nomi dello scope globale:
lo spazio dei nomi del modulo. Le definizioni di classe pongono un
altro spazio dei nomi nello scope locale.

È importante rendersi conto che gli scope sono determinati
testualmente: lo scope globale di una funzione definita in un modulo è
lo spazio dei nomi del modulo, non importa da dove o con quale alias
la funzione viene chiamata. D'altra parte, la ricerca effettiva dei
nomi viene fatta dinamicamente, a tempo di esecuzione --- tuttavia, la
definizione del linguaggio sta evolvendo verso la risoluzione statica
dei nomi, a tempo di "compilazione", quindi non fare affidamento sulla
risoluzione dinamica dei nomi! (Infatti, le variabili locali sono già
determinate staticamente.)

Una particolarità speciale di Python è che -- se non è in vigore
alcuna istruzione "global" o "nonlocal" -- le assegnazioni ai nomi
vanno sempre nello scope più interno. Le assegnazioni non copiano i
dati --- vincolano solo i nomi agli oggetti. Lo stesso vale per le
eliminazioni: l'istruzione "del x" rimuove il vincolo di "x" dallo
spazio dei nomi riferito dallo scope locale. In realtà, tutte le
operazioni che introducono nuovi nomi utilizzano lo scope locale: in
particolare, le istruzioni "import" e le definizioni di funzioni
vincolano il nome del modulo o della funzione nello scope locale.

L'istruzione "global" può essere utilizzata per indicare che
particolari variabili vivono nello scope globale e dovrebbero essere
rilegate lì; l'istruzione "nonlocal" indica che particolari variabili
vivono in uno scope circoscritto e dovrebbero essere rilegate lì.


9.2.1. Esempio di visibilità e spazi dei nomi
---------------------------------------------

Questo è un esempio che dimostra come fare riferimento ai diversi
scope e spazi dei nomi e come "global" e "nonlocal" influenzano il
vincolo delle variabili:

   def scope_test():
       def do_local():
           spam = "local spam"

       def do_nonlocal():
           nonlocal spam
           spam = "nonlocal spam"

       def do_global():
           global spam
           spam = "global spam"

       spam = "test spam"
       do_local()
       print("After local assignment:", spam)
       do_nonlocal()
       print("After nonlocal assignment:", spam)
       do_global()
       print("After global assignment:", spam)

   scope_test()
   print("In global scope:", spam)

L'output del codice di esempio è:

   After local assignment: test spam
   After nonlocal assignment: nonlocal spam
   After global assignment: nonlocal spam
   In global scope: global spam

Nota come l'assegnazione *locale* (che è predefinita) non abbia
cambiato il vincolo di *spam* di *scope_test*. L'assegnazione
"nonlocal" ha cambiato il vincolo di *spam* di *scope_test* e
l'assegnazione "global" ha cambiato il vincolo a livello di modulo.

Si può anche vedere che non c'era alcun vincolo precedente per *spam*
prima dell'assegnazione "global".


9.3. Una prima occhiata alle classi
===================================

Le classi introducono un po' di nuova sintassi, tre nuovi tipi di
oggetti e alcune nuove semantiche.


9.3.1. Sintassi della definizione di classe
-------------------------------------------

La forma più semplice di definizione di classe appare così:

   class ClassName:
       <statement-1>
       .
       .
       .
       <statement-N>

Le definizioni di classe, come le definizioni di funzione (istruzioni
"def"), devono essere eseguite prima di avere effetto. (Potresti
concepibilmente collocare una definizione di classe in un ramo di
un'istruzione "if" o all'interno di una funzione.)

In pratica, le istruzioni all'interno di una definizione di classe
saranno solitamente definizioni di funzioni, ma sono ammesse e
talvolta utili --- torneremo su questo più tardi. Le definizioni di
funzioni all'interno di una classe hanno normalmente una forma
peculiare di elenco degli argomenti, dettata dalle convenzioni di
chiamata per i metodi --- anche questo è spiegato più tardi.

Quando una definizione di classe viene inserita, viene creato un nuovo
spazio dei nomi e utilizzato come scope locale --- quindi, tutte le
assegnazioni alle variabili locali vanno in questo nuovo spazio dei
nomi. In particolare, qui le definizioni di funzioni vincolano il nome
della nuova funzione.

Quando una definizione di classe viene lasciata normalmente (tramite
la fine), viene creato un *oggetto classe*. Questo è essenzialmente un
wrapper attorno ai contenuti dello spazio dei nomi creato dalla
definizione di classe; impareremo di più sugli oggetti classe nella
sezione successiva. Lo scope locale originale (quello in vigore appena
prima che la definizione di classe fosse inserita) viene ripristinato
e l'oggetto classe è vincolato qui al nome della classe dato
dall'intestazione della definizione di classe ("NomeClasse"
nell'esempio).


9.3.2. Oggetti della Classe
---------------------------

Gli oggetti della classe supportano due tipi di operazioni:
riferimenti agli attributi e istanziazione.

I *riferimenti agli attributi* utilizzano la sintassi standard
utilizzata per tutti i riferimenti agli attributi in Python:
"oggetto.nome". I nomi degli attributi validi sono tutti i nomi che
erano nello spazio dei nomi della classe quando l'oggetto classe è
stato creato. Quindi, se la definizione della classe apparisse così:

   class MyClass:
       """A simple example class"""
       i = 12345

       def f(self):
           return 'hello world'

allora "MyClass.i" e "MyClass.f" sono riferimenti agli attributi
validi, restituendo rispettivamente un intero e un oggetto funzione.
Gli attributi di classe possono anche essere assegnati, quindi puoi
cambiare il valore di "MyClass.i" per assegnazione. "__doc__" è anche
un attributo valido, restituendo la stringa di documentazione
appartenente alla classe: ""Una classe di esempio semplice"".

L'*istanziamento* della classe utilizza la notazione funzionale. Basta
immaginare che l'oggetto classe sia una funzione senza parametri che
restituisce una nuova istanza della classe. Per esempio (assumendo la
classe sopra menzionata):

   x = MyClass()

crea una nuova *istanza* della classe e assegna questo oggetto alla
variabile locale "x".

L'operazione di istanziazione ("chiamare" un oggetto classe) crea un
oggetto vuoto. Molte classi preferiscono creare oggetti con istanze
personalizzate per uno stato iniziale specifico. Pertanto, una classe
può definire un metodo speciale chiamato "__init__()", come questo:

   def __init__(self):
       self.data = []

Quando una classe definisce un metodo "__init__()", l'istanziamento
della classe invoca automaticamente "__init__()" per la nuova istanza
della classe creata. Quindi, in questo esempio, una nuova istanza
inizializzata può essere ottenuta con:

   x = MyClass()

Ovviamente, il metodo "__init__()" può avere argomenti per una
maggiore flessibilità. In tal caso, gli argomenti forniti
all'operatore di istanziazione della classe vengono passati a
"__init__()". Per esempio,

   >>> class Complex:
   ...     def __init__(self, realpart, imagpart):
   ...         self.r = realpart
   ...         self.i = imagpart
   ...
   >>> x = Complex(3.0, -4.5)
   >>> x.r, x.i
   (3.0, -4.5)


9.3.3. Oggetti Istanza
----------------------

Ora, cosa possiamo fare con gli oggetti istanza? Le uniche operazioni
comprese dagli oggetti istanza sono i riferimenti agli attributi. Ci
sono due tipi di nomi di attributi validi: attributi dati e metodi.

Gli *attributi dati* corrispondono alle "variabili di istanza" in
Smalltalk, e ai "membri dati" in C++. Gli attributi dati non devono
essere dichiarati; come le variabili locali, nascono nel momento in
cui vengono assegnati per la prima volta. Per esempio, se "x" è
l'istanza di "MyClass" creata sopra, il seguente pezzo di codice
stamperà il valore "16", senza lasciare traccia:

   x.counter = 1
   while x.counter < 10:
       x.counter = x.counter * 2
   print(x.counter)
   del x.counter

The other kind of instance attribute reference is a *method*. A method
is a function that "belongs to" an object.

I nomi dei metodi validi di un oggetto istanza dipendono dalla sua
classe. Per definizione, tutti gli attributi di una classe che sono
oggetti funzione definiscono i metodi corrispondenti delle sue
istanze. Quindi nel nostro esempio, "x.f" è un riferimento a un metodo
valido, poiché "MyClass.f" è una funzione, ma "x.i" no, poiché
"MyClass.i" non lo è. Tuttavia, "x.f" non è la stessa cosa di
"MyClass.f" --- è un *oggetto metodo*, non un oggetto funzione.


9.3.4. Oggetti Metodo
---------------------

Di solito, un metodo viene chiamato subito dopo essere stato
associato:

   x.f()

Nell'esempio "MyClass", questo restituirà la stringa "'hello world'".
Tuttavia, non è necessario chiamare un metodo subito: "x.f" è un
oggetto metodo e può essere memorizzato e chiamato in un secondo
momento. Per esempio:

   xf = x.f
   while True:
       print(xf())

continuerà a stampare "hello world" fino alla fine dei tempi.

Cosa succede esattamente quando un metodo viene chiamato? Potreste
aver notato che "x.f()" è stato chiamato senza argomento sopra, anche
se la definizione della funzione per "f()" specifica un argomento. Che
fine ha fatto l'argomento? Sicuramente Python solleva un'eccezione
quando una funzione che richiede un argomento viene chiamata senza
alcuno --- anche se l'argomento non è effettivamente usato...

In realtà, potreste aver indovinato la risposta: la caratteristica
speciale dei metodi è che l'istanza dell'oggetto viene passata come
primo argomento della funzione. Nel nostro esempio, la chiamata
"x.f()" è esattamente equivalente a "MyClass.f(x)". In generale,
chiamare un metodo con una lista di *n* argomenti è equivalente a
chiamare la funzione corrispondente con una lista di argomenti creata
inserendo l'istanza del metodo prima del primo argomento.

In generale, i metodi funzionano come segue. Quando viene referenziato
un attributo non dato di un'istanza, viene cercata la classe
dell'istanza. Se il nome denota un attributo di classe valido che è un
oggetto funzione, i riferimenti all'istanza dell'oggetto e all'oggetto
funzione vengono impacchettati in un oggetto metodo. Quando l'oggetto
metodo viene chiamato con una lista di argomenti, viene costruita una
nuova lista di argomenti dall'istanza dell'oggetto e la lista degli
argomenti, e l'oggetto funzione viene chiamato con questa nuova lista
di argomenti.


9.3.5. Variabili di Classe e Istanza
------------------------------------

Generalmente parlando, le variabili di istanza sono per i dati unici a
ciascuna istanza e le variabili di classe sono per gli attributi e i
metodi condivisi da tutte le istanze della classe:

   class Dog:

       kind = 'canine'         # class variable shared by all instances

       def __init__(self, name):
           self.name = name    # instance variable unique to each instance

   >>> d = Dog('Fido')
   >>> e = Dog('Buddy')
   >>> d.kind                  # shared by all dogs
   'canine'
   >>> e.kind                  # shared by all dogs
   'canine'
   >>> d.name                  # unique to d
   'Fido'
   >>> e.name                  # unique to e
   'Buddy'

Come discusso in Una parola su nomi e oggetti, i dati condivisi
possono avere effetti potenzialmente sorprendenti coinvolgendo oggetti
*mutable* come liste e dizionari. Per esempio, la lista *tricks* nel
seguente codice non dovrebbe essere usata come variabile di classe
perché solo una singola lista verrebbe condivisa da tutte le istanze
di *Dog*:

   class Dog:

       tricks = []             # mistaken use of a class variable

       def __init__(self, name):
           self.name = name

       def add_trick(self, trick):
           self.tricks.append(trick)

   >>> d = Dog('Fido')
   >>> e = Dog('Buddy')
   >>> d.add_trick('roll over')
   >>> e.add_trick('play dead')
   >>> d.tricks                # unexpectedly shared by all dogs
   ['roll over', 'play dead']

Il design corretto della classe dovrebbe usare una variabile di
istanza invece:

   class Dog:

       def __init__(self, name):
           self.name = name
           self.tricks = []    # creates a new empty list for each dog

       def add_trick(self, trick):
           self.tricks.append(trick)

   >>> d = Dog('Fido')
   >>> e = Dog('Buddy')
   >>> d.add_trick('roll over')
   >>> e.add_trick('play dead')
   >>> d.tricks
   ['roll over']
   >>> e.tricks
   ['play dead']


9.4. Osservazioni Varie
=======================

Se lo stesso nome di attributo appare sia in un'istanza che in una
classe, la ricerca degli attributi dà priorità all'istanza:

   >>> class Warehouse:
   ...    purpose = 'storage'
   ...    region = 'west'
   ...
   >>> w1 = Warehouse()
   >>> print(w1.purpose, w1.region)
   storage west
   >>> w2 = Warehouse()
   >>> w2.region = 'east'
   >>> print(w2.purpose, w2.region)
   storage east

Gli attributi dati possono essere referenziati dai metodi così come
dagli utenti ordinari ("clienti") di un oggetto. In altre parole, le
classi non sono utilizzabili per implementare tipi di dati astratti
puri. Infatti, nulla in Python permette di imporre l'incapsulamento
dei dati --- è tutto basato su convenzioni. (D'altra parte,
l'implementazione di Python, scritta in C, può nascondere
completamente i dettagli di implementazione e controllare l'accesso a
un oggetto se necessario; questo può essere utilizzato dalle
estensioni a Python scritte in C.)

I clienti dovrebbero usare gli attributi dati con cautela --- i
clienti potrebbero infrangere gli invarianti mantenuti dai metodi
alterando i loro attributi dati. Si noti che i clienti possono
aggiungere attributi dati propri a un oggetto istanza senza
influenzare la validità dei metodi, purché vengano evitati conflitti
di nomi --- ancora una volta, una convenzione di nomenclatura può
risparmiare molti mal di testa.

Non c'è alcuna scorciatoia per riferirsi agli attributi dati (o ad
altri metodi!) dall'interno dei metodi. Trovo che questo in realtà
aumenti la leggibilità dei metodi: non c'è possibilità di confondere
variabili locali e variabili di istanza quando si scorre un metodo.

Spesso, il primo argomento di un metodo è chiamato "self". Questa è
niente più che una convenzione: il nome "self" non ha assolutamente
alcun significato speciale per Python. Tieni presente, tuttavia, che
non seguire la convenzione potrebbe rendere il tuo codice meno
leggibile per altri programmatori Python, e non è nemmeno impossibile
che venga scritto un programma *class browser* che si basa su tale
convenzione.

Qualsiasi oggetto funzione che è un attributo di classe definisce un
metodo per le istanze di quella classe. Non è necessario che la
definizione della funzione sia testualmente inclusa nella definizione
della classe: è anche possibile assegnare un oggetto funzione a una
variabile locale nella classe. Per esempio:

   # Function defined outside the class
   def f1(self, x, y):
       return min(x, x+y)

   class C:
       f = f1

       def g(self):
           return 'hello world'

       h = g

Ora "f", "g" e "h" sono tutti attributi della classe "C" che si
riferiscono a oggetti funzione, e di conseguenza sono tutti metodi
delle istanze della classe "C" --- "h" essendo esattamente equivalente
a "g". Si noti che questa pratica di solito solo serve a confondere il
lettore di un programma.

I metodi possono chiamare altri metodi utilizzando gli attributi di
metodo dell'argomento "self":

   class Bag:
       def __init__(self):
           self.data = []

       def add(self, x):
           self.data.append(x)

       def addtwice(self, x):
           self.add(x)
           self.add(x)

I metodi possono referenziare nomi globali allo stesso modo delle
funzioni ordinarie. Lo scope globale associato a un metodo è il modulo
contenente la sua definizione. (Una classe non è mai utilizzata come
uno scope globale.) Sebbene raramente si incontri una buona ragione
per usare dati globali in un metodo, ci sono molti usi legittimi dello
scope globale: ad esempio, le funzioni e i moduli importati nello
scope globale possono essere utilizzati dai metodi, così come le
funzioni e le classi definite in esso. Di solito, la classe contenente
il metodo è definita anch'essa in tale scope globale, e nella sezione
successiva troveremo alcune buone ragioni per cui un metodo potrebbe
voler referenziare la propria classe.

Ogni valore è un oggetto e quindi ha una *classe* (chiamata anche il
suo *tipo*). È memorizzato come "object.__class__".


9.5. Ereditarietà
=================

Naturalmente, una caratteristica del linguaggio non sarebbe degna del
nome "classe" senza supportare l'ereditarietà. La sintassi per la
definizione di una classe derivata è simile:

   class DerivedClassName(BaseClassName):
       <statement-1>
       .
       .
       .
       <statement-N>

Il nome "BaseClassName" deve essere definito in uno spazio dei nomi
accessibile dallo scope contenente la definizione della classe
derivata. Al posto di un nome di classe base, sono permesse anche
altre espressioni arbitrarie. Questo può essere utile, per esempio,
quando la classe base è definita in un altro modulo:

   class DerivedClassName(modname.BaseClassName):

L'esecuzione di una definizione di classe derivata procede nello
stesso modo di una classe base. Quando viene costruito l'oggetto
classe, viene ricordata la classe base. Questo viene utilizzato per
risolvere i riferimenti agli attributi: se un attributo richiesto non
è trovato nella classe, la ricerca procede a cercarlo nella classe
base. Questa regola viene applicata ricorsivamente se la classe base
stessa è derivata da un'altra classe.

Non c'è nulla di speciale nell'istanza di classi derivate:
"DerivedClassName()" crea una nuova istanza della classe. I
riferimenti ai metodi vengono risolti come segue: viene cercato
l'attributo di classe corrispondente, scendendo lungo la catena delle
classi base se necessario, e il riferimento al metodo è valido se
questo produce un oggetto funzione.

Le classi derivate possono ignorare i metodi delle loro classi base.
Poiché i metodi non hanno privilegi speciali quando chiamano altri
metodi dello stesso oggetto, un metodo di una classe base che chiama
un altro metodo definito nella stessa classe base può finire per
chiamare un metodo di una classe derivata che lo sostituisce. (Per
programmatori C++: tutti i metodi in Python sono effettivamente
"virtual".)

Un metodo sovrascritto in una classe derivata può in realtà voler
estendere piuttosto che semplicemente sostituire il metodo della
classe base con lo stesso nome. C'è un modo semplice per chiamare
direttamente il metodo della classe base: basta chiamare
"BaseClassName.methodname(self, arguments)". Questo è occasionalmente
utile anche per i clienti. (Si noti che questo funziona solo se la
classe base è accessibile come "BaseClassName" nello scope globale.)

Python ha due funzioni built-in che funzionano con l'ereditarietà:

* Usa "isinstance()" per controllare il tipo di un'istanza:
  "isinstance(obj, int)" sarà "True" solo se "obj.__class__" è "int" o
  una classe derivata da "int".

* Usa "issubclass()" per controllare l'ereditarietà delle classi:
  "issubclass(bool, int)" è "True" poiché "bool" è una sottoclasse di
  "int". Tuttavia, "issubclass(float, int)" è "False" poiché "float"
  non è una sottoclasse di "int".


9.5.1. Ereditarietà Multipla
----------------------------

Python supporta anche una forma di ereditarietà multipla. Una
definizione di classe con più classi base è simile:

   class DerivedClassName(Base1, Base2, Base3):
       <statement-1>
       .
       .
       .
       <statement-N>

Per la maggior parte degli scopi, nei casi più semplici, si può
pensare alla ricerca di attributi ereditati da una classe padre come
alla profondità prima, da sinistra a destra, senza cercare due volte
nella stessa classe dove c'è una sovrapposizione nella gerarchia.
Pertanto, se un attributo non viene trovato in "DerivedClassName",
viene cercato in "Base1", quindi (ricorsivamente) nelle classi base di
"Base1", e se non è stato trovato lì, viene cercato in "Base2", e così
via.

Infatti, è leggermente più complesso di così; l'ordine di risoluzione
del metodo cambia dinamicamente per supportare le chiamate cooperative
a "super()". Questo approccio è noto in altri linguaggi con
ereditarietà multipla come call-next-method ed è più potente della
chiamata super trovata nei linguaggi a ereditarietà singola.

L'ordinamento dinamico è necessario perché tutti i casi di
ereditarietà multipla mostrano una o più relazioni di diamante (dove
almeno una delle classi padre può essere accessibile attraverso
percorsi multipli dalla classe più bassa). Per esempio, tutte le
classi ereditano da "object", quindi qualsiasi caso di ereditarietà
multipla fornisce più di un percorso per raggiungere "object". Per
evitare che le classi base vengano accessibili più di una volta,
l'algoritmo dinamico linearizza l'ordine di ricerca in modo che
preserva l'ordunquezione da sinistra a destra specificata in ogni
classe, che chiama ogni genitore solo una volta, e che è monotònico
(significando che una classe può essere sottoclasse senza influenzare
l'ordine di precedenza dei suoi genitori). Insieme, queste proprietà
rendono possibile progettare classi affidabili ed estensibili con
ereditarietà multipla. Per maggiori dettagli, vedi The Python 2.3
Method Resolution Order.


9.6. Variabili Private
======================

Non esistono variabili di istanza "private" che non possono essere
accessibili se non all'interno di un oggetto in Python. Tuttavia, c'è
una convenzione seguita dalla maggior parte del codice Python: un nome
prefisso con un trattino basso (es. "_spam") dovrebbe essere trattato
come una parte non pubblica dell'API (sia che sia una funzione, un
metodo o un membro dato). Dovrebbe essere considerato un dettaglio di
implementazione e soggetto a cambiamento senza preavviso.

Poiché esiste un caso d'uso valido per i membri privati della classe
(ovvero per evitare conflitti di nomi con nomi definiti dalle
sottoclassi), c'è un supporto limitato per tale meccanismo, chiamato
*offuscamento dei nomi*. Qualsiasi identificatore della forma "__spam"
(almeno due trattini bassi iniziali, al massimo uno finale) è
testualmente sostituito con "_classname__spam", dove "classname" è il
nome attuale della classe con i trattini bassi iniziali rimossi.
Questo offuscamento viene eseguito senza riguardo alla posizione
sintattica dell'identificatore, purché si trovi all'interno della
definizione di una classe.

Vedi anche:

  The private name mangling specifications for details and special
  cases.

L'offuscamento dei nomi è utile per permettere alle sottoclassi di
sovrascrivere i metodi senza rompere le chiamate ai metodi interni
alla classe. Per esempio:

   class Mapping:
       def __init__(self, iterable):
           self.items_list = []
           self.__update(iterable)

       def update(self, iterable):
           for item in iterable:
               self.items_list.append(item)

       __update = update   # private copy of original update() method

   class MappingSubclass(Mapping):

       def update(self, keys, values):
           # provides new signature for update()
           # but does not break __init__()
           for item in zip(keys, values):
               self.items_list.append(item)

L'esempio sopra funzionerebbe anche se "MappingSubclass" dovesse
introdurre un identificatore "__update" poiché viene sostituito con
"_Mapping__update" nella classe "Mapping" e "_MappingSubclass__update"
nella classe "MappingSubclass" rispettivamente.

Si noti che le regole di offuscamento sono progettate soprattutto per
evitare incidenti; è ancora possibile accedere o modificare una
variabile considerata privata. Questo può essere utile in circostanze
speciali, come nel debugger.

Si noti che il codice passato a "exec()" o "eval()" non considera la
classe nome dell'invocante per essere la classe attuale; questo è
simile all'effetto della dichiarazione "global", l'effetto del quale è
analogamente limitato al codice che è compilato insieme. La stessa
restrizione si applica a "getattr()", "setattr()" e "delattr()", così
come quando si fa riferimento direttamente a "__dict__".


9.7. Varie ed Eventuali
=======================

A volte è utile avere un tipo di dato simile al "record" del Pascal o
al "struct" del C, raggruppando insieme alcuni elementi dati con nome.
L'approccio idiomatico è utilizzare "dataclasses" per questo scopo:

   from dataclasses import dataclass

   @dataclass
   class Employee:
       name: str
       dept: str
       salary: int

   >>> john = Employee('john', 'computer lab', 1000)
   >>> john.dept
   'computer lab'
   >>> john.salary
   1000

Un pezzo di codice Python che si aspetta un particolare tipo di dato
astratto può spesso essere passato a una classe che emula i metodi di
quel tipo di dato. Per esempio, se hai una funzione che formatta
alcuni dati da un oggetto file, puoi definire una classe con i metodi
"read()" e "readline()" che ottengono i dati da un buffer di stringhe,
e passarla come argomento.

Gli oggetti metodo d'istanza hanno anch'essi attributi: "m.__self__" è
l'oggetto istanza con il metodo "m()", e "m.__func__" è l'oggetto
funzione corrispondente al metodo.


9.8. Iteratori
==============

Ormai avrai probabilmente notato che la maggior parte degli oggetti
contenitori può essere iterata utilizzando un'istruzione "for":

   for element in [1, 2, 3]:
       print(element)
   for element in (1, 2, 3):
       print(element)
   for key in {'one':1, 'two':2}:
       print(key)
   for char in "123":
       print(char)
   for line in open("myfile.txt"):
       print(line, end='')

Questo stile di accesso è chiaro, conciso e conveniente. L'uso degli
iteratori permea e unifica Python. Dietro le quinte, l'istruzione
"for" chiama la funzione "iter()" sull'oggetto contenitore. La
funzione restituisce un oggetto iteratore che definisce il metodo
"__next__()" che accede agli elementi del contenitore uno alla volta.
Quando non ci sono più elementi, "__next__()" solleva un'eccezione
"StopIteration" che indica al ciclo "for" di terminare. Puoi chiamare
il metodo "__next__()" utilizzando la funzione built-in "next()";
questo esempio mostra come funziona tutto:

   >>> s = 'abc'
   >>> it = iter(s)
   >>> it
   <str_iterator object at 0x10c90e650>
   >>> next(it)
   'a'
   >>> next(it)
   'b'
   >>> next(it)
   'c'
   >>> next(it)
   Traceback (most recent call last):
     File "<stdin>", line 1, in <module>
       next(it)
   StopIteration

Avendo visto la meccanica dietro il protocollo dell'iteratore, è
facile aggiungere il comportamento di iteratore alle tue classi.
Definisci un metodo "__iter__()" che restituisce un oggetto con un
metodo "__next__()". Se la classe definisce "__next__()", allora
"__iter__()" può semplicemente restituire "self":

   class Reverse:
       """Iterator for looping over a sequence backwards."""
       def __init__(self, data):
           self.data = data
           self.index = len(data)

       def __iter__(self):
           return self

       def __next__(self):
           if self.index == 0:
               raise StopIteration
           self.index = self.index - 1
           return self.data[self.index]

   >>> rev = Reverse('spam')
   >>> iter(rev)
   <__main__.Reverse object at 0x00A1DB50>
   >>> for char in rev:
   ...     print(char)
   ...
   m
   a
   p
   s


9.9. Generatori
===============

I *generatori* sono uno strumento semplice e potente per creare
iteratori. Sono scritti come funzioni regolari ma utilizzano
l'istruzione "yield" ogni volta che vogliono restituire dati. Ogni
volta che si chiama "next()" su di esso, il generatore riprende da
dove si era interrotto (ricorda tutti i valori dei dati e quale
istruzione è stata eseguita per ultima). Un esempio mostra che i
generatori possono essere creati con estrema facilità:

   def reverse(data):
       for index in range(len(data)-1, -1, -1):
           yield data[index]

   >>> for char in reverse('golf'):
   ...     print(char)
   ...
   f
   l
   o
   g

Tutto ciò che può essere fatto con i generatori può essere fatto anche
con gli iteratori basati su classi come descritto nella sezione
precedente. Ciò che rende i generatori così compatti è che i metodi
"__iter__()" e "__next__()" sono creati automaticamente.

Un'altra caratteristica chiave è che le variabili locali e lo stato di
esecuzione sono salvati automaticamente tra le chiamate. Questo rende
la funzione più facile da scrivere e molto più chiara rispetto a un
approccio che utilizza variabili di istanza come "self.index" e
"self.data".

Oltre alla creazione automatica dei metodi e al salvataggio dello
stato del programma, quando i generatori terminano, sollevano
automaticamente "StopIteration". In combinazione, queste
caratteristiche rendono facile creare iteratori con lo stesso sforzo
di scrivere una funzione regolare.


9.10. Espressioni di Generatore
===============================

Alcuni generatori semplici possono essere codificati in modo conciso
come espressioni utilizzando una sintassi simile alle comprensioni di
lista ma con parentesi tonde anziché quadre. Queste espressioni sono
progettate per situazioni in cui il generatore viene utilizzato
immediatamente da una funzione racchiudente. Le espressioni di
generatore sono più compatte ma meno versatili rispetto alle
definizioni complete di generatore e tendono a essere più efficienti
in termini di memoria rispetto alle comprensioni di lista equivalenti.

Esempi:

   >>> sum(i*i for i in range(10))                 # sum of squares
   285

   >>> xvec = [10, 20, 30]
   >>> yvec = [7, 5, 3]
   >>> sum(x*y for x,y in zip(xvec, yvec))         # dot product
   260

   >>> unique_words = set(word for line in page  for word in line.split())

   >>> valedictorian = max((student.gpa, student.name) for student in graduates)

   >>> data = 'golf'
   >>> list(data[i] for i in range(len(data)-1, -1, -1))
   ['f', 'l', 'o', 'g']

-[ Note a piè di pagina ]-

[1] Tranne per una cosa. Gli oggetti modulo hanno un attributo segreto
    di sola lettura chiamato "__dict__" che restituisce il dizionario
    usato per implementare lo spazio dei nomi del modulo; il nome
    "__dict__" è un attributo ma non un nome globale. Ovviamente,
    l'uso di questo violerebbe l'astrazione dell'implementazione dello
    spazio dei nomi e dovrebbe essere limitato a cose come debugger
    post-mortem.
