18.1.6.2 Recupero delle informazioni

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.