5.8. Introduzione al modulo dialect.py

La classe Dialectizer è una semplice (e un po' stupida) specializzazione della classe BaseHTMLProcessor. Questa classe sottopone un blocco di testo ad una serie di sostituzioni, ma al contempo fa in modo che tutto ciò che è racchiuso in un blocco <pre>...</pre> rimanga inalterato.

Per gestire i blocchi <pre>, si definiscono due nuovi metodi in Dialectizer: start_pre ed end_pre.

Esempio 5.16. Gestire tag specifici

    def start_pre(self, attrs):             1
        self.verbatim += 1                  2
        self.unknown_starttag("pre", attrs) 3

    def end_pre(self):                      4
        self.unknown_endtag("pre")          5
        self.verbatim -= 1                  6
1 Il metodo start_pre è chiamato ogni volta che SGMLParser trova un tag <pre> nel codice HTML di partenza. Più avanti, scopriremo esattamente perché succede questo. Il metodo accetta un singolo parametro, attrs, che contiene gli attributi del tag (se ce ne sono). Il parametro attrs è una lista di coppie chiave/valore, proprio come quella accettata dal metodo unknown_starttag.
2 Nel metodo reset, viene inizializzato un attributo che serve come contatore per i tag <pre>. Ogni volta che si incontra un tag <pre>, si incrementa il contatore; ogni volta che si incontra un tag </pre>, si decrementa il contatore. Si potrebbe usarlo semplicemente come flag, settandolo ad 1 e resettarndolo a 0, ma è altrettanto facile gestirlo come contatore, ed in questo modo si copre il caso, insolito ma possibile, in cui ci siano tag <pre> annidati. Fra poco vedremo come questo contatore torni utile.
3 Questo è l'unica tipologia di elaborazione specifica che applichiamo ai tag <pre>. Ora si passa la lista degli attributi al metodo unknown_starttag, così che si possa svolgere l'elaborazione standard.
4 Il metodo end_pre è chiamato ogni volta che la classe SGMLParser incontra un tag </pre>. Dato che i tag di chiusura non contengono attributi, il metodo non accetta parametri.
5 Per prima cosa, si vuole eseguire l'elaborazione standard, esattamente come per gli altri tag di chiusura.
6 Quindi, si decrementa il nostro contatore, per indicare che il blocco <pre> è stato chiuso.

A questo punto, vale la pena di tuffarsi un po' più a fondo nel codice di SGMLParser. Ho ripetutamente affermato (e finora mi avete creduto sulla parola) che SGMLParser cerca i singoli tag e chiama per ognuno di loro il metodo specifico, se esiste. Per esempio, abbiamo appena visto come definire i metodi start_pre ed end_pre per gestire i tag <pre> e </pre>. Ma come succede? Bene, non si tratta di magia, ma solo di buon codice Python.

Esempio 5.17. SGMLParser

    def finish_starttag(self, tag, attrs):               1
        try:                                            
            method = getattr(self, 'start_' + tag)       2
        except AttributeError:                           3
            try:                                        
                method = getattr(self, 'do_' + tag)      4
            except AttributeError:                      
                self.unknown_starttag(tag, attrs)        5
                return -1                               
            else:                                       
                self.handle_starttag(tag, method, attrs) 6
                return 0                                
        else:                                           
            self.stack.append(tag)                      
            self.handle_starttag(tag, method, attrs)    
            return 1                                     7

    def handle_starttag(self, tag, method, attrs):      
        method(attrs)                                    8
1 A questo punto, SGMLParser ha già trovato un tag di apertura ed ha analizzato la lista degli attributi. L'unica cosa che rimane da fare è di stabilire se c'è uno specifico metodo per gestire il tipo di tag trovato, oppure se è necessario chiamare il metodo predefinito (unknown_starttag).
2 La “magia” di SGMLParser non è altro che la nostra vecchia amica, la funzione getattr. Quello che forse non vi è ancora chiaro è che getattr è capace di trovare anche i metodi definiti nei “discendenti” di un oggetto, oltre che nell'oggetto stesso. Qui l'oggetto è self, l'istanza dell'oggetto corrente. Quindi se la variabile tag vale 'pre', questa chiamata a getattr cercherà un metodo start_pre nell'istanza corrente, che è un'istanza della classe Dialectizer.
3 La funzione getattr solleva una eccezione AttributeError se il metodo che sta cercando non esiste nell'oggetto (inclusi i suoi “discendenti”), ma questo ci sta bene perché abbiamo racchiuso la chiamata a getattr in un blocco try...except che intercetta esplicitamente l'eccezione AttributeError.
4 Dato che non è stato trovato un metodo start_xxx, qui si cerca anche un metodo do_xxx, prima di rinunciare. Questo modo alternativo di chiamare i metodi che gestiscono i tag, è di solito adoperato per quei tag che non si usano a coppie (“standalone”, ndt), come <br>, che non ha nessun corrispondente tag di chiusura. Tuttavia potete usare l'uno o l'altro metodo; così potete osservare SGMLParser lavorare contemporaneamente per ciascuno dei tag. Tuttavia, non si dovrebbero definire sia il metodo start_xxx che il metodo do_xxx per gestire lo stesso tag; in un caso del genere, solo il metodo start_xxx sarebbe chiamato.
5 Un'altra eccezione AttributeError, il che significa che la chiamata a getattr è fallita anche con do_xxx. Dato che non abbiamo trovato né un metodo start_xxx né un metodo do_xxx corrispondente al tag, qui si intercetta l'eccezione e si chiama il metodo predefinito, unknown_starttag.
6 Ricordate che i blocchi try...except possono anche avere una clausola else, che è eseguita se nessuna eccezione è sollevata all'interno del blocco try...except. Ovviamente, questo significa è stato trovato un metodo do_xxx corrispondende al tag, che quindi viene invocato.
7 A proposito, non preoccupatevi dei diversi valori di ritorno; in teoria essi dovrebbero indicare qualcosa, ma in pratica non sono mai usati. Non vi preccupate neanche dell'istruzione self.stack.append(tag); la classe SGMLParser tiene traccia internamente del fatto che i vostri tag di apertura e chiusura siano opportunamente bilanciati, ma neanche questa informazione viene in pratica utilizzata. In teoria, potreste usare questo modulo per verificare che tutti i tag siano bilanciati, ma probabilmente non ne vale la pena, e comunque la cosa va oltre gli scopi di questo capitolo. Abbiamo cose più importanti di cui preoccuparci in questo momento.
8 I metodi start_xxx e do_xxx non vengono chiamati direttamente; il tag, il metodo e gli attributi dei tag vengono passati a questa funzione, handle_starttag, di modo che le classi derivate da questa possano ridefinire tutti i metodi, questo e gli altri che gestiscono i tag di apertura. Noi non abbiamo bisogno di un tale livello di controllo, quindi ci limitiamo a lasciare che questo metodo faccia ciò che deve, vale a dire chiamare il metodo (start_xxx o do_xxx) con la lista degli attributi. Ricordate, un metodo è una funzione, ottenuta come valore di ritorno della chiamata a getattr, e le funzioni sono oggetti (lo so che vi state stancando di sentirlo, e prometto che smetterò di dirlo non appena avremo esaurito i modi di usare la cosa a nostro vantaggio). A questo punto, l'oggetto funzione è passato al metodo di attivazione (handle_starttag) come argomento, e questi a sua volta chiama la funzione. A questo punto, non è neccessario sapere che funzione è, qual'è il suo nome o dov'è definita; l'unica cosa che occorre sapere sulla funzione è che va invocata con un unico argomento, attrs.

Ora possiamo tornare al programma inizialmente previsto: Dialectizer. Lo avevamo lasciato al punto in cui stavamo per definire metodi specifici per gestire i tag <pre> e </pre>. C'è solamente una cosa che rimane da fare, ed è elaborare i blocchi di testo con le nostre sostituzioni predefinite. Per fare ciò dobbiamo ridefinire il metodo handle_data.

Esempio 5.18. Ridefinizione del metodo handle_data

    def handle_data(self, text):                                         1
        self.pieces.append(self.verbatim and text or self.process(text)) 2
1 Il metodo handle_data è invocato con un solo argomento, il testo da elaborare.
2 Nella classe base BaseHTMLProcessor, il metodo handle_data aggiungeva semplicemente il testo di input al buffer di output, self.pieces. Nel nostro caso la logica è solo leggermente più complessa. Se ci si trova all'interno di un blocco <pre>...</pre>, allora la variablie self.verbatim avrà un valore maggiore di 0, ed in questo caso si vuole aggiungere il testo al buffer di output senza modificarlo. Altrimenti, viene chiamato un metodo separato per eseguire le sostituzioni e quindi il risultato viene aggiunto al buffer di output. In Python, lo si può fare con una sola linea di codice, usando il trucchetto and-or.

Siamo vicini a capire completamente Dialectizer. L'unico punto mancante è, nello specifico, che tipo di sostituzioni vengono effettuate. Se conoscete qualcosa del linguaggio Perl, sapete che laddove sono richieste delle complesse sostituzioni di testo, l'unico vero strumento per farlo sono le espressioni regolari.