|
|
In questo articolo completiamo il discorso introduttivo al Python esaminandone le caratteristiche più avanzate: classi, ereditarietà ed eccezioni. Vedremo che il linguaggio utilizza un modello di OOP molto semplice ma potente, che supporta ereditarietà multipla e operator overloading. Uno sguardo al Python di Michele Sciabarrà Se l'introduzione di Python della volta scorsa vi è parso interessante, questa seconda parte è una buona occasione per approfondirlo. Ovviamente non abbiamo alcuna pretesa di essere esaustivi, non potendo trattare un completo linguaggio di programmazione in 10 pagine, esempi compresi. Tuttavia questi due articoli dovrebbero essere sufficienti a muovere i primi passi e decidere se vale la pena continuare. Entriamo subito nel vivo esaminando le caratteristiche un po' più avanzate concernenti programmazione ad oggetti e la gestione degli errori. Vedremo infatti che Python pur nella sua grande semplicità è orientato ad oggetti in maniera così decisa che è stato proposto come linguaggio standard per lo scripting di oggetti distribuiti CORBA. Istanze Abbiamo visto i moduli, ovvero file che contengono definizioni di funzioni e altri comandi. Creando un nuovo file, chiamato per esempio modulo.py, contenente la definizione di funzione f(), è possibile importarlo con import modulo per poi chiamare la funzione f usando la sintassi modulo.f(). Per inciso, il file è importabile se la directory che lo contiene si trova nel PYTHONPATH. L'impostazione del PYTHONPATH varia da sistema a sistema, ma generalmente si tratta di impostare la variabile di ambiente PYTHONPATH. Notare che f in questo caso è una funzione, non un metodo o altro, anche se per accedervi occorre utilizzare il prefisso modulo. Un modulo è infatti un esempio di namespace; gli elementi in un namespace sono detti attributi. Per accedere ad un elemento di un namespace si utilizza la sintassi namespace.attributo. I builtin (per esempio dir) si trovano nel namespace __builtins__., e sono accedibili (come caso particolare) anche se non si specifica un prefisso. Un altro modo per evitare il prefisso è utilizzare from <namespace> import <attribute>, ma è meglio non abusare di questa facility per non avere problemi di collisioni di nomi. Le istanze delle classi sono anche essi dei namespace, anche se si comportano in maniera più sofisticata dei moduli. Si tratta in un certo senso della naturale evoluzione della programmazione modulare verso la programmazione ad oggetti. Nel resto dell'articolo si assumono basi di OOP: non spiegheremo che cosa è una classe, una istanza, un costruttore o una funzione virtuale, per cui se siete a digiuni di queste nozioni potreste incontrare qualche difficoltà nella lettura. Abbiamo visto che in Python ci sono svariati tipi di dato, come le sequenze, i dizionari, e i moduli. Adesso introdurremo un nuovo tipo di dato, ovvero la classe. Prima vediamo come si usano le classi esistenti, poi esamineremo come si definiscono nuove classi. Sfruttiamo l'aspetto interattivo del Python, che ci consente di imparare cose nuove sperimentando con l'interprete a riga di comando. Come esperimento per imparare l'uso di oggetti preleveremo ed esamineremo una pagina Web con una connessione http, usando la classe HTTP del modulo httplib: >>> import httplib >>> httplib.HTTP <class httplib.HTTP at 874be0> >>> h = httplib.HTTP("192.168.1.1") >>> h <httplib.HTTP instance at 875930> Innanzitutto notiamo che le classi, come le funzioni, sono contenute dentro moduli. In particolare la classe HTTP è contenuta nel modulo httplib, per cui occorre importare il modulo (altrimenti tale classe non è accessibile). Valutando httplib.HTTP osserviamo che si tratta di un oggetto di tipo classe, utilizzabile però in maniera analoga ad una funzione. Chiamando la classe, otteniamo un oggetto di tipo istanza della classe, come si vede nell'esempio. In pratica la classe è una funzione costruttore che produce istanze. L'oggetto istanza così costruito si comporta in maniera simile ad un modulo, ovvero come contenitore di funzioni che possono essere chiamate. In realtà un oggetto è qualcosa di più di un modulo, in quanto mantiene uno stato separato per ogni istanza. Quindi la classe è la naturale evoluzione del concetto di modulo: un oggetto è un namespace analogamente ad un modulo. La differenza principale è che si possono creare istanze diverse dello stesso modulo, ottenendo namespace separati con variabili indipendenti. Questo è il punto cruciale. I dati contenuti in una istanza di una classe vengono inizializzati chiamando una particolare funzione di inizializzazione: __init__. L'ultimo elemento importante che differenzia le classi dai moduli è il fatto che su di esse si può applicare l'ereditarietà, come vedremo più avanti. Riassumendo:
Torniamo al nostro esempio, e utilizzando l'istanza di HTTP chiamandone i metodi: >>> h.putrequest('GET', '/') >>> h.endheaders() >>> h.getreply() (200, 'OK', <mimetools.Message instance at 876bc0>) >>> f = h.getfile() >>> lines = f.readlines() >>> lines[0] '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">\012' Di ogni oggetto, per usarlo occorre conoscerne il funzionamento leggendone la documentazione. Nel caso che stiamo esaminando adesso, una istanza di HTTP invia delle richieste corrispondenti ai comandi HTTP. Questa classe in realtà non maschera molto il protocollo http, in quanto occorre conoscerlo almeno per sommi capi. In particolare, occorre sapere che la richiesta per prelevare una pagina web è GET, seguita dall'URL della pagina senza l'indirizzo dell'host. Dopo la richiesta, occorre anche aggiungere delle informazioni supplementari che vengono date al Web Server utilizzando degli header. Nel nostro caso non inviamo alcuna informazione supplementare, completando la richiesta con endheader(), e leggiamo la risposta del Web Server. Poiché è OK (codice 200, stringa di messaggio OK, altre informazioni nella istanza mimetools.Message) possiamo leggere la pagina web tramite un oggetto file per la lettura. Per semplicità, leggiamo le righe ponendole in una lista, e ne stampiamo solo la prima. Classi Impariamo adesso a creare nuove classi. Nel caso più semplice possiamo dichiarare una classe vuota: >>> class Void: ... pass ... >>> x = Void() >>> x <__main__.Void instance at 85ff80> >>> x.a Traceback (innermost last): File "<interactive input>", line 0, in ? AttributeError: a >>> x.a=1 >>> y = Void() >>> y.a=2 >>> print x.a,y.a 1 2 Utilizzando la keyword class abbiamo dichiarato una nuova classe, in questo chiamata Void. In una definizione di classe possono esserci comandi di qualsiasi genere, che vengono eseguiti durante la definizione della classe. I comandi per noi interessanti comunque sono quelli che dichiarano attributi, ovvero variabili e funzioni che diventano campi e metodi della classe. Gli altri comandi possono servire per esempio a definire dei metodi condizionalmente (per esempio in un modo sotto Windows e un altro sotto Unix) ma è meglio non abusare di queste possibilità. Ad una classe non vengono assegnati una volta e per tutte tutti i suoi attributi, ma possono essere aggiunti anche dopo che l'istanza è stata creata. Infatti in genere gli attributi vengono aggiunti in fase di inizializzazione. Come si vede nel'esempio, dopo aver dichiarato la classe, utilizziamo la funzione costruttore, avente lo stesso nome della classe, per creare nuove istanze. Nell'esempio utilizzando Void come funzione abbiamo creato l'istanza. Ogni instanza è un namespace separato che inizialmente non contiene alcun attributo. La natura dinamica del Python ci consente di aggiungere, "al volo", nuovi attribuiti, nell'esempio a. Che le istanze siano namespace separati e indipendenti si vede anche dal fatto che creando due istanze, possiamo assegnare ad ogni istanza un valore diverso per l'attribuito a. Otteniamo, come ci si aspetta, due a separati e indipendenti per ogni istanza.
Le classi divengono interessanti quando contengono campi e metodi. Nel Listato 1 possiamo vedere la dichiarazione di una classe List, contenuta nel file list.py, che andiamo subito ad utilizzare: >>> import list >>> x = list.List(2) >>> x.printall <method List.printall of List instance at 8654a0> >>> x.printall() 2 >>> x = x.push(1) >>> x.printall() 1 2 >>> x=x.append(3) >>> x.printall() 1 2 3 Come si vede, abbiamo definito una classe lista con tre metodi: append, push e printall. Esaminiamo ora in dettaglio il meccanismo della definizione della classe. Dichiarando una classe ci ritroviamo con una funzione costruttore che ha lo stesso nome della classe. In realtà questo costruttore crea soltanto l'oggetto istanza, ma non lo inizializza. Se occorre effettuare delle inizializzazioni supplementari (caso abbastanza frequente), si deve definire una funzione di inizializzazione che si deve chiamare __init__. L'uso dei doppi underscore all'inizio e alla fine è una convenzione Python usata ovunque sia necessario definire nomi che hanno un significato speciale, e non è limitata solo a questo caso. La __init__ viene chiamata automaticamente dopo che è stata creata l'istanza dell'oggetto. Veniamo ora a quello che è un punto critico, ed è basilare per comprendere tutto il meccanismo della OOP in Python. Dovrebbe essere chiaro come funzionano in Python le funzioni (non c'è niente di speciale, a parte il fatto che sono solitamente contenute dentro dei moduli). Ora, nelle classi non ci sono funzioni ma metodi. In OOP, in generale i metodi sono funzioni speciali, in quanto conoscono l'oggetto a cui appartengono. In Java, C++ e altri linguaggi OOP l'accesso all'oggetto corrente è implicito e nascosto: il programmatore non se ne accorge. In Python invece l'oggetto corrente viene passato esplicitamente come primo parametro della chiamata del metodo. Confrontando l'ultimo esempio con il listato il meccanismo dovrebbe diventare abbastanza chiaro. Precisamente, scrivendo x = list.List(2) si costruisce un oggetto istanza, poi viene chiamato automaticamente __init__(self, data, next=None). Il primo parametro di __init__ è l'oggetto appena costruito, il secondo è il parametro fornito al costruttore (2). In questo esempio si utilizza anche la feature dei parametri di default: poiché non abbiamo specificato il terzo parametro questo assume il valore di default None. Il meccanismo è analogo quando invochiamo i metodi: chiamato x.printall() viene chiamata la funzione printall passando x come primo parametro. Per convenzione il primo parametro di un metodo viene chiamato self. Non c'è niente di speciale in questo nome ma non seguire questa convenzione può compromettere la leggibilità del vostro listato ad altri programmatori Python. Non ci sono modi per accedere direttamente ai campi di un oggetto: occorre usare sempre il prefisso self. Per quanto questa cosa possa apparire noiosa, in pratica questo migliora la leggibilità e evita ambiguità con le variabili locali. Ereditarietà L'ereditarietà è il meccanismo che consente di riutilizzare codice già esistente organizzato in classi: grazie ad essa possiamo creare nuove classi che estendono e modificano quelle esistenti. Esemplifichiamo il funzionamento, considerando una classe Punto2D, che estendiamo per ottenere un Punto3D: import math class Punto2D: def __init__(self, x, y): self.x = x self.y = y def dist(self): return math.sqrt(self.x **2 + self.y **2) class Punto3D(Punto2D): def __init__(self, x, y, z): Punto2D.__init__(self, x, y) self.z = z def dist(self): return math.sqrt(Punto2D.dist(self)**2 + self.z**2) Per ereditare da una classe si usa la sintassi class Derivata(Base1,Base2), dove Derivata è la classe che eredita dalle classi Base1 , Base2. Nell'esempio di sopra è mostrato un caso di ereditarietà singola ma Python supporta in generale l'ereditarietà multipla. La nuova classe eredita tutti i metodi delle classi base, il che significa che un metodo disponibile in una Base1 o Base2 è disponibile anche nella classe Derivata. Un metodo può trovarsi anche in più di una delle classi base: in tal caso viene usato il metodo trovato effettuando una ricerca deep-first nel grafo delle classi. La cosa importante è che ereditando possiamo ridefinire i metodi della classe base, come si vede nell'esempio, dove il metodo dist di Punto3D ridefinisce il metodo dist di Punto2D. Per accedere ai metodi della classe base (operazione necessaria per sfruttare il codice preesistente) si deve far riferimento esplicitamente al metodo chiamandolo con il nome della classe e passando self come parametro. Nell'esempio si nota come vengano chiamati esplicitamente Punto2D.dist e Punto2D.__init__. Infine accenniamo al fatto che Python supporta anche l'operator overloading: è possibile per esempio ridefinire l'operatore + definendo il metodo __sum__, l'operatore * ridefinendo il metodo __mult__ e così via. Non trattiamo in dettaglio queste caratteristiche per ragioni di spazio. Eccezioni Durante l'esecuzione dei programmi possono insorgere degli errori. Linguaggi tradizionali come il C o il Pascal non prendono particolari provvedimenti per gestirli (è compito del programmatore "stare attento" e verificare bene i valori ritornati per riconoscere e gestire gli errori). In pratica però una condizione di errore è sempre qualcosa di particolare che altera il normale flusso del programma, e che tende a sfuggire all'attenzione del programmatore. Queste condizioni eccezionali, in pratica così eccezionali non sono, e devono essere in qualche modo gestite perché i programmi diventino robusti (ovvero non si piantano ogni 5 minuti). Non a caso i "disastri a catena" che avvengono nei programmi hanno origine in qualche errore non gestito che si propaga causando errori su errori fino alla terminazione del programma (nei casi fortunati) o il blocco del sistema (di solito). Per gestire le condizioni di errore è stato inventato, fin dai tempi antichi dell'informatica (vent'anni fa), il meccanismo delle eccezioni. Per molto tempo però la gestione delle eccezioni è rimasta confinata ai linguaggi assembler: da qualche anno è approdata anche ai linguaggi ad alto livello, come C++, Java e, appunto, Python. Vediamo come funzionano le eccezioni considerando la gestione di un errore tanto semplice quanto (spesso) inaspettato: una divisione per zero: >>> x = 0 >>> >>> 1/x Traceback (innermost last): File "<interactive input>", line 0, in ? ZeroDivisionError: integer division or modulo >>> try: ... y = 1/x ... except ZeroDivisionError: ... print "cannot divide!" ... cannot divide! >>> ZeroDivisionError <class exceptions.ZeroDivisionError at 7687e0> In caso di eccezione, l'esecuzione si interrompe e causa la stampa di un messaggio di errore. Per essere esatti, durante l'esecuzione di un programma una eccezione causa il ritorno dalla funzione o metodo in cui ci si trova e la generazione di un eccezione nel punto in cui si ritorna. Questo meccanismo viene ripetuto, portando ad un ritorno forzato da tutte le chiamate correnti finché non si raggiunge il toplevel (che causa la terminazione con un messaggio errore), a meno che l'eccezione non venga in qualche modo catturata e gestita. In questo modo le condizioni di errore non sono ignorabili a meno che il programmatore non decida esplicitamente di ignorarle. Comunque in questo mod si tende a confinare gli errori in precisi sottosistemi che non causano la terminazione anomala del programma. Nell'esempio vediamo anche come funziona il meccanismo di gestione delle eccezioni: si sottopone a controllo il blocco che può causare l'eccezione tramite try. Le eccezioni possono essere provocate esplicitamente utilizzando raise. Se scatta una eccezione, questa viene confrontata con le possibili eccezioni che si vogliono gestire, usando la clausola except. Non ci deve essere necessariamente una sola clausola except ma anzi è utile che ce ne sia più d'una. Scatterà quella corrispondente al tipo di eccezione sollevato. Se nessuna va bene, l'eccezione si propaga come se non ci fosse stato alcun controllo. Questa procedura è anche il modo più corretto di gestirla: controllare gli errori che si prevedono e lasciar passare quelli inaspettati in modo che in fase di test e debug si possano rilevare le condizioni di errore non previste. Accenniamo infine al fatto che in Python la try può anche avere le clausole else e finally. La else viene eseguita se il codice sottoposto a try non causa eccezioni. La clausola finally invece viene eseguita in ogni caso, sia nel caso che il codice causi eccezioni, sia nel caso che tutti fili liscio. È anche possibile utilizzare classi definite dall'utente per organizzare gerarchicamente le eccezioni. Anche questo non lo trattiamo in dettaglio per motivi di spazio. Esempio: Gestione Form Concludiamo l'articolo con un esempio un po' più dettagliato e significativo: un piccolo programma che può essere usato per i vostri script Web di gestione form. Tengo a precisare che l'esempio qui pubblicato è stato scritto utilizzando Windows 98 con Personal Web Server. L'unico accorgimento per utilizzarlo in questo ambiente è quello di aggiungere al registro la key "HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\W3SVC\Parameters\Script Map\.py" con valore di tipo stringa "C:\Python\Python.exe %s", e di porre i vostri script in una directory web-shared con il permesso execute attivato. Lo script comunque ha girato senza problemi sotto Linux. In questo caso è stato usato il meccanismo del bang-path, ovvero inserire come prima riga del codice il riferimento all'interprete dello script con il prefisso #! Una schermata dello script è visibile in Figura 2, mentre lo script stesso è nel Listato 2.
Spendiamo due parole sul funzionamento dello script anche perché il Python è posizionabile come una alternativa al Perl, e gli script CGI sono probabilmente il campo di applicazione più immediato del Python. Nel nostro script abbiamo utilizzato il modulo standard cgi per decifrare i parametri dello script. Come è noto il protocollo CGI invia i dati in un formato encrittato non particolarmente user-friendly. Chiamando form = cgi.FieldStorage viene costruito un oggetto contente i parametri passati allo script decifrati. A questo oggetto è possibile accedere come array associativo, estraendo i campi della form: form["nome"], eccetera. Attenzione che se si accede ad un campo non definito si ottiene una eccezione, per cui occorre verificare l'esistenza di una chiave usando form.has_key("nome"). Nello script abbiamo anche usato altre due feature del Python adatte alla generazione di pagine Web: il triple quote """ e l'operatore di I/O %. Per inserire in uno script Python stringhe di testo contenenti newline si deve usare come delimitatore tre virgolette. Un altro aspetto importante è che la print stampa una stringa, e le stringhe sono in Python immutabili. Per cui, anche se è possibile, fare "taglia e cuci" con le stringhe per comporre la pagina di output, è abbastanza inefficiente e scomodo. Per ovviare, esiste un meccanismo di output analogo alla printf del C, che illustriamo con un esempio: >>> x = 1 >>> y = 'hello' >>> z = 64 >>> print "x=%d y=%s z=%c" % (x,y,z) La stringa che segue il print contiene delle specifiche di formato, composte dal % seguito da un carattere: d indica i numeri decimali, s le stringhe e c i caratteri. Le specifiche di formato vengono sostituite dai parametri che seguono l'operatore % dopo la stringa. Nell'esempio vediamo come il %d venga sostituito dal valore decimale di x, il %s dalla stringa y mentre il %c genera il carattere il cui codice ascii è contenuto nella variabile z (il codice ascii di '@' è appunto 64). Conclusioni Il Python sta crescendo molto ed occupa ormai un posto rispettabile nel mondo dei linguaggi di scripting, ormai quasi al pari del più noto Perl. In effetti, una chiara prova della diffusione del linguaggio è data da freshmeat.net, un sito che pubblica il nuovo freeware disponibile in rete: freshmeat riportava 10 nuovi package Perl recenti e ben 8 Python… Come dire, il linguaggio si avvicina in popolarità e diffusione al Perl e conquista ogni giorno nuovi cultori. Spero con questi articoli di aver contribuito alla sua diffusione, con lo scopo non di proporre un poco utile linguaggio per passare qualche momento di relax, ma uno ottimo strumento da utilizzare in pratica per risolvere ben precise categorie di problemi (nella fattispecie scripting Web, System Adminstration e supporto alla programmazione). Michele Sciabarrà, laureato in Scienze dell'Informazione, è direttore tecnico di SATORI Network Solutions, ditta marchigiana focalizzata su Java e le tecnologie di Internet. Programma in svariati linguaggi da 15 anni, e in Java da quando il linguaggio era in versione alpha. È stato tra gli espositori di applicazioni Java già alla 1st Italian Java Conference, ha scritto decine di articoli su Java e sviluppato numerose applicazioni in Java. Mantiene sul Web un corso online di Java in italiano (http://www.satorins.com/CorsoJava)
|