9. Classi

Le classi servono a unire insieme dati e funzionalità. Creare una classe equivale a creare un nuovo tipo, a partire dal quale possono essere create nuove istanze (oggetti). Ciascuna istanza può avere degli attributi suoi propri, che ne mantengono lo stato. Le istanze possono anche avere dei metodi (definiti nella classe) che ne modificano lo stato.

A differenza di altri linguaggi di programmazione, il meccanismo delle classi in Python aggiunge pochissima sintassi e semantica: è un misto delle classi che si possono trovare in C++ e in Modula-3. Le classi Python forniscono tutti gli strumenti standard della programmazione a oggetti (OOP): il meccanismo dell’ereditarietà consente di avere più di una classe-base, una sotto-classe può sovrascrivere i metodi della sua classe-madre, e un metodo può invocare il metodo della classe-madre con lo stesso nome. Gli oggetti possono contenere quanti dati si desidera. Proprio come i moduli, le classi partecipano della natura dinamica di Python: sono create a runtime e possono essere modificate ulteriormente dopo la loro creazione.

Usando la terminologia di C++, di solito i membri di una classe (inclusi i dati) sono pubblici (a eccezione di quanto spiegato in Variabili private) e tutte le funzioni interne sono virtuali. Come in Modula-3, non ci sono scorciatoie per referenziare i membri di un oggetto dall’interno dei suoi metodi: un metodo è chiamato con un primo argomento esplicito che rappresenta l’oggetto stesso, che è fornito implicitamente dalla chiamata. Le classi sono oggetti esse stesse, come in Smalltalk: questo permette la semantica per l’importazione e la rinomina. A differenza di C++ e Modula-3, i tipi predefiniti possono essere usati come classi-base che l’utente può estendere. Inoltre, come in C++, la maggior parte degli operatori predefiniti che godono di sintassi speciale (gli operatori aritmetici, l’indicizzazione, etc.) possono essere ridefiniti per le istanze delle classi.

(Dal momento che non esiste una terminologia universale per le classi, utilizziamo occasionalmente il lessico di C++ e Smalltalk. Sarebbe preferibile usare il lessico di Modula-3 che, rispetto a C++, è più simile a Python per quel che riguarda gli oggetti: ma sospettiamo che pochi lettori abbiano familiarità con questo linguaggio.)

9.1. A proposito di nomi e oggetti

Gli oggetti hanno una identità univoca e più di un nome (in più di uno scope) può essere collegato allo stesso oggetto: è ciò che in altri linguaggi si chiama aliasing. Questa caratteristica, a prima vista, non è sempre apprezzata in Python e può tranquillamente essere ignorata quando si gestiscono oggetti di tipo immutabile (numeri, stringhe, tuple). Tuttavia, lo aliasing può avere effetti imprevisti sulla semantica del codice Python che maneggia oggetti mutabili come liste, dizionari e la maggior parte degli altri tipi. Di solito questi effetti sono sfruttati positivamente dal programma, dal momento che lo aliasing è in qualche modo simile ai puntatori. Per esempio, passare un oggetto è più economico, dal momento che l’implementazione sottostante deve solo passare un puntatore; e se una funzione modifica un oggetto passato come argomento, il codice chiamante vedrà le modifiche: questo elimina la necessità di un doppio meccanismo di passaggio degli argomenti, come avviene in Pascal.

9.2. Scope e namespace in Python

Prima di affrontare le classi, dobbiamo parlare delle regole degli scope in Python. [1] Le classi manipolano i namespace in modo sottile ed è necessario sapere come funzionano scope e namespace per capire che cosa succede davvero. D’altra parte, la comprensione di questi argomenti è utile in qualsiasi applicazione avanzata di Python.

Iniziamo con alcune definizioni.

Un namespace è una mappatura che collega nomi a oggetti. Di solito i namespace sono attualmente implementati come dizionari (per ragioni di performance), ma dall’esterno questo non si vede e potrebbe cambiare in futuro. Esempi di namespace sono l’insieme dei nomi predefiniti (che comprende funzioni come abs() e i nomi delle eccezioni predefinite); i nomi globali di un modulo; i nomi locali a una chiamata di funzione. In un certo senso, l’insieme degli attributi di un oggetto è anch’esso un namespace. La cosa fondamentale da capire sui namespace è che non c’è assolutamente nessuna relazione tra i nomi di diversi namespace. Per esempio, due moduli diversi potrebbero definire entrambi una funzione maximize senza nessuna confusione: gli utenti dei due moduli devono riferirsi a questa funzione premettendo il nome del modulo.

