Gestione dei record in Python/2

Nella scorsa puntata ho descritto i pregi e le virtù delle
namedtuple, un concetto che è stato introdotto nella libreria standard
di Python con la versione 2.6 (attualmente in alpha) ma che può essere
utilizzato proficuamente fin da
subito, semplicemente scaricando la ricetta di Raymond
Hettinger
. In questa puntata farò uso delle namedtuple per gestire
i record provenienti da un database a darò qualche consiglio su
come processare e come visualizzare tali record.


Estrarre tabelle da un database

L’approccio più semplice e meno invasivo per estrarre una tabella di
namedtuple da un database è quello definirsi una funzione
get_table_from_db analoga alla get_table discussa nella prima
parte, che estrae i dati dal cursore e dalla query SQL (assumo qui che
siate familiari con la gestione dei database in Python, ovvero con il
PEP 249 che definisce la cosiddetta DB API 2):

# easydb.py
from operator import itemgetter
from namedtuple import namedtuple

def get_table_from_db(cursor, query_templ, query_args=(), ntuple=None):
    if query_args:
        cursor.execute(query_templ, query_args)
    else:
        cursor.execute(query_templ)
    rows = cursor.fetchall()
    fields = map(itemgetter(0), cursor.description)
    Ntuple = ntuple or namedtuple('DBTuple', fields)
    yield Ntuple(*fields)
    for row in rows:
        yield Ntuple(*row)

if __name__ == '__main__': # test
    from sqlite3 import dbapi2
    conn = dbapi2.connect(':memory:')
    conn.execute('create table test(id integer, descr varchar)')
    conn.execute("insert into test values (1,'one')")
    conn.execute("insert into test values (2,'two')")
    for rec in get_table_from_db(conn.cursor(), 'select * from test'):
        print rec

Notate in particolare la riga

fields = map(itemgetter(0), cursor.description)

che estrae i nomi dei campi dall’attributo .description del
cursore (che è una lista di tuple) prendendo il primo elemento di ogni
tupla usando la funzione di utilità itemgetter del modulo
operator; si potrebbe ottenere lo stesso risultato con una list
comprehension fields = [x[0] for x in cursor.description] ma
itemgetter è la soluzione più ovvia.

Notate anche che sebbene questo esempio usi il database SQLite
perché è incluso di default con la
distribuzione di Python a partire dalla versione 2.5, potete
facilmente adattarlo a qualunque altro database.
Infine, notate che se lo schema è conosciuto a priori, potete passare
a get_table_from_db una namedtuple predefinita ed usare quella
invece di generarla automaticamente a partire dalla query.

Se eseguite lo script otterrete come risultato:

$ python easydb.py
DBTuple(id='id', descr='descr')
DBTuple(id=1, descr=u"one")
DBTuple(id=2, descr=u"two")

Un approccio di più alto livello

La DB API 2 è di livello piuttosto basso e molti preferiscono utilizzare
librerie più sofisticate; quella che va per la maggiore probabilmente è
SQLAlchemy, in cui l’esempio precedente potrebbe essere scritto come
segue:

# sa.py
from sqlalchemy import create_engine
from namedtuple import namedtuple

def get_table_from_db(engine, query_templ, query_args=(), ntuple=None):
    if query_args:
        result = engine.execute(query_templ, query_args)
    else:
        result = engine.execute(query_templ)
    Ntuple = ntuple or namedtuple('DBTuple', result.keys)
    yield Ntuple(*result.keys)
    for row in result:
        yield Ntuple(*row)

if __name__ == '__main__': # test
    e = create_engine('sqlite:///:memory:')
    e.execute('create table test(id integer, descr varchar)')
    e.execute("insert into test values (1,'one')")
    e.execute("insert into test values (2,'two')")
    for rec in get_table_from_db(e, 'select * from test'):
        print rec

