Alcune applicazioni possono beneficiare dell'accesso diretto all'albero di analisi. La parte rimanente di questa sezione dimostrerà come l'albero di analisi fornisca l'accesso alla documentazione del modulo definita nella docstring, senza la necessità che il codice sotto analisi venga caricato in un interprete in esecuzione tramite la direttiva import. Questo risulta molto utile nell'analisi di codice inaffidabile.
In generale, l'esempio dimostrerà come l'albero di analisi possa venire attraversato per recuperare informazioni interessanti. Sono state sviluppate due funzioni ed un insieme di classi per permettere un accesso via programma alle funzioni di alto livello ed alle definizioni di classi fornite dal modulo. Le classi estraggono le informazioni dall'albero di analisi e ne forniscono l'accesso attraverso ad un livello semantico utile, una funzione fornisce una semplice funzionalità di corrispondenze tra modelli a basso livello, mentre l'altra funzione definisce un'interfaccia ad alto livello alle classi gestendo le operazioni sui file secondo le necessità del chiamante. Tutti i file sorgenti qui citati che non siano parte dell'installazione di Python sono situati nella directory Demo/parser/ della distribuzione.
La natura dinamica di Python consente al programmatore una grande flessibilità, ma la maggior parte dei moduli ne richiedono solamente una piccola parte quando si tratta di definire delle classi, delle funzioni o dei metodi. In questo esempio, le sole definizioni che verranno considerate sono quelle definite al livello più alto del loro contesto, ad esempio una funzione definita da un'istruzione def alla colonna zero di un modulo, ma non una funzione definita all'interno di un ramo di un costrutto if ... else, anche se ci sono dei buoni motivi per far questo in certe situazioni. L'annidamento di definizioni verrà gestito dal codice sviluppato nell'esempio.
Per costruire i metodi di estrazione del livello più alto, dobbiamo conoscere l'aspetto della struttura dell'albero di analisi e quanto di tale struttura è utile ai nostri scopi. Python utilizza un albero di analisi abbastanza profondo, quindi esistono numerosi nodi intermedi. È importante leggere e capire la grammatica formale usata da Python. Questa viene definita nel file Grammar/Grammar nella distribuzione. Considerate il caso di interesse più semplice quando si ricerca una docstrings: un modulo che contenga solamente una doctring e nient'altro (vedete il file docstring.py).
"""Un po' di documentazione """
Utilizzando l'interprete per dare un'occhiata all'albero di analisi, troveremo una sconcertante massa di numeri e parentesi, con la documentazione sepolta profondamente in mezzo a tuple annidate.
>>> import parser >>> import pprint >>> ast = parser.suite(open('docstring.py').read()) >>> tup = ast.totuple() >>> pprint.pprint(tup) (257, (264, (265, (266, (267, (307, (287, (288, (289, (290, (292, (293, (294, (295, (296, (297, (298, (299, (300, (3, '"""Un po' di documentazione.\n"""'))))))))))))))))), (4, ''))), (4, ''), (0, ''))
I numeri come primo elemento di ogni nodo nell'albero costituiscono i tipi dei nodi; si riferiscono direttamente a simboli terminali e non terminali della grammatica. Sfortunatamente, questi vengono indicati nella rappresentazione interna con degli interi e le strutture Python generate non li modificano. Comunque i moduli symbol e token forniscono nomi simbolici per i tipi dei nodi e dizionari che mappano per ogni tipo di nodo gli interi in nomi simbolici .
Nell'output presentato sopra, la tupla più esterna contiene quattro
elementi: l'intero 257
e tre tuple addizionali. Il tipo di
nodo 257
ha il nome simbolico file_input. Ognuna
delle tuple più interne contiene un intero come primo elemento; questi
interi, 264
, 4
e 0
, rappresentano rispettivamente
i tipi di nodi stmt, NEWLINE e
ENDMARKER. Fate attenzione che questi valori potrebbero
variare a seconda della versione di Python in uso; consultate
symbol.py e token.py per i dettagli su come vengono
mappati. Dovrebbe essere abbastanza chiaro che il nodo più esterno
referenzia principalmente la sorgente di ingresso piuttosto che il
contenuto del file e può essere trascurato per il momento. Il nodo
stmt è molto più interessante. In particolare, tutte le
docstring si trovano in sotto alberi che vengono formati nella stessa
maniera in cui viene formato questo nodo, con la sola differenza della
stringa stessa. L'associazione tra la docstring in tale albero e
l'entità definita (classe, funzione o modulo) che questa descrive
viene data dalla posizione del sotto albero della docstring
all'interno dell'albero che definisce la struttura descritta.
Sostituendo la docstring reale con qualcosa che rappresenti una
componente variabile dell'albero, può permettere ad un semplice approccio
di corrispondenza fra modelli di controllare per ogni dato sotto albero
l'equivalenza delle docstring con modelli generici. Dato che
l'esempio mostra l'estrazione delle informazioni, possiamo richiedere
in maniera sicura che l'albero sia in forma di tupla piuttosto che in
forma di lista, permettendo ad una semplice rappresentazione di una variabile
di essere nella forma ['nome_variabile']
. La
corrispondenza tra modelli può venire implementata con una semplice funzione
ricorsiva che restituisca un valore booleano ed un dizionario di
corrispondenze tra nomi di variabile e valori (vedete il file
example.py).
from types import ListType, TupleType def match(pattern, data, vars=None): if vars is None: vars = {} if type(pattern) is ListType: vars[pattern[0]] = data return 1, vars if type(pattern) is not TupleType: return (pattern == data), vars if len(data) != len(pattern): return 0, vars for pattern, data in map(None, pattern, data): same, vars = match(pattern, data, vars) if not same: break return same, vars
Utilizzando questa semplice rappresentazione per le variabili sintattiche e i tipi di nodo simbolici, il modello per i sotto alberi di docstring candidati diventa abbastanza leggibile (vedete il file example.py).
import symbol import token DOCSTRING_STMT_PATTERN = ( symbol.stmt, (symbol.simple_stmt, (symbol.small_stmt, (symbol.expr_stmt, (symbol.testlist, (symbol.test, (symbol.and_test, (symbol.not_test, (symbol.comparison, (symbol.expr, (symbol.xor_expr, (symbol.and_expr, (symbol.shift_expr, (symbol.arith_expr, (symbol.term, (symbol.factor, (symbol.power, (symbol.atom, (token.STRING, ['docstring']) )))))))))))))))), (token.NEWLINE, '') ))
Utilizzando la funzione match() con questo modello, è facile estrarre il modulo docstring dall'albero di analisi creato in precedenza:
>>> found, vars = match(DOCSTRING_STMT_PATTERN, tup[1]) >>> found 1 >>> vars {'docstring': '"""Some documentation.\n"""'}
Una volta che i dati specifici possono essere estratti da una
locazione dove ci si aspetta siano presenti, la domanda di dove si
possano trovare le informazioni necessita di una risposta. Quando
ci si occupa delle docstring, la risposta è semplice: la docstring è il
primo nodo stmt in un blocco di codice (tipi di nodo file_input
o suite). Un modulo consiste in un singolo
nodo file_input, le definizioni di classe e di funzione
contengono ognuna esattamente un nodo suite. Classi e
funzioni vengono prontamente identificate come sotto alberi di nodi dei
blocchi di codice che iniziano con
(stmt, (compound_stmt, (classdef, ...
o
(stmt, (compound_stmt, (funcdef, ...
. Notate che questi
sotto alberi non possono essere verificati da match() visto che
essa non supporta la ricerca su nodi fratelli multipli
senza riguardo per il numero. Per superare questa limitazione può
venire usata una funzione di ricerca più elaborata, ma questo è
sufficiente per l'esempio.
Avuta l'abilità di determinare se un'istruzione possa essere una docstring ed estarre la stringa reale dall'istruzione, è necessario compiere delle operazioni di percorrimento dell'albero di analisi per un intero modulo, estrarre l'informazione sui nomi definiti in ogni contesto del modulo ed associare ogni docstring ai nomi. Il codice che esegue questo lavoro non è complicato, ma necessita di alcune spiegazioni.
Questa interfaccia pubblica per le classi è chiara e dovrebbe essere probabilmente un po' più flessibile. Ogni blocco ``major'' del modulo viene descritto da un oggetto che fornisce alcuni metodi per la ricerca e da un costruttore che accetta come minimo il sotto albero dell'albero di analisi completo che lo descrive. Il costruttore ModuleInfo accetta un parametro name facoltativo poiché esso non può determinare altrimenti il nome del modulo.
Le classi pubbliche includono ClassInfo, FunctionInfo e ModuleInfo. Tutti gli oggetti forniscono i metodi get_name(), get_docstring(), get_class_names() e get_class_info(). Gli oggetti ClassInfo supportano get_method_names() e get_method_info() mentre le altre classi forniscono get_function_names() e get_function_info().
All'interno di ogni forma di blocco di codice che le classi pubbliche rappresentano, la maggior parte delle informazioni si trova nella stessa forma e vi si accede nello stesso modo, con la differenza che alle funzioni definite al livello più alto in queste classi ci si riferisce come ``methodi''. Da quando la differenza nella terminologia riflette una reale distinzione semantica dalle funzioni definite fuori dalla classe, l'implementazione necessita di mantenere questa distinzione. Da questo punto in poi, la maggior parte delle funzionalità delle classi pubbliche può venire implementata in una classe base comune, SuiteInfoBase, con gli accessori per le funzioni e le informazioni sui metodi forniti altrove. Notate che c'è una sola classe che rappresenta le informazioni su funzioni e metodi; questo è parallelo all'uso dell'istruzione def per definire entrambi i tipi di elemento.
La maggior parte delle funzioni accessorie vengono dichiarate in SuiteInfoBase e non necessitano di essere sovrascritte nelle sotto classi. Più importante, l'estrazione della maggior parte delle informazioni da un albero di analisi viene gestita attraverso un metodo chiamato dal costruttore SuiteInfoBase. Il codice di esempio per la maggior parte delle classi è chiaro quando viene letto dal lato della grammatica formale, ma il metodo che crea ricorsivamente nuovi oggetti informativi richiede un'ulteriore verifica. Qui ci sono le parti rilevanti della definizione di SuiteInfoBase da example.py:
class SuiteInfoBase: _docstring = '' _name = '' def __init__(self, tree = None): self._class_info = {} self._function_info = {} if tree: self._extract_info(tree) def _extract_info(self, tree): # estrazione della docstring if len(tree) == 2: found, vars = match(DOCSTRING_STMT_PATTERN[1], tree[1]) else: found, vars = match(DOCSTRING_STMT_PATTERN, tree[3]) if found: self._docstring = eval(vars['docstring']) # rileva le definizioni for node in tree[1:]: found, vars = match(COMPOUND_STMT_PATTERN, node) if found: cstmt = vars['compound'] if cstmt[0] == symbol.funcdef: name = cstmt[2][1] self._function_info[name] = FunctionInfo(cstmt) elif cstmt[0] == symbol.classdef: name = cstmt[2][1] self._class_info[name] = ClassInfo(cstmt)
Dopo l'inizializzazione di alcuni stati interni, il costruttore chiama il metodo _extract_info(). Questo metodo esegue la verifica dell'estrazione dell'informazione che avviene nell'intero esempio. L'estrazione ha due fasi distinte: la localizzazione della docstring nell'albero di analisi passato e la scoperta di definizioni aggiuntive all'interno del blocco di codice rappresentato dall'albero di analisi.
L'if iniziale determina se la suite annidata sia per la ``forma breve'' o per la ``forma lunga''. La forma abbreviata viene usata quando il blocco di codice è sulla stessa riga della sua definizione, come in
def square(x): "Eleva al quadrato un argomento."; return x ** 2
mentre la forma lunga utilizza un blocco indentato e permette l'annidamento delle definizioni:
def make_power(exp): "Crea una funzione che eleva un argomento all'esponente `exp'." def raiser(x, y=exp): return x ** y return raiser
Quando viene usata la forma abbreviata, il blocco di codice può contenere una docstring come primo, e possibilmente unico, elemento small_stmt. L'estrazione di una docstring simile è un pò differente e richiede soltanto una parte del modello completo usato nel caso più comune. Appena implementata, la docstring verrà trovata soltanto se c'è un singolo nodo small_stmt nel nodo small_stmt. Dal momento che la maggior parte delle funzioni e dei metodi che usano la forma abbreviata non forniscono una docstring, ciò potrebbe essere considerato sufficiente. L'estrazione della docstring procede usando la funzione match() come descritto sopra ed il valore della docstring viene conservato come attributo dell'oggetto SuiteInfoBase.
Dopo l'estrazione della docstring, si attiva un semplice algoritmo per rintracciare la definizione sui nodi stmt del nodo suite. Il caso speciale della forma abbreviata non viene testato; dal momento che non ci sono nodi stmt nella forma abbreviata, l'algoritmo salta tacitamente il singolo nodo simple_stmt e giustamente non rileva alcuna definizione annidata.
Ogni istruzione nel blocco di codice viene classificata come una definizione di classe, una definizione di funzione o metodo, o come qualcos'altro. Per le istruzioni di definizione, viene estratto il nome dell'elemento definito e creato un oggetto che rappresenta in modo appropriato la definizione, con il sotto albero definente passato come argomento al costruttore. Tali oggetti vengono conservati in variabili d'istanza e possono venire recuperati per nome utilizzando i metodi d'accesso appropriati.
Le classi pubbliche forniscono qualsiasi accesso necessario che sia più specifico di quelli forniti dalla classe SuiteInfoBase, ma l'algoritmo di estrazione effettivo rimane comune a tutte le forme di blocco di codice. Una funzione d'alto livello può essere usata per estrarre l'insieme completo di informazioni da un file sorgente (vedete il file example.py).
def get_docs(fileName): import os import parser source = open(fileName).read() basename = os.path.basename(os.path.splitext(fileName)[0]) ast = parser.suite(source) return ModuleInfo(ast.totuple(), basename)
Questa funzione fornisce un'interfaccia per la documentazione di un modulo di facile utilizzo. Se l'informazione richiesta non viene estratta dal codice di questo esempio, il codice può venire esteso in punti ben definiti per fornire ulteriori funzionalità.
Vedete Circa questo documento... per informazioni su modifiche e suggerimenti.