A proposito: usiamo il termine attributo per qualsiasi nome che segue un punto: per esempio, nell’espressione z.real, real è un attributo dell’oggetto z. Tecnicamente, un riferimento a un nome in un modulo è un riferimento a un attributo di quel modulo: nell’espressione modname.funcname, modname è un oggetto-modulo e funcname è un suo attributo. In questo caso c’è una corrispondenza ovvia tra gli attributi del modulo e i nomi globali definiti nel modulo: condividono lo stesso namespace! [2]

Gli attributi possono essere di sola lettura o scrivibili: a questi ultimi è possibile assegnare dei valori. Gli attributi dei moduli sono scrivibili: potete scrivere modname.the_answer = 42. Gli attributi scrivibili sono anche eliminabili con l’istruzione del. Per esempio, del modname.the_answer rimuoverà l’attributo the_answer dall’oggetto modname.

I namespace sono creati in momenti diversi e hanno cicli di vita diversi. Il namespace che contiene i nomi predefiniti è creato dall’interprete Python all’avvio e non viene mai distrutto. Il namespace globale di un modulo è creato al momento della lettura delle definizioni del modulo; anche questi di solito durano fin quando l’interprete è in esecuzione. Le istruzioni eseguite all’invocazione dell’interprete, sia quelle che sono lette da un file script sia quelle eseguite in modalità interattiva, sono considerate parte di un modulo chiamato __main__, e quindi hanno un loro namespace globale. (I nomi predefiniti, in effetti, vivono anch’essi in un modulo: questo si chiama builtins.)

Il namespace locale di una funzione viene creato al momento di chiamare la funzione e distrutto quando la funzione restituisce il suo risultato o emette un’eccezione che non viene gestita all’interno della funzione. (In realtà, «dimenticato» è un termine più appropriato per descrivere quello che accade.) Naturalmente le invocazioni ricorsive hanno ciascuna il proprio namespace.

Uno scope è una regione di codice Python, all’interno del programma, dove il namespace è direttamente accessibile. Con «direttamente accessibile» intendiamo che un riferimento non qualificato (senza ricorrere alla notazione col punto) a un nome riesce effettivamente a raggiungere quel nome nel namespace.

Anche se gli scope sono determinati in modo statico, sono usati in modo dinamico. In qualsiasi momento durante l’esecuzione del programma esistono tre o quattro scope annidati, i cui namespace sono direttamente accessibili:

  • lo scope più interno, dove un nome è cercato per prima cosa, contiene i nomi locali;

  • gli scope di ogni eventuale funzione di ordine superiore, che sono ricercati dal più prossimo al più lontano, contengono nomi non-locali ma anche non-globali;

  • il penultimo scope più lontano contiene i nomi globali del modulo corrente;

  • lo scope più generale (dove il nome è cercato per ultimo) è il namespace che contiene i nomi predefiniti.

Se un nome è dichiarato global, allora tutti i riferimenti a questo puntano direttamente allo scope intermedio che contiene i nomi globali del moduli. Per ri-collegare variabili che si trovano fuori dallo scope più interno, potete usare l’istruzione nonlocal. Se dichiarata nonlocal, una variabile è di sola lettura: tentare di scrivere in questa variabile non farà altro che creare un nuova variabile nello scope locale, lasciando immutata la variabile esterna con il medesimo nome.

In genere lo scope locale «vede» i nomi locali al codice della funzione corrente. Al di fuori di una funzione, lo scope locale vede lo stesso namespace dello scope globale: ovvero, il namespace del modulo.

È importante capire che gli scope sono determinati «dal testo del codice». Lo scope globale di una funzione definita in un modulo è il namespace di quel modulo: non importa da dove è chiamata la funzione, o con quale alias. Ma d’altro canto, la ricerca di un nome avviene dinamicamente, a runtime. È anche vero che l’architettura del linguaggio evolve verso la risoluzione statica dei nomi, a compile time, quindi non dovreste fare affidamento sulla risoluzione dinamica dei nomi (e in effetti, le variabili locali sono già determinate in modo statico).