Io personalmente non amo troppo SQLAlchemy ed in genere gli ORM,
per due motivi: sono troppo sofisticati (simple is better than complex)
e nascondono l’SQL al programmatore (explicit is better than implicit).
Ciò detto, sono il primo a dire che ci sarebbe molto bisogno di una
DB API 3 ufficiale, di più alto livello della DB API 2, senza per questo
essere un ORM. In pratica, mi piacerebbe avere un equivalente
dell’engine di SQLAlchemy nella libreria standard, e che il recordset
ritornato da una query fosse costituito da namedtuple, non da tuple
ordinarie.

Generare tabelle

Un lavoro comunissimo è quello di leggere dei dati da un database,
processarli e produrre come output una tabella di risultati. L’output
potrebbe essere un file CSV di numeri da usare per un grafico
oppure semplicemente una tabella HTML da
pubblicare nel sito aziendale. Un workflow tipico è il
seguente, da leggere dall’alto verso il basso:

<data source>
        |
        | get_table
        |
 <initial table>
        |
        | processor
        |
 <intermediate table>
        |
        | processor
        |
<final table>
        |
        | renderer
        |
 <output>

Il processore è un oggetto che prende
una tabella in ingresso e ritorna una tabella in uscita, eventualmente con
un numero di righe e/o di colonne diverso di quello in ingresso.
Siccome le tabelle sono degli oggetti iterabili, è naturale implementare
un processore in Python tramite un generatore che prendere
un iterabile e ritorna un iterabile.
In generale, vi possono essere più processori che agiscono uno dopo l’altro e
quindi più tabelle intermedie. L’ultimo processore ritorna la tabella
finale che viene successivamente convertita in una stringa e
salvata in un file, in formato CSV, HTML, XML o altro.

Per esempio, supponiamo di voler generare una tabella HTML.
In tal caso ci serve un (pre)processore che converte una tabella di
record astratti in una tabella di record concreti, che non sono
altro che sequenze di stringhe in cui i caratteri speciali
dell’HTML sono stati escaped; tale processore può essere
implementato come un semplice generatore:

def htmlescape(table):
    "Converts a table of records into a table of HTML-escaped records"
    for rec in table:
        yield [cgi.escape(str(field), quote=True) for field in rec]

Si noti che htmlescape è un processore del tutto generico che
non ha neppure bisogno che i record in ingresso siano delle namedtuple:
è sufficiente che siano delle sequenze generiche.

Il renderer finale può essere implementato come segue:

class HtmlTable(object):
    "Convert a sequence header+body into a HTML table"
    # this is just a pedagogic implementation, in a real implementation
    # you should not hard-code your css at the Python level.
    name = "noname"
    border = "1"
    summary = ""
    css = """\
    <style>
    tr.even { background-color: lightgreen }
    tr.odd { background-color: lightgray }
    th { background-color: lightblue }
    </style>
    """
    def __init__(self, header_plus_body):
        self.header, self.body = headtail(header_plus_body)

    def render(self):
        join = os.linesep.join
        templ = '''\
        %s
        <table id="%s" border="%s" summary="%s">
        %%s
        </table>''' % (self.css, self.name, self.border, self.summary)
        head, tail = headtail(self) # post-processed head and tail
        h = '<thead>\n%s\n</thead>\n' % join(head)
        b = '<tbody>\n%s\n</tbody>\n' % join(join(r) for r in tail)
        return templ % (h+b)

    def __iter__(self):
        yield ['<tr>'] + ['<th>%s</th>' % h for h in self.header] + ['</tr>']
        for r, row in enumerate(self.body):
            ls = ['<tr class="%s">' % ["even", "odd"][r % 2]]
            for col in row:
                ls.append('<td>%s</td>' % col)
            ls.append('</tr>')
            yield ls

