Subsections


9. Classi

Il meccanismo delle classi di Python è in grado di aggiungere classi al linguaggio con un minimo di nuova sintassi e semantica. È un miscuglio dei meccanismi delle classi che si trovano in C++ e Modula-3. Come pure per i moduli, in Python le classi non pongono una barriera invalicabile tra la definizione e l'utente, piuttosto fanno affidamento su una politica-utente di ``rispetto della definizione''. Le funzionalità più importanti delle classi sono comunque mantenute in tutta la loro potenza: il meccanismo di ereditarietà permette classi base multiple, una classe derivata può sovrascrivere qualsiasi metodo delle sue classi base, un metodo può chiamare il metodo di una classe base con lo stesso nome. Gli oggetti possono contenere una quantità arbitraria di dati privati.

Secondo la terminologia C++, tutti i membri delle classi (inclusi i dati membri) sono pubblici, e tutte le funzioni membro sono virtuali. Non ci sono speciali costruttori o distruttori. Come in Modula-3, non ci sono scorciatoie per riferirsi ai membri dell'oggetto dai suoi metodi: la funzione del metodo viene dichiarata con un primo argomento esplicito che rappresenta l'oggetto, che viene fornito in modo implicito dalla chiamata. Come in Smalltalk, le classi in sé sono oggetti, quantunque nel senso più ampio del termine: in Python, tutti i tipi di dati sono oggetti. Ciò fornisce una semantica per l'importazione e la ridenominazione. Ma, diversamente da da quanto accade in C++ o Modula-3, i tipi built-in non possono essere usati come classi base per estensioni utente. Inoltre, come in C++ ma diversamente da quanto accade in Modula-3, la maggior parte degli operatori built-in con una sintassi speciale (operatori aritmetici, sottoselezioni ecc.) possono essere ridefiniti mediante istanze di classe.


9.1 Qualche parola sulla terminologia

Data la mancanza di una terminologia universalmente accettata quando si parla di classi, si farà occasionalmente uso di termini Smalltalk e C++. -- Vorrei usare termini Modula-3, dato che la sua semantica orientata agli oggetti è più vicina a quella di Python di quanto sia quella del C++, ma mi aspetto che pochi dei miei lettori ne abbiano sentito parlare.

Si avvisa che per i lettori con conoscenze di programmazione orientata agli oggetti c'è un'inghippo terminologico: il termine ``oggetto'' in Python non significa necessariamente un'istanza di classe. Come in C++ e in Modula-3, e diversamente da Smalltalk, in Python non tutti i tipi sono classi: i tipi built-in di base come gli interi e le liste non lo sono, e non lo sono neanche alcuni tipi un po' più esotici come i file. Comunque tutti i tipi Python condividono una parte di semantica comune che trova la sua miglior descrizione nell'uso della parola oggetto.