Una peculiarità di Python è che, in assenza di istruzioni global o nonlocal, gli assegnamenti alle variabili sono sempre indirizzati allo scope più interno. Gli assegnamenti non copiano i dati, collegano semplicemente i nomi agli oggetti. Lo stesso vale per le eliminazioni: l’istruzione del x rimuove il collegamento di x dal namespace ricercato dallo scope locale. In effetti, tutte le operazioni che introducono nomi nuovi utilizzano lo scope locale: in particolare, le istruzioni import e le definizioni di funzione collegano il nome del modulo o della funzione allo scope locale.

L’istruzione global può essere usata per indicare che una particolare variabile vive nello scope globale e dovrebbe essere ri-collegata lì; l’istruzione nonlocal indica che una particolare variabile vive nel namespace di ordine superiore e dovrebbe essere ri-collegata lì.

9.2.1. Esempi di scope e namespace

Questo esempio dimostra come riferirsi ai diversi scope e namespace e come global e nonlocal influiscono sul collegamento 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("Dopo un'assegnazione locale:", spam)
    do_nonlocal()
    print("Dopo un'assegnazione 'nonlocal':", spam)
    do_global()
    print("Dopo un'assegnazione 'global':", spam)

scope_test()
print("Nello scope globale:", spam)

L’output di questo esempio è:

Dopo un'assegnazione locale: test spam
Dopo un'assegnazione 'nonlocal': nonlocal spam
Dopo un'assegnazione 'global': nonlocal spam
Nello scope globale: global spam

Si noti che l’assegnazione locale (che è il comportamento di default) non cambia il collegamento della variabile spam della funzione scope_test. D’altra parte l’assegnamento nonlocal cambia il collegamento dello spam di test_spam, e l’assegnamento global cambia il collegamento dello spam del modulo.

Si noti inoltre che non esisteva un collegamento per la variabile spam prima dell’assegnamento global.

9.3. Introduzione alle classi

Le classi introducono qualche nuovo aspetto nella sintassi, tre tipi di oggetto nuovi e della nuova semantica.

9.3.1. Sintassi della definizione di una classe

Questa è la forma più semplice di definizione di una classe:

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

La definizione delle classi, come quella delle funzioni (l’istruzione def) deve essere eseguita prima di avere qualsiasi effetto. (Si potrebbe anche collocare la definizione in un ramo di un’istruzione if, o all’interno di una funzione.)

In pratica, le istruzioni all’interno di una definizione di classe sono in genere definizioni di funzione: ma sono permesse anche altre istruzioni, e talvolta sono anzi utili (ne riparleremo in seguito). Le definizioni di funzione all’interno della classe di solito hanno una particolare lista di parametri, dovuta alle convenzioni di chiamata per i metodi (di nuovo, ne riparleremo).

Quando il flusso di esecuzione del codice entra nella definizione della classe, un nuovo namespace viene creato e usato come scope locale: ovvero, tutte le successive assegnazioni di variabili locali finiscono in questo nuovo namespace. In particolare, le definizioni di funzione collegano qui il nome della funzione.

Quando si esce dalla definizione della classe nel modo normale (perché il flusso di esecuzione abbandona la classe), viene creato un oggetto-classe. Questo oggetto è in sostanza un «contenitore» per il contenuto del namespace creato dalla definizione della classe: diremo di più sugli oggetti-classe nella prossima sezione. Lo scope locale originario (quello che era attivo subito prima di entrare nella definizione della classe) viene ripristinato e l’oggetto-classe viene collegato in questo namespace al nome fornito nell’intestazione della definizione della classe (nel nostro esempio, ClassName).

9.3.2. Gli oggetti-classe

Gli oggetti-classe supportano due tipi di operazione: il riferimento agli attributi e l’istanziamento.

Il riferimento agli attributi utilizza la normale sintassi che si usa per queste operazioni in Python: obj.name. Un nome di attributo è valido se era nel namespace della classe al momento della creazione dell’oggetto-classe. Quindi, se una definizione di classe è fatta così,

