Programmazione 4 commenti

Gestione dei record in Python/2

di Michele Simionato

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:

ABCD
1234
5678
><&"

È 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!

Pubblicato il
28 Mag 2008
Tag

Commenti

  • Loris il 15 Giu 2008

    sbaglio o manca il metodo headtail

  • micheles il 15 Giu 2008

    Non sbagli, ma consideralo importato dalla puntata precedente ;)

  • gioby il 19 Ago 2008

    """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!! :D Come funziona esattamente?

  • Lawrence Oluyede il 19 Ago 2008

    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.

Screencast e videocorsi di programmazione
Stacktrace RSS Feed Stacktrace via E-mail
Hai idee per un articolo? Faccelo sapere!