Gli oggetti sono dotati di individualità, e nomi multipli (in ambiti di visibilità multipla) possono essere associati allo stesso oggetto. In altri linguaggi ciò è noto come `aliasing'. Questo non viene di solito apprezzato dando una prima occhiata al linguaggio, e può essere ignorato senza problemi quando si ha a che fare con tipi di base immutabili (numeri, stringhe, tuple). Comunque, l'aliasing ha un effetto (voluto!) sulla semantica del codice Python che riguarda oggetti mutabili come liste, dizionari e la maggior parte dei tipi che rappresentano entità esterne al programma (file, finestre etc.). Questo viene usato a beneficio del programma, dato che gli alias si comportano per certi versi come puntatori. Per esempio passare un oggetto è economico, dato che per implementazione viene passato solo un puntatore. E se una funzione modifica un oggetto passato come argomento, le modifiche saranno visibili al chiamante -- questo ovvia al bisogno di due meccanismi diversi per passare gli argomenti come in Pascal.


9.2 Gli ambiti di visibilità di Python e gli spazi dei nomi

Prima di introdurre le classi, è necessario dire qualcosa circa le regole sugli ambiti di visibilità di Python. Le definizioni di classe fanno un paio di graziosi giochetti con gli spazi dei nomi, ed è necessario conoscere come funzionano gli scope e gli spazi dei nomi per comprendere bene quello che succede. Detto per inciso, la conoscenza di questo argomento è utile ad ogni programmatore Python avanzato.

Si inizia con alcune definizioni.

Lo spazio dei nomi è una mappa che collega i nomi agli oggetti. La maggior parte degli spazi dei nomi vengono attualmente implementati come dizionari Python, ma nell'uso normale ciò non è avvertibile in alcun modo (eccetto che per la velocità di esecuzione), e potrebbe cambiare in futuro. Esempi di spazi dei nomi sono: l'insieme dei nomi built-in (funzioni come abs() ed i nomi delle eccezioni built-in), i nomi globali in un modulo e i nomi locali in una chiamata di funzione. In un certo senso l'insieme degli attributi di un oggetto costituisce anch'esso uno spazio dei nomi. La cosa davvero importante da sapere al riguardo è che non c'è assolutamente nessuna relazione tra nomi uguali in spazi dei nomi diversi; ad esempio due moduli diversi potrebbero entrambi definire una funzione ``massimizza'' senza possibilità di fare confusione -- gli utenti dei moduli dovranno premettere ad essa il nome del modulo.

A proposito, spesso si utilizza la parola attributo per qualsiasi nome che segua un punto -- per esempio, nell'espressione z.real, real è un attributo dell'oggetto z. A rigor di termini, i riferimenti a nomi nei moduli sono riferimenti ad attributi: nell'espressione nomemodulo.nomefunzione, nomemodulo è un oggetto modulo e nomefunzione è un suo attributo. In questo caso capita che si tratti di una mappa diretta tra gli attributi del modulo e i nomi globali definiti nel modulo: essi condividono lo stesso spazio dei nomi! 9.1

Gli attributi possono essere a sola lettura o scrivibili. Nel secondo caso, è possibile assegnare un valore all'attributo. Gli attributi dei moduli sono scrivibili: si può digitare "nomemodulo.la_risposta = 42". Gli attributi scrivibili possono anche essere cancellati con l'istruzione del, per esempio "del nomemodulo.la_risposta" rimuoverà l'attributo la_risposta dall'oggetto chiamato nomemodulo.

Gli spazi dei nomi vengono creati in momenti diversi ed hanno tempi di sopravvivenza diversi. Lo spazio dei nomi che contiene i nomi built-in viene creato all'avvio dell'interprete Python e non viene mai cancellato. Lo spazio dei nomi globale di un modulo viene creato quando viene letta la definizione del modulo; normalmente anche lo spazio dei nomi del modulo dura fino al termine della sessione. Le istruzioni eseguite dall'invocazione a livello più alto dell'interprete, lette da un file di script o interattivamente, vengono considerate parte di un modulo chiamato __main__, cosicché esse hanno il proprio spazio dei nomi globale. In effetti anche i nomi built-in esistono in un modulo, chiamato __builtin__.

Lo spazio dei nomi locali di una funzione viene creato quando viene invocata una funzione e cancellato quando la funzione restituisce o solleva un'eccezione che non viene gestita al suo interno. In effetti, `oblio' sarebbe un modo migliore di descrivere che cosa accade in realtà. Naturalmente invocazioni ricorsive hanno ciascuna il proprio spazio dei nomi locale.

Uno scope è una regione del codice di un programma Python dove uno spazio dei nomi è accessibile direttamente. ``Direttamente accessibile'' qui significa che un riferimento non completamente qualificato ad un nome cerca di trovare tale nome nello spazio dei nomi.

Sebbene gli scope siano determinati staticamente, essi sono usati dinamicamente. In qualunque momento durante l'esecuzione sono in uso esattamente tre scope annidati (cioè, esattamente tre spazi dei nomi sono direttamente accessibili): lo scope più interno, in cui viene effettuata per prima la ricerca, contiene i nomi locali, lo scope mediano, esaminato successivamente, contiene i nomi globali del modulo corrente, e lo scope più esterno (esaminato per ultimo) è lo spazio dei nomi che contiene i nomi built-in.

