Programmazione 4 commenti
Gestione dei record in Python/2
diNella 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:
- tramite l'ereditarietà, ovvero derivando una sottoclasse di
HtmlTablee sovrascrivendo il metodorender; - in maniera funzionale, usando
HtmlTablecome 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!
- Pubblicato il
- 28 Mag 2008
- Tag


sbaglio o manca il metodo headtail
Non sbagli, ma consideralo importato dalla puntata precedente ;)
"""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?
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.