class MyClass:
    """Un semplice esempio di classe."""
    i = 12345

    def f(self):
        return 'hello world'

allora MyClass.i e MyClass.f sono riferimenti validi agli attributi, e restituiscono un intero e un oggetto-funzione rispettivamente. Gli attributi della classe possono anche essere assegnati, ovvero potete cambiare il valore di MyClass.i con un assegnamento. Anche __doc__ è un attributo valido, e restituisce la docstring della classe ( "Un semplice esempio di classe.")

Lo istanziamento usa invece la notazione di chiamata di funzione. Fate finta che la classe sia una funzione senza parametri che restituisce una nuova istanza della classe. Per esempio, con riferimento alla classe dell’esempio precedente,

x = MyClass()

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

L’operazione di istanziamento («invocare» un oggetto-classe) crea un oggetto vuoto. Molto spesso le classi preferiscono creare istanze predisposte con uno specifico stato iniziale. Per questo è possibile definire nella classe un metodo speciale chiamato __init__(), così:

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

Se la classe definisce un metodo __init__() allora l’operazione di istanziamento lo invoca automaticamente per l’istanza appena creata. Quindi nel nostro esempio, una nuova istanza già inizializzata può essere ottenuta con:

x = MyClass()

Naturalmente il metodo __init__() può essere reso più flessibile dotandolo di parametri. In questo caso gli argomenti passati all’istanziamento della classe sono trasferiti al metodo __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

Che cosa possiamo fare con gli oggetti-istanza? L’unica operazione possibile con questi oggetti è il riferimento agli attributi. Ci sono due tipi di nomi di attributo validi: i dati («campi») e i metodi.

I dati corrispondono alle «variabili di istanza» di Smalltalk e ai «data members» di C++. Gli attributi-dati non devono essere dichiarati: proprio come le variabili locali, iniziano a esistere nel momento in cui sono assegnati per la prima volta. Per esempio, se x è una istanza della classe MyClass che abbiamo definito sopra, questo codice scriverà il valore «16» senza lasciar traccia:

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

L’altro tipo di attributo dell’istanza è il metodo. Un metodo è una funzione che «appartiene» all’oggetto-istanza. (In Python, il termine «metodo» non si usa solo in relazione alle istanze delle classi: anche altri tipi di oggetti possono avere dei metodi. Per esempio, gli oggetti-lista hanno metodi come append, insert, remove, sort e così via. In ogni caso, nel resto di questo capitolo, useremo «metodo» solo per riferirci ai metodi degli oggetti-istanza di una classe, a meno che non sia specificato diversamente.)

I nomi validi per i metodi di un’istanza dipendono dalla sua classe. Per definizione, tutti gli attributi della classe che corrispondono a degli oggetti-funzione sono metodi della sua istanza. Quindi nel nostro esempio x.f è un riferimento valido al metodo, dal momento che MyClass.f è una funzione; ma x.i non lo è, perché MyClass.i non è una funzione. Tuttavia x.f non è la stessa cosa di MyClass.f: il primo è un oggetto-metodo, il secondo è un oggetto-funzione.

9.3.4. Oggetti-metodo

Di solito un metodo viene invocato non appena è stato collegato:

x.f()

Nell’esempio di MyClass, questa chiamata restituirà la stringa 'hello world'. Tuttavia non è necessario invocare il metodo immediatamente: x.f è un oggetto-metodo che può essere «conservato» e chiamato più tardi. Per esempio,

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

continuerà a scrivere hello world fino alla fine del mondo.

Che cosa succede di preciso quando un metodo è invocato? Avrete notato che l’invocazione x.f() è stata fatta senza passare argomenti, anche se la definizione di f() specifica in effetti un parametro. Che cosa succede a questo? Certamente Python dovrebbe emettere un’eccezione se una funzione che richiede un argomento è invocata senza passarlo, anche se poi l’argomento non dovesse essere usato nella funzione stessa…

In realtà probabilmente avrete indovinato la risposta: la peculiarità dei metodi è che l’oggetto-istanza è passato automaticamente come primo argomento della funzione. Nel nostro esempio, la chiamata x.f() è esattamente equivalente a MyClass.f(x). In generale, invocare un metodo con una lista di n argomenti è equivalente a chiamare la corrispondente funzione con una lista di argomenti identica, ma che inserisce al primo posto l’oggetto-istanza.