Se il nome viene dichiarato come globale, allora tutti i riferimenti e gli assegnamenti sono diretti allo scope mediano che contiene i nomi globali del modulo. Altrimenti, tutte le variabili trovate fuori dallo scope più interno saranno in sola lettura.

Di solito lo scope locale si riferisce ai nomi locali della funzione corrente (dal punto di vista del codice). All'esterno delle funzioni, lo scope locale fa riferimento allo stesso spazio dei nomi come scope globale: lo spazio dei nomi del modulo. La definizione di una classe colloca ancora un'altro spazio dei nomi nello scope locale.

È importante capire che gli scope vengono determinati letteralmente secondo il codice: lo scope globale di una funzione definita in un modulo è lo spazio dei nomi di quel modulo, non importa da dove o con quale alias venga invocata la funzione. D'altro lato, l'effettiva ricerca dei nomi viene fatta dinamicamente in fase di esecuzione. Comunque la definizione del linguaggio si sta evolvendo verso la risoluzione statica dei nomi, al momento della ``compilazione'', quindi non si faccia affidamento sulla risoluzione dinamica dei nomi! Di fatto le variabili locali vengono già determinate staticamente.

Un cavillo particolare di Python è che gli assegnamenti prendono sempre in esame lo scope più interno. Gli assegnamenti non copiano dati, associano solamente nomi ad oggetti. Lo stesso vale per le cancellazioni: l'istruzione "del x" rimuove l'associazione di x dallo spazio dei nomi in riferimento allo scope locale. In effetti tutte le operazioni che introducono nuovi nomi usano lo scope locale: in particolare, le istruzioni import e le definizioni di funzione associano il nome del modulo o della funzione nello scope locale. L'istruzione global può essere usata per indicare che particolari variabili vivono nello scope globale.


9.3 Un primo sguardo alle classi

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


9.3.1 La Sintassi della definizione di classe

La forma più semplice di definizione di una classe è del tipo:

class NomeClasse:
    <istruzione-1>
    .
    .
    .
    <istruzione-N>

Le definizione di classe, come le definizioni di funzione (istruzioni def), devono essere eseguite prima di avere qualunque effetto. È plausibile che una definizione di classe possa essere collocata in un ramo di un'istruzione if, o all'interno di una funzione.

In pratica, le istruzioni dentro una definizione di classe saranno di solito definizioni di funzione, ma è permesso, e talvolta utile, che vi si trovino altre istruzioni, ci ritorneremo sopra più avanti. Le definizioni di funzione dentro una classe normalmente hanno un elenco di argomenti di aspetto peculiare, dettato da convenzioni di chiamata dei metodi. Ancora una volta, questo verrà spiegato più avanti.

Quando viene introdotta una definizione di classe, viene creato un nuovo spazio dei nomi, usato come scope locale. Perciò tutti gli assegnamenti a variabili locali finiscono in questo nuovo spazio dei nomi. In particolare, le definizione di funzione vi associano il nome della nuova funzione.

