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

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