Tuesday, May 19, 2015

Intermezzo su list comprehensions e generators

Ho iniziato a usare Python circa 3 anni fa, dopo aver passato anni a scrivere codice in C++, Java e Visual Basic. All'inizio scrivevo come ero stato sempre abituato, con cicli for e while, scrivendo funzioni e un po' di programmazione per oggetti. Python mi ha fatto conoscere tipologie dai dati che prima non usavo, come le liste, le tuple, i dizionari e i set. C'è voluto altro tempo per capire che Python è un linguaggio con una ricca scelta di strumenti di programmazione che, grazie alla necessità di velocizzare l'esecuzione di alcune tipologie di software, mi ha costretto a considerare scelte differenti da quelle che caratterizzavano lo stile precedente. Se inizi a lavorare con le list comprehensions e i generatori ci vuole comunque un po' di tempo per capire che le puoi usare dappertutto, al posto di cicli for e minimizzando lo storage intermedio di dati. Oggi faccio una pausa nell'elaborazione del software per il calcolo astronomico e parlo un po' di questi strumenti.

Gli esempi che si trovano nei manuali sono utili per afferrare l'idea, ma sono troppo semplici per essere esaustivi delle possibilità che i metodi funzionali offrono, per cui ho pensato di scrivere un programmino d'esempio che complica un po' le cose ma spero faccia capire meglio come si puo' procedere.

Se aprite una console Python e scrivete:

import this

viene fuori un Easter Egg (Uovo di Pasqua), che è il seguente (per curiosità è la PEP 20):

The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

Nella comunità Python questa piccola poesia è nota e citata quanto i Monty Python (che hanno dato il nome al linguaggio), e trovate spesso piccole citazioni che vanno a braccetto con spam e eggs e altre divertenti citazioni dei comici inglesi. Mi interessa farvi notare l'enfasi che viene data alla leggibilità del codice, alla sua semplicità costruttiva, e alla ricerca dell'unica soluzione che risulta ovvia (potremmo dire pythonica).

Il piccolo esercizio che segue discende da un software che ho scritto per gestire la mia spesa domestica, visto che la mia banca online mi permette di esportare in formato Excel un batch di operazioni bancarie. Python ha, ovviamente, delle belle librerie di terze parti per leggere file nel formato xls di Excel, ma preferisco, per vecchia abitudine, usare i file csv, che hanno il pregio della leggibilità diretta come testi.

Dovendo assemblare vari pezzi (il sito della mia banca non mi permette di estrarre tutto l'archivio storico in un colpo solo, per cui sono costretto a unire degli estratti parziali), mi sono posto il problema di come eliminare tutte le linee di intestazione e commento e ridurmi alle sole linee che contengono le operazioni bancarie.

Per avere un codice più essenziale ho usato estesamente le list comprehension e i generatori, come faccio sempre allego il codice poche righe alla volta e le commento,

from datetime import date, timedelta
import re, string
from random import randrange, shuffle

# helper functions:
# rand_string() generates a random shuffled string from all ascii chars
# europ_date converts a date in a string conforming to european format

def rand_shuffled_string():
    all_letters = list(string.ascii_letters)
    shuffle(all_letters)
    return "".join(all_letters)

def european_date(y,m,d, diff):
    return (date(y,m,d) + timedelta(diff)).strftime("%d/%m/%Y")

In breve: importo alcuni moduli essenziali: date e timedelta da datetime per la gestione della data, re per le regular expressions che uso più avanti, string per generare dei set di caratteri ascii dalle funzioni built-in e randrange e shuffle dal modulo random, per generare delle stringhe pseudocasuali.

Le due funzioni helper, banalmente, servono a creare una stringa pseudocasuale e una stringa per la data in formato europeo.

# To create a text file on hard disk we use a list generator
# to generate a text file where each row contains a date and a string

file_out = open("prova_gen.dat","w")
[file_out.write(
    "#".join(
        (european_date(2000,1,1,t),
         rand_shuffled_string(),
         '\n')
        )
    )
    if randrange(0,100) < 90
    else file_out.write("<----------------------- header\n")
    for t in xrange(0,3650,15)
]
file_out.close()

La list comprehension è formata da una funzione applicata ad una o più variabili che entrano in un ciclo e puo' comprendere un filtro di tipo if ... else. Nel caso specifico, applico la funzione helper european_date() alla variabile t che rappresenta lo scarto temporale tra la data di inizio e l'estremo del ciclo for. In pratica creo una data in formato europeo ogni quindici giorni a partire dal 1 gennaio 2000, la associo ad una stringa pseudocasuale e salvo la riga su disco se un numero causale tra 0 e 100 rimane compreso tra 0 e 90, quindi per il 90% dei casi, altrimenti scrivo sul file una pseudo riga di intestazione.

E' interessante notare che l'inclusione di funzioni esterne da me create, quindi presenti nel namespace, mi permette di creare list comprehension molto elaborate ma ancora molto leggibili.

# reload the file just written, filtering rows not begininng with a date
pattern = '\d{2}\/\d{2}\/\d{4}'
less_ = (i for i in open("prova_gen.dat") if re.match(pattern, i) != None)
all_set = (i.split("#") for i in less_)

# generates a list of tuples with date and event 
events = [(i[0], i[1]) for i in all_set]
# prints the first 10 tuples
print events[:10]

Nelle ultimo righe faccio il lavoro di rilettura del file e di filtraggio delle righe. Vorrei farvi notare come la funzione open(<file>) funziona come un generatore, mettendo a disposizione, uno alla volta, i contenuti del file in forma di righe singole. Viene ciclato tutto il file per estrarre le sole righe che contengono una data (a questo scopo uso l'espressione regolare del pattern, che corriponde ad una definita sequenza di caratteri). Il tutto, anziché come in precedenza, in una lista comprehension, in un generatore di espressioni, che quindi non genera una lista e, di fatto, non impegna memoria. Se provo a stampare la variabile less_ l'interprete Python mi risponde <generator object at 0x7f2f39985b90>, quindi in questa fase non ho ancora nulla di leggibile. Idem per la variabile all_set, che splitta ogni singola riga usando il separatore # che ho definito in fase di creazione del file

Infine la variabile event è generata attraverso una list comprehension, quindi è una lista, contenente tuple di due elementi (data e stringa pseudocasuale), che posso stampare o utilizzare altrimenti come insieme di dati. Si noti che, in tutto questo procedimento, è solo alla fine che viene generata una variabile che impegna memoria. Non ho dovuto infatti usare storage intermedi, pur avendo scomposto il procedimento in più fasi. In questo modo spero di avervi mostrato un po' di programmazione funzionale e di lazy evaluation (tipico dei generatori). Si dice lazy (pigro) perchè non pretende di buttare fuori l'output tutto insieme, ma fornisce un risultato alla volta quando richiesto da un'altra funzione o istruzione.

Il prossimo post ritorna sulle VSOP2013 e sull'elaborazione delle matrici di dati che abbiamo reso disponibili. Sarà un'occasione per riprendere il discorso sull'uso dei generatori. Come vedremo, sono anche una delle possibilità tecniche offerte per la velocizzazione del codice.

No comments:

Post a Comment

How to create a virtual linux machine with qemu under Debian or Ubuntu with near native graphics performance

It's been a long time since my latest post. I know, I'm lazy. But every now and then I like to publish something that other people c...