Se non riuscite a comprendere esattamente come funziona, può essere utile dare un’occhiata all’implementazione. Quando referenziamo un attributo (che non sia un dato) di una classe, il nome viene cercato nell’istanza della classe. Se il nome corrisponde a un attributo che è un oggetto-funzione, allora un oggetto-metodo viene creato mettendo insieme (puntatori a) l’oggetto-istanza e l’oggetto-funzione appena trovato, per formare un nuovo oggetto astratto: l’oggetto-metodo, appunto. Quando l’oggetto-metodo è invocato con una lista di argomenti, una nuova lista viene creata unendola all’oggetto-istanza: l’oggetto-funzione viene chiamato con questa nuova lista di argomenti.

9.3.5. Variabili di classe e di istanza

In generale, le variabili di istanza sono per i dati che devono restare unici per ciascuna istanza; le variabili di classe sono per attributi e metodi condivisi tra tutte le istanze della classe:

class Dog:

    kind = 'canine'         # variabile di classe condivisa tra le istanze

    def __init__(self, name):
        self.name = name    # variabile di istanza unica per ciascuna istanza

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.kind                  # condivisa tra tutti i cani
'canine'
>>> e.kind                  # condivisa tra tutti i cani
'canine'
>>> d.name                  # unica per d
'Fido'
>>> e.name                  # unica per e
'Buddy'

Come abbiamo visto in A proposito di nomi e oggetti, i dati condivisi possono avere comportamenti sorprendenti quando sono oggetti mutabili come liste e dizionari. Nell’esempio che segue, la lista tricks non dovrebbe essere utilizzata come una variabile di classe, perché una singola lista verrebbe condivisa tra tutte le istanze di Dog:

class Dog:

    tricks = []             # uso sbagliato di una variabile di classe

    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                # inaspettata condivisione tra tutte le istanze
['roll over', 'play dead']

Il design corretto per la classe prevede l’uso di una variabile di istanza, invece:

class Dog:

    def __init__(self, name):
        self.name = name
        self.tricks = []    # crea una lista vuota per ciascuna istanza

    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 è usato per un attributo di classe e uno di istanza, allora il meccanismo di ricerca dà priorità all’attributo di 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 che sono dati possono essere referenziati anche dai metodi, oltre che dai normali «clienti» (utilizzatori) di un oggetto. In altre parole, le classi non sono adatte a implementare tipi di dati astratti. In effetti, non è in alcun modo possibile in Python garantire la protezione di un dato: questo può essere solo basato su convenzioni. (D’altro canto, la implementazione di Python, scritta in C, può nascondere completamente i dettagli di implementazione e controllare l’accesso a un oggetto, se necessario: si può sfruttare questo aspetto scrivendo delle estensioni di Python in C.)

I «clienti» dovrebbero fare attenzione a usare gli attributi-dati: potrebbero scompaginare delle invarianti utilizzate dai metodi, sovrascrivendole con i loro attributi-dati. Si noti che i clienti possono aggiungere dati per conto proprio a un oggetto-istanza, senza compromettere il funzionamento dei metodi, fintanto che non ci sono conflitti tra i nomi. Ancora una volta, l’uso di una convenzione per i nomi può risparmiare molti grattacapi.

Non ci sono particolari scorciatoie per referenziare dati (o metodi) dall’interno dei metodi. Riteniamo che in questo modo il codice sia più leggibile: non c’è la possibilità di confondere variabili locali e variabili di istanza quando si scorre con l’occhio il codice di un metodo.

Di solito il primo parametro di un metodo viene chiamato self. Si tratta solo di una convenzione: il nome di per sé non ha alcun significato speciale per Python. Notate tuttavia che non seguire questa convenzione rende il vostro codice meno leggibile per gli altri programmatori Python; è anche probabile che gli strumenti di introspezione del codice si basino sul rispetto di questa convenzione.

Ogni oggetto-funzione che sia un attributo di classe definisce un oggetto-metodo per l’istanza di quella classe. Non è necessario che il codice della funzione sia fisicamente contenuto all’interno della definizione della classe: si può anche assegnare una variabile locale a un oggetto-funzione esterno. Per esempio:

# Una funzione definita all'esterno della classe
def f1(self, x, y):
    return min(x, x+y)

class C:
    f = f1

    def g(self):
        return 'hello world'

    h = g

Adesso f, g e h sono tutti attributi della classe C, che si riferiscono a oggetti-funzione: di conseguenza sono anche tutti metodi delle istanze della classe; h sarà esattamente equivalente a g. Si noti però che questa pratica in genere confonde solo le idee a chi deve leggere il codice.

I metodi possono invocare altri metodi usando gli attributi-metodo del loro parametro 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 accedere ai nomi globali nello stesso modo delle funzioni ordinarie. Lo scope globale associato a un metodo è il modulo che contiene la definizione. (Una classe non è mai utilizzata come scope globale.) Anche se di rado esiste una ragione valida per accedere a un dato globale dall’interno di un metodo, ci sono comunque motivi validi per usare lo scope globale: per cominciare, le funzioni e i moduli importati nello scope globale possono essere usate dai metodi, così come le funzioni e le classi ivi definite. In genere la classe che contiene il metodo è essa stessa definita nello scope globale, e nella prossima sezione vedremo dei buoni motivi per cui un metodo potrebbe voler accedere al nome della sua stessa classe.

Ogni valore è un oggetto e di conseguenza ha una classe, che è chiamata il suo tipo. Il nome della classe/tipo è conservato in object.__class__.

9.5. Ereditarietà

Naturalmente una classe non sarebbe degna di questo nome se non supportasse l’ereditarietà. La sintassi per definire una sotto-classe è questa:

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

Il nome della classe-madre BaseClassName deve essere definito in uno scope che contiene la definizione della sotto-classe. Al posto del nome della classe-madre è anche consentito inserire un’espressione arbitraria. Questo è utile, per esempio, quando la classe-madre è definita in un altro modulo:

class DerivedClassName(modname.BaseClassName):

L’esecuzione della definizione di una sotto-classe è simile a quella della classe-madre. Al momento della sua costruzione, l’oggetto-classe ricorda la sua classe-madre. In questo modo può risolvere i riferimenti agli attributi: se viene richiesto un attributo che non si trova nella classe, la ricerca procede nella classe-madre. Il meccanismo si applica ricorsivamente, se la classe-madre a sua volta deriva da qualche altra classe.

Non vi è nulla di speciale nell’istanziare una sotto-classe: DerivedClassName() crea una nuova istanza della classe. I riferimenti ai metodi sono risolti così: si cerca il corrispondente attributo di classe, scendendo lungo la catena delle classi-madri se necessario; il riferimento al metodo è valido se il nome trovato corrisponde a un oggetto-funzione.

Le sotto-classi possono sovrascrivere i metodi delle loro classi-madri. Dal momento che i metodi non hanno privilegi speciali quando chiamano altri metodi dello stesso oggetto, un metodo in una classe-madre che chiama un altro metodo definito nella stessa classe potrebbe finire per chiamare in realtà un metodo sovrascritto in una sotto-classe. (Per i programmatori C++: tutti i metodi in Python sono virtual.)

Un metodo di una sotto-classe potrebbe voler estendere invece che semplicemente rimpiazzare il metodo della classe-madre con lo stesso nome. Per chiamare il metodo della classe-madre, semplicemente basta chiamare BaseClassName.methodname(self, arguments). Talvolta questa tecnica può servire anche al codice «cliente». (Si noti però che questo funziona solo se la classe-madre è accessibile nello scope globale come BaseClassName.)

Python ha due funzioni predefinite che si occupano di ereditarietà:

  • isinstance() controlla il tipo di un’istanza: isinstance(obj, int) restituirà True se obj.__class__ è un int o qualcosa derivato da int.

  • issubclass() controlla l’ereditarietà di una classe: issubclass(bool, int) restituisce True, dal momento che la classe bool è una sotto-classe di int. Al contrario, issubclass(float, int) è False perché float non è una sotto-classe di int.

9.5.1. Ereditarietà multipla

Python supporta anche una forma di ereditarietà multipla. Una classe con più di una classe-madre si può scrivere così:

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