Quando una definizione di classe è terminata normalmente (passando per la sua chiusura), viene creato un oggetto classe. Esso è fondamentalmente un involucro (NdT: wrapper) per i contenuti dello spazio dei nomi creato dalla definizione della classe; impareremo di più sugli oggetti classe nella sezione seguente. Lo scope locale originale (quello in essere proprio prima che venisse introdotta la definizione di classe) viene ripristinato, e l'oggetto classe è quivi associato al nome della classe indicato nell'intestazione della definizione (NomeClasse nell'esempio).


9.3.2 Oggetti classe

Gli oggetti classe supportano due tipi di operazioni: riferimenti ad attributo e istanziazione.

I riferimenti ad attributo usano la sintassi standard utilizzata per tutti i riferimenti ad attributi in Python: oggetto.nome. Nomi di attributi validi sono tutti i nomi che si trovavano nello spazio dei nomi della classe al momento della creazione dell'oggetto classe. Così, se la definizione di classe fosse del tipo:

class MiaClasse:
    """Una semplice classe d'esempio"""
    i = 12345
    def f(self):
        return 'ciao mondo'

Quindi MiaClasse.i e MiaClasse.f sarebbero riferimenti validi ad attributi, che restituirebbero rispettivamente un intero ed un oggetto metodo. Sugli attributi di una classe è anche possibile effettuare assegnamenti, quindi è possibile cambiare il valore di MiaClasse.i con un assegnamento. Anche __doc__ è un attributo valido, in sola lettura, che restituisce la stringa di documentazione della classe: "Una semplice classe di esempio".

L'istanziazione di una classe usa la notazione delle funzioni. Si comporta come se l'oggetto classe sia una funzione senza parametri che restituisce una nuova istanza della classe. A esempio, (usando la classe definita sopra):

x = MiaClasse()

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

L'operazione di instanziazione (la ``chiamata'' di un oggetto classe) crea un oggetto vuoto. In molti casi si preferisce che vengano creati oggetti con uno stato iniziale noto. Perciò una classe può definire un metodo speciale chiamato __init__(), come in questo caso:

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

Quando una classe definisce un metodo __init__(), la sua instanziazione invoca automaticamente __init__() per l'istanza di classe appena creata. Così nel nostro esempio una nuova istanza, inizializzata, può essere ottenuta con:

x = MiaClasse()

Naturalmente il metodo __init__() può avere argomenti, per garantire maggior flessibilità. In tal caso, gli argomenti forniti all'istanziazione della classe vengono passati a __init__(). Per esempio,

>>> class Complesso:
...     def __init__(self, partereale, partimag):
...         self.r = partereale
...         self.i = partimag
... 
>>> x = Complesso(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 sole operazioni che essi conoscono per mezzo dell'istanziazione degli oggetti sono i riferimenti ad attributo. Ci sono due tipi di nomi di attributo validi.

Chiameremo il primo attributo dato. Corrispondono alle ``variabili istanza'' in Smalltalk, e ai ``dati membri'' in C++. Gli attributi dato non devono essere dichiarati; come le variabili locali, essi vengono alla luce quando vengono assegnati per la prima volta. Per esempio, se x è l'istanza della MiaClasse precedentemente creata, 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

Il secondo tipo di riferimenti ad attributo conosciuti dagli oggetti istanza sono i metodi. Un metodo è una funzione che ``appartiene a'' un oggetto. In Python, il termine metodo non è prerogativa delle istanze di classi: altri tipi di oggetto possono benissimo essere dotati di metodi; p.e. gli oggetti lista hanno metodi chiamati append, insert, remove, sort, e così via. Comunque più sotto useremo il termine metodo intendendo esclusivamente i metodi degli oggetti istanza di classe, a meno che diversamente specificato.

I nomi dei metodi validi per un oggetto istanza dipendono dalla sua classe. Per definizione, tutti gli attributi di una classe che siano oggetti funzione (definiti dall'utente) definiscono metodi corrispondenti alle sue istanze. Così nel nostro esempio x.f è un riferimento valido ad un metodo, dato che MiaClasse.f è una funzione, ma x.i non lo è, dato che MiaClasse.i non è una funzione. Però x.f non è la stessa cosa di MiaClasse.f: è un oggetto metodo, non un oggetto funzione.


9.3.4 Oggetti metodo

Di solito un metodo viene invocato direttamente, p.e.:

x.f()

Nel nostro esempio, questo restituirà la stringa 'ciao mondo'. Comunque, non è necessario invocare un metodo in modo immediato: x.f è un oggetto metodo, e può essere messo da parte e chiamato in un secondo tempo. Ad esempio:

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

continuerà a stampare "ciao mondo" senza fine.

Che cosa succede esattamente quando viene invocato un metodo? Forse si è notato che x.f() è stato invocato nell'esempio sopra senza argomenti, anche se la definizione di funzione per f specificava un argomento. Che cosa è accaduto all'argomento? Di sicuro Python solleva un'eccezione quando una funzione che richiede un argomento viene invocata senza nessun argomento, anche se poi l'argomento non viene effettivamente utilizzato...

In realtà si potrebbe aver già indovinato la risposta: la particolarità dei metodi è che l'oggetto viene passato come primo argomento della funzione. Nel nostro esempio, la chiamata x.f() è esattamente equivalente a MiaClasse.f(x). In generale, invocare un metodo con una lista di n argomenti è equivalente a invocare la funzione corrispondente con una lista di argomenti creata inserendo l'oggetto cui appartiene il metodo come primo argomento.

Se non fosse ancora chiaro il funzionamento dei metodi, uno sguardo all'implementazione potrebbe forse rendere più chiara la faccenda. Quando viene fatto un riferimento ad un attributo di un'istanza che non è un attributo dato, viene ricercata la sua classe. Se il nome indica un attributo di classe valido che è un oggetto funzione, viene creato un oggetto metodo `impacchettando' insieme in un oggetto astratto (puntatori a) l'oggetto istanza e l'oggetto funzione appena trovato: questo è l'oggetto metodo. Quando l'oggetto metodo viene invocato con una lista di argomenti viene `spacchettato' e costruita una nuova lista di argomenti dall'oggetto istanza e dalla lista di argomenti originale, l'oggetto funzione viene invocato con questa nuova lista di argomenti.


9.4 Note sparse

Gli attributi dato prevalgono sugli attributi metodo con lo stesso nome; per evitare accidentali conflitti di nomi, che potrebbero causare bug difficili da scovare in programmi molto grossi, è saggio usare una qualche convenzione che minimizzi le possibilità di conflitti, per esempio usare le maiuscole per l'iniziale dei nomi di metodi, far precedere i nomi di attributi dato da una piccola stringa particolare (probabilmente basterebbe un trattino basso, di sottolineatura), o usare verbi per i metodi e sostantivi per gli attributi dato.

Si può fare riferimento agli attributi dato dai metodi, come pure dagli utenti ordinari (utilizzatori finali) di un oggetto. In altre parole, le classi non sono utilizzabili per implementare tipi di dato puramente astratti. In effetti, in Python non c'è nulla che renda possibile imporre l'occultamento dei dati (`data hiding'), ci si basa unicamente sulle convenzioni. D'altra parte, l'implementazione di Python, scritta in C, può nascondere completamente i dettagli dell'implementazione e il controllo degli accessi a un oggetto se necessario; questo può essere utilizzato da estensioni a Python scritte in C.

Gli utilizzatori finali dovrebbero usare gli attributi dato con cura, potrebbero danneggiare degli invarianti conservati dai metodi sovrascrivendoli con i loro attributi dato. Si noti che gli utilizzatori finali possono aggiungere degli attributi dato propri ad un oggetto istanza senza intaccare la validità dei metodi, fino quando vengano evitati conflitti di nomi. Ancora una volta, una convenzione sui nomi può evitare un sacco di mal di testa.

Non ci sono scorciatoie per referenziare attributi dato (o altri metodi!) dall'interno dei metodi. Trovo che questo in realtà aumenti la leggibilità dei metodi: non c'è possibilità di confondere le variabili locali e le variabili istanza quando si scorre un metodo.

Convenzionalmente, il primo argomento dei metodi è spesso chiamato self. Questa non è niente di più che una convenzione: il nome self non ha assolutamente alcun significato speciale in Python. Si noti comunque che se non si segue tale convenzione il proprio codice può risultare meno leggibile ad altri programmatori Python, ed è anche plausibile che venga scritto un programma browser delle classi che si basi su tale convenzione.

Qualsiasi oggetto funzione che sia attributo di una classe definisce un metodo per le istanze di tale classe. Non è necessario che il codice della definizione di funzione sia racchiuso nella definizione della classe: va bene anche assegnare un oggetto funzione a una variabile locale nella classe. Per esempio:

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

class C:
    f = f1
    def g(self):
        return 'ciao mondo'
    h = g

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

I metodi possono chiamare altri metodi usando gli attributi metodo dell'argomento self, p.e.:

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 che contiene la definizione della classe (la classe in sé stessa non viene mai usata come scope globale!). Mentre raramente si incontrano buone ragioni per usare dati globali in un metodo, ci sono molti usi legittimi dello scope globale: per dirne uno, funzioni e moduli importati nello scope globale possono essere usati dai metodi, come pure funzioni e classi in esso definite. Di solito la classe che contiene il metodo è essa stessa definita in tale scope globale, e nella sezione seguente saranno esposte alcune ottime ragioni per le quali un metodo potrebbe voler referenziare la sua stessa classe!


9.5 Ereditarietà

Naturalmente le classi non sarebbero degne di tal nome se non supportassero l'ereditarietà. La sintassi per la definizione di una classe derivata ha la forma seguente:

class NomeClasseDerivata(NomeClasseBase):
    <istruzione-1>
    .
    .
    .
    <istruzione-N>

Il nome NomeClasseBase dev'essere definito in uno scope contenente la definizione della classe derivata. Al posto di un nome di classe base è permessa anche un'espressione. Questo è utile quando la classe base è definita in un altro modulo, p.e.,

class NomeClasseDerivata(nomemodulo.NomeClasseBase):

L'esecuzione della definizione di una classe derivata procede nello stesso modo di una classe base. Quando viene costruito l'oggetto classe, la classe base viene memorizzata. Viene usata per risolvere i riferimenti ad attributi: se un attributo richiesto non viene rinvenuto nella classe, allora viene cercato nella classe base. Tale regola viene applicata ricorsivamente se la classe base è a sua volta derivata da una qualche altra classe.

Non c'è nulla di speciale da dire circa l'istanziazione delle classi derivate: NomeClasseDerivata() crea una nuova istanza della classe. I riferimenti ai metodi vengono risolti così: viene ricercato il corrispondente attributo di classe, discendendo lungo la catena delle classi base se necessario, e il riferimento al metodo è valido se questo produce un oggetto funzione.

Le classi derivate possono sovrascrivere i metodi delle loro classi base. Dato che i metodi non godono di alcun privilegio speciale, quando chiamano altri metodi dello stesso oggetto, un metodo di una classe base che chiami un altro metodo definito nella stessa classe base può in effetti finire col chiamare un metodo di una classe derivata che prevale su di esso (per i programmatori C++: in Python tutti i metodi sono virtuali).

La sovrascrizione di un metodo di una classe derivata può in effetti voler estendere più che semplicemente rimpiazzare il metodo della classe base con lo stesso nome. C'è un semplice modo per chiamare direttamente il metodo della classe base: basta invocare "NomeClasseBase.nomemetodo(self, argomenti)". Questo in alcune occasioni è utile pure agli utilizzatori finali. Si noti che funziona solo se la classe base viene definita o importata direttamente nello scope globale.


9.5.1 Ereditarietà multipla

Python supporta pure una forma limitata di ereditarietà multipla. Una definizione di classe con classi base multiple ha la forma seguente:

class NomeClasseDerivata(Base1, Base2, Base3):
    <istruzione-1>
    .
    .
    .
    <istruzione-N>

La sola regola necessaria per chiarire la semantica è la regola di risoluzione usata per i riferimenti agli attributi di classe. Essa è prima-in-profondità, da-sinistra-a-destra. Perciò, se un attributo non viene trovato in NomeClasseDerivata, viene cercato in Base1, poi (ricorsivamente) nelle classi base di Base1 e, solo se non vi è stato trovato, viene ricercato in Base2, e così via.

Ad alcuni una regola `prima in larghezza' (NdT: breadth first), che ricerca in Base2 e Base3 prima che nelle classi base di Base1, sembra più naturale. Comunque ciò richiederebbe di sapere se un particolare attributo di Base1 sia in effetti definito in Base1 o in una delle sue classi base prima che si possano valutare le conseguenze di un conflitto di nomi con un attributo di Base2. La regola prima-in-profondità non fa alcuna differenza tra attributi direttamente definiti o ereditati di Base1.

È chiaro che un uso indiscriminato dell'ereditarietà multipla è un incubo per la manutenzione, visto che Python si affida a convenzioni per evitare conflitti accidentali di nomi. Un problema caratteristico dell'ereditarietà multipla è quello di una classe derivata da due classi che hanno una classe base comune. Mentre è abbastanza semplice calcolare cosa succede in questo caso (l'istanza avrà una singola copia delle ``variabili istanza'' o attributi dato usati dalla classe base comune), non c'è evidenza dell'utilità di tali semantiche.


9.6 Variabili private

C'è un supporto limitato agli identificatori privati di una classe. Qualsiasi identificatore nella forma __spam (come minimo due trattini bassi all'inizio, al più un trattino basso in coda) viene rimpiazzato, a livello di codice eseguito, con _nomeclasse__spam, dove nomeclasse è il nome della classe corrente, privato dei trattini di sottolineatura all'inizio. Questa mutilazione viene eseguita senza riguardo alla posizione sintattica dell'identificatore, quindi può essere usata per definire istanze, metodi e variabili di classe private, come pure globali, e anche per memorizzare variabili d'istanza private per questa classe, sopra istanze di altre classi. Potrebbe capitare un troncamento, nel caso in cui il nome mutilato fosse più lungo di 255 caratteri. Al di fuori delle classi, o quando il nome della classe consiste di soli trattini di sottolineatura, non avviene alcuna mutilazione.

La mutilazione dei nomi fornisce alle classi un modo semplice per definire variabili istanza e metodi ``privati'', senza doversi preoccupare di variabili istanza definite da classi derivate, o di pasticci con le variabili compiuti da codice esterno alla classe. Si noti che le regole di mutilazione sono pensate principalmente per evitare errori accidentali; è ancora possibile, a propria discrezione, accedere o modificare una variabile considerata privata. Questo può anche essere utile, per esempio al debugger, e questa è una ragione per cui questa scappatoia non viene impedita. Derivare una classe con lo stesso nome della classe base rende possibile l'uso delle variabili private della classe base.

Si noti che il codice passato a exec, eval() o evalfile() non considera il nome di classe della classe che le invoca come classe corrente; ciò è simile a quanto succede con la dichiarazione global, il cui effetto è ristretto in modo simile al codice che viene byte-compilato assieme. La stessa limitazione si applica a getattr(), setattr() e delattr(), come pure quando si fa riferimento direttamente a __dict__.


9.7 Rimasugli e avanzi

A volte possono essere utili tipi di dati simili al ``record'' del Pascal o allo ``struct'' del C, che riuniscono insieme alcuni elementi dato dotati di nome. Lo si otterrà facilmente definendo una classe vuota, p.e.:

class Employee:
    pass

john = Employee() # Crea un record vuoto

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

A un pezzo di codice Python che si aspetta un particolare tipo di dato astratto si può invece spesso passare una classe che emula i metodi di tale tipo di dato. Per esempio, se si ha una funzione che effettua la formattazione di alcuni dati provenienti da un oggetto file, si può definire una classe con metodi read() e readline() che invece prende i dati da un buffer di stringhe, e lo passa come argomento.

Anche gli oggetti istanza di metodo hanno attributi: m.im_self è l'oggetto di cui il metodo è un'istanza, e m.im_func è l'oggetto funzione corrispondente al metodo.


9.8 Le eccezioni possono essere classi

Definire le eccezioni da parte dell'utente in classi è giusto. Usando tale meccanismo diventa possibile creare gerarchie estendibili di eccezioni.

Ci sono due nuove forme (semantiche) valide per l'istruzione raise:

raise Classe, istanza

raise instanza

Nella prima forma, istanza dev'essere un'istanza di Classe o di una sua classe derivata. Nella seconda si tratta di un'abbreviazione per:

raise istanza.__class__, istanza

Una classe in una clausola except è compatibile con un'eccezione se è della stessa classe o di una sua classe base di questa (non funziona all'inverso, una clausola except che riporti una classe derivata non è compatibile con una classe base). Per esempio, il codice seguente stamperà B, C, D in tale ordine:

class B:
    pass
class C(B):
    pass
class D(C):
    pass

for c in [B, C, D]:
    try:
        raise C()
    except D:
        print "D"
    except C:
        print "C"
    except B:
        print "B"

Si noti che, se le clausole except fossero invertite (con "except B" all'inizio), verrebbe stampato B, B, B. Viene infatti attivata la prima clausola except che trova corrispondenza.

Quando viene stampato un messaggio di errore per un'eccezione non gestita e si tratta di una classe, viene stampato il nome della classe, poi un duepunti e uno spazio, infine l'istanza convertita in una stringa tramite la funzione built-in str().


9.9 Iteratori

Ormai si sarà notato che l'esecuzione della maggior parte degli oggetti può essere replicata ciclicamente mediante un'istruzione for:

for elemento in [1, 2, 3]:
    print elemento
for elemento in (1, 2, 3):
    print elemento
for chiave in {'uno':1, 'due':2}:
    print chiave
for carattere in "123":
    print carattere
for line in open("myfile.txt"):
    print line

Questo stile d'accesso è chiaro, conciso e conveniente. L'uso degli iteratori pervade e unifica Python; dietro le quinte, l'istruzione for richiama sull'oggetto contenitore la funzione iter(): l'oggetto iteratore da essa restituito definisce il metodo next(), il quale introduce degli elementi nel contenitore uno per volta. Quando non ci sono più elementi, next() solleva un'eccezione StopIteration che termina il ciclo iniziato dal for. L'esempio che segue mostra come ciò funzioni:

>>> s = 'abc'
>>> it = iter(s)
>>> it
<iterator object at 0x00A1DB50>
>>> it.next()
'a'
>>> it.next()
'b'
>>> it.next()
'c'
>>> it.next()

Traceback (most recent call last):
  File "<pyshell#6>", line 1, in -toplevel-
    it.next()
StopIteration

Visti i meccanismi di base del protocollo iteratore, è semplice aggiungere un comportamento iteratore alle proprie classi, basta definire un metodo __iter__() che restituisca un oggetto con un metodo next(). Se la classe definisce next(), __iter__() può restituire solo self:

>>> class Reverse:
    "Iteratore per eseguire un ciclo al contrario 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]

>>> for carattere in Reverse('spam'):
	print carattere

m
a
p
s


9.10 Generatori

I generatori sono semplici ma efficaci strumenti per creare iteratori. Sono scritti come funzioni regolari, pur usando l'istruzione yield ogni qualvolta restituiscano dei dati. Siccome ricorda tutti i valori dei dati e l'ultima espressione eseguita, alla chiamata a next() il generatore riprende da dove s'era fermato. Di seguito si mostra come sia facile crearne uno:

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

Ciò che è fattibile con i generatori può essere fatto con iteratori basati su una classe, come visto nella precedente sezione. La creazione automatica dei metodi __iter__() e next() rende i generatori così compatti.

Un'altra caratteristica chiave è il salvataggio automatico delle variabili locali e dello stato d'esecuzione tra una chiamata e l'altra, cosa che rende la funzione più facile a scriversi e più chiara di un approccio con variabili di classe come self.index e self.data.

Oltre alla creazione automatica di metodi e salvataggio dello stato del programma, quando terminano, i generatori sollevano l'eccezione StopIteration. Insieme, queste caratteristiche facilitano la creazione di iteratori con la semplice scrittura di una normalissima funzione.



Footnotes

... nomi!9.1
Eccetto che per una cosa. Gli oggetti modulo hanno un attributo segreto a 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 usarlo viola l'astrazione dell'implementazione dello spazio dei nomi, e l'uso dovrebbe essere limitato a cose tipo i debugger post-mortem.
Vedete Circa questo documento... per informazioni su modifiche e suggerimenti.