Notate che HtmlTable può essere interpretato anche come un processore,
visto che HtmlTable(table) è un oggetto iterabile che ritorna
blocchi di codice HTML. Il metodo .render può essere pensato
come il renderizzatore di default, ma è possibile usare dei renderizzatori
più sofisticati, in almeno due modi:

  1. tramite l’ereditarietà, ovvero derivando una sottoclasse di HtmlTable
    e sovrascrivendo il metodo render;
  2. in maniera funzionale, usando HtmlTable come un processore e passando
    il suo output ad un renderizzatore completamente indipendente.

Entrambe le possibilità hanno dei pro e dei contro, ma
l’approccio funzionale è più indicato se lo scopo finale
è quello di disaccoppiare il codice. Inoltre, la composizione funzionale
è concettualmente più leggera di una gerarchia di ereditarietà.
Questo assicura semplicità e maggiore scalabilità a casi più complessi.

È banale verificare che il tutto funziona con un semplice test:

def test():
    page = """\
    <html>
    <head>
    </head>
    <body>
     %s
    </body>
    </html>
    """
    def get_test_table():
        return 'ABCD', '1234', '5678', '><&"'
    t = HtmlTable(htmlescape(get_test_table()))
    print >> file('output.html', 'w'), page % t.render()

In questo esempio get_test_table legge la tabella iniziale, htmlescape
è il processore e HtmlTable è il renderer. Eseguendo il test si ottiene
la tabella seguente:

A B C D
1 2 3 4
5 6 7 8
> < &

È chiaro che l’approccio che ho delineato in questo articolo è del tutto
generale e si applica direttamente anche ad altri casi; lascio come
esercizio scrivere un processore/renderizzatore che converte
in formato XML, Latex o CSV.

I lettori delle Avventure di un Pythonista in Schemeland avranno
riconosciuto l’inflenza della programmazione funzionale.
Non si tratta di un caso fortuito: io
sono dell’idea che la conoscenza di linguaggi non-mainstream sia molto
utile anche quando si programma esclusivamente in linguaggi
mainstream. In particolare, la conoscenza dei linguaggi funzionali vi
permette di mettere in dubbio concetti che paiono dogmi indiscutibili
in certi ambienti (tipo la “bontà” della programmazione ad oggetti) e
di aprirvi a design alternativi. Non è un caso neppure il fatto che
Python (che fin dall’inizio non è mai stato un linguaggio a oggetti
bigotto alla Java) si stia muovendo sempre più verso soluzioni
funzionali, sia nel linguaggio core ( list comprehensions,
generator expressions, tuple unpacking, ecc) che nelle librerie
(itertools, namedtuple, ecc).

La miniserie non finisce qui: c’è ancora molto da dire sul
problema della visualizzazione di tabelle e a questo argomento
dedicheremo interamente la terza ed ultima parte. Ci vediamo
alla prossima, happy hacking!

Comments

  1. sbaglio o manca il metodo
    headtail

  2. micheles says:

    Non sbagli, ma consideralo importato dalla
    puntata precedente 😉

  3. “””Ntuple = ntuple or namedtuple(‘DBTuple’, result.keys)”””
    Domanda: cosa é ntuple? il nome con cui verranno chiamate le named tuple in python 2.6?
    Onestamente non sapevo che si potessero instanziare degli oggetti in questo modo, con una espressione condizionale: geniale!! 😀 Come funziona esattamente?

  4. Lawrence Oluyede says:

    ntuple è il nome di uno degli argomenti della funzione.

    In pratica puoi passare una named tuple dall’esterno, se non lo fai ne istanzia una.

    or viene usato per fare in modo che alla variabile venga assegnato il primo dei due valori che ha un valore “true”. Se non fornisci una namedtuple come argomento la variabile ntuple rimarrà None e quindi per side effect viene eseguita l’istanziazione della nuova namedtuple e viene assegnata a tale variabile.

Policy per i commenti: Apprezzo moltissimo i vostri commenti, critiche incluse. Per evitare spam e troll, e far rimanere il discorso civile, i commenti sono moderati e prontamente approvati poco dopo il loro invio.