Nei casi più semplici e per la maggior parte dei casi d’uso, potete assumere che la ricerca di un attributo ereditato proceda da sinistra a destra, con una «ricerca in profondità», e senza cercare una seconda volta nella stessa classe quando le gerarchie si sovrappongono. Quindi, se un attributo non viene trovato in DerivedClassName, lo si cerca in Base1, quindi ricorsivamente nelle classi-madre di Base1, quindi in Base2 e così via.

In realtà le cose sono leggermente più complicate; il meccanismo di ricerca dei metodi cambia dinamicamente per supportare chiamate cooperative alla funzione super(). Questo approccio è noto come «call-next-method» in alcuni linguaggi dotati di ereditarietà multipla e offre più possibilità rispetto al super dei linguaggi con ereditarietà semplice.

La ricerca con ordinamento dinamico si rende necessaria perché tutte le ereditarietà multiple finiscono per avere una o più gerarchie «a rombo»: ovvero, almeno una delle classi-madre può essere raggiunta in più di un modo a partire dalla sotto-classe. Per esempio, in Python tutte le classi ereditano da object, quindi tutti gli schemi di ereditarietà multipla hanno necessariamente più di un percorso per arrivare a object. Per evitare di cercare più di una volta nelle classi-madre, l’algoritmo dinamico traccia un percorso di ricerca lineare tale da preservare il principio «da sinistra a destra», da raggiungere ciascuna classe-madre una sola volta, e da essere monotonico (ovvero, è possibile creare una sotto-classe senza influenzare il percorso di ricerca già esistente per la gerarchia superiore). Queste proprietà, nel loro insieme, rendono possibile la progettazione di classi affidabili ed estensibili in un contesto di ereditarietà multipla. Per ulteriori dettagli, si veda https://www.python.org/download/releases/2.3/mro/.

9.6. Variabili private

In Python non esiste il concetto di istanza «privata» di una variabile, che è accessibile solo dall’interno del suo oggetto. Tuttavia esiste una convenzione, adottata quasi ovunque nel codice scritto in Python: un nome che inizia con il «trattino basso» (per esempio _spam) dovrebbe essere trattato come una componente non-pubblica della API (che sia una funzione, un metodo o un dato). Dovrebbe essere considerato come un dettaglio di implementazione, suscettibile di essere modificato in futuro senza preavviso.

Esiste almeno uno scenario reale in cui è desiderabile disporre di una variabile privata: quando si vogliono evitare conflitti con nomi definiti dalle sotto-classi. Python fornisce un supporto limitato per questa necessità, attraverso il name mangling. Tutti i nomi che hanno almeno due «trattini bassi» iniziali e non più di un «trattino basso» finale (come __spam) sono rimpiazzati con _classname__spam, dove classname è il nome della classe corrente senza trattini bassi iniziali. Questa manipolazione avviene per tutti gli identificatori di questo tipo, indipendentemente dalla loro posizione, purché siano definiti all’interno della classe.

La manipolazione dei nomi permette alle sotto-classi di sovrascrivere un metodo senza comprometterne l’invocazione da un’altra 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   # una copia privata del metodo update()

class MappingSubclass(Mapping):

    def update(self, keys, values):
        # sovrascrive update() con una nuova *signature*
        # ma non rompe il funzionamento della chiamata in __init__()
        for item in zip(keys, values):
            self.items_list.append(item)

Questo esempio funzionerebbe anche se MappingSubclass volesse introdurre un suo __update, dal momento che sarebbe rimpiazzato con _Mapping__update nella classe-madre e con _MappingSubclass__update nella sotto-classe.

Si noti che il meccanismo del mangling vuole essere soprattutto un modo per evitare conflitti di nomi: è comunque sempre possibile accedere o modificare una variabile «privata». Questo può essere anzi utile in talune circostanze, per esempio in un debugger.

Si noti inoltre che il codice passato alle funzioni exec() o eval() non considera la classe che invoca il metodo come la classe «corrente» e quindi non usa quel nome per il mangling; è un effetto simile a quello dell’istruzione global, che infatti, anch’essa, vale solo nel codice che è stato compilato insieme (nel senso di byte-compiled). Lo stesso vale per getattr(), setattr() e delattr(), e anche quando si utilizza direttamente __dict__.

9.7. Note varie

Può essere utile talvolta disporre di una struttura-dati simile al «record» di Pascal o a «struct» in C, impacchettando insieme alcuni dati referenziati con variabili. Si può usare una classe vuota:

class Employee:
    pass

john = Employee()  # Crea una scheda di impiegato vuota

# Riempie i campi della scheda
john.name = 'John Doe'
john.dept = 'computer lab'
john.salary = 1000

Se una sezione di codice Python si aspetta di ricevere uno specifico tipo di dato astratto, le si può passare invece una classe che emula i metodi di quel tipo di dato. Per esempio, se avete una funzione che formatta dei dati provenienti da un file di testo, potete definire una classe con dei metodi read() e readline() che invece prelevano i dati da una stringa-buffer, e passare questa alla funzione come argomento.

Gli oggetti-metodi di istanza hanno a loro volta degli attributi: m.__self__ è l’oggetto-istanza che possiede il metodo m(), e m.__func__ è l’oggetto-funzione corrispondente al metodo.

9.8. Iteratori

Avrete probabilmente già notato che è possibile iterare su molti oggetti contenitori con l’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 modo di accesso è chiaro, sintetico, efficiente. L’uso degli iteratori è onnipresente in Python. Dietro le quinte, l’istruzione for chiama la funzione iter() dell’oggetto contenitore. La funzione restituisce un oggetto iteratore, che a sua volta definisce il metodo __next__(), che accede agli elementi del contenitore, uno alla volta. Quando gli elementi sono finiti, __next__() emette un’eccezione StopIteration, che comunica all’istruzione for di terminare. Potete chiamare direttamente il metodo __next__() usando la funzione predefinita next(). Questo esempio spiega come funziona il meccanismo:

>>> s = 'abc'
>>> it = iter(s)
>>> it
<iterator object at 0x00A1DB50>
>>> 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

Conoscendo il meccanismo che governa il comportamento degli iteratori, è facile aggiungere questa funzionalità alle vostre classi. Occorre definire un metodo __iter__() che restituisce un oggetto a sua volta dotato di un metodo __next__(). Se la classe definisce già __next__(), allora __iter__() può limitarsi a restituire self:

class Reverse:
    """Un iteratore che cicla all'indietro su una sequenza."""
    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

Un generatore è uno strumento semplice e potente per creare iteratori. I generatori sono definiti come normali funzioni che però utilizzano l’istruzione yield quando vogliono restituire dei dati. Ogni volta che la funzione next() viene chiamata su un generatore, questo riprende l’esecuzione da dove l’aveva interrotta (ricorda tutti i valori in sospeso e qual è stata l’ultima istruzione eseguita). Ecco un esempio che mostra come creare un generatore può essere molto semplice:

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 un generatore può anche essere fatto con un iteratore in una classe, come visto nel paragrafo precedente. I generatori però sono più compatti grazie al fatto che i metodi __iter__() e __next__() vengono creati automaticamente.

Un altro vantaggio importante è che le variabili locali e lo stato dell’esecuzione vengono salvati tra una chiamata e l’altra. In questo modo scrivere la funzione è più facile e molto più chiaro, rispetto a dover usare variabili di istanza come self.index e self.data.

Oltre alla creazione automatica dei metodi e alla persistenza dello stato del programma, un generatore emette automaticamente un’eccezione StopIteration quando termina. Combinate insieme, queste caratteristiche permettono di creare iteratori con la stessa facilità con cui si scrive una normale funzione.

9.10. Espressioni-generatore

Alcuni semplici generatori possono essere scritti in modo sintetico come delle espressioni, usando una sintassi simile a quella delle list comprehension, ma con le parentesi tonde invece delle parentesi quadre. Queste espressioni sono adatte alle situazioni in cui il generatore è consumato immediatamente da una funzione di ordine superiore. Le espressioni-generatore sono più compatte, ma meno versatili rispetto a un normale generatore; tendono a consumare meno memoria dell’equivalente list comprehension.

Esempi:

>>> sum(i*i for i in range(10))                 # somma di quadrati
285

>>> xvec = [10, 20, 30]
>>> yvec = [7, 5, 3]
>>> sum(x*y for x,y in zip(xvec, yvec))         # prodotto scalare
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