Programmare in Python/Qt - parte 1
posted in python, programmazione, qt |Il Python è un linguaggio di scripting moderno ed evoluto, dotato di librerie standard molto complete e numerosissime librerie di terze parti per fare praticamente di tutto. In virtù di ciò è possibile sviluppare applicazioni Python molto rapidamente e questo lo rende un linguaggio ideale, fra le altre cose, per la creazione di prototipi. Le Qt sono librerie C++ per la creazione di interfacce grafiche. Sono rilasciate con licenza GPL dalla Trolltech e sono le fondamenta del desktop manager KDE. Sono disponibili dei binding delle Qt per Python, chiamati PyQt, grazie ai quali è possibile usare Python e Qt insieme. Python e Qt sono disponibili su vari sistemi, inclusi GNU/Linux e Windows, e rappresentano perciò una vera soluzione multipiattaforma.
Per esplorare le potenzialità del binomio Python/Qt andremo a sviluppare un’applicazione didattica, una semplice calcolatrice statistica per calcolare gli errori sulle misurazioni di grandezze fisiche. Dato un insieme di valori, che rappresentano le misurazioni di una data grandezza, la nostra calcolatrice ci fornirà informazioni come media, deviazione standard, errore medio, ecc. Niente di trascendentale, ma abbastanza per usare Python e Qt in modo non banale. Nel prosieguo daremo per scontata una conoscenza dei concetti base sia del Python che delle Qt, che dispongono di ottima documentazione introduttiva qui e qui.
Per organizzare al meglio la nostra applicazione la suddivideremo in due parti fondamentali: un modello con la logica della calcolatrice e un’interfaccia utente attraverso cui manipolare il modello e mostrare i risultati.
Il modello
Il Python offre nativamente diversi tipi di collezione per contenere gli oggetti, fra i quali uno che fa proprio al caso nostro: la lista. In Python le liste sono sequenze dinamiche di oggetti, di qualunque tipo: possiamo quindi utilizzare una lista per contenere i valori delle misurazioni. In un modulo poi definiamo le varie operazioni da eseguire sulla lista per calcolare i dati di nostro interesse.
“”" Funzioni che eseguono calcoli statistici su sequenze di numeri. “”" from math import sqrt def media(sequence): “”"Calcola il valore medio della sequenza.”"” return sum(sequence) / len(sequence) def errore_semplice_medio(sequence): “”"Calcola l’errore semplice medio della sequenza.”"” med = media(sequence) return sum([abs(x-med) for x in sequence]) / len(sequence) def varianza(sequence): “”"Calcola la varianza della sequenza.”"” med = media(sequence) return sum([(x-med)**2 for x in sequence]) / len(sequence) def deviazione_standard(sequence): “”"Calcola la deviazione standard della sequenza.”"” return sqrt(varianza(sequence)) def frequenze(sequence): “”"Calcola la frequenza dei valori nella sequenza e li restituisce in forma di dizionario {valore:frequenza}.”"” valori = set(sequence) return dict(zip(valori, [sequence.count(v) for v in valori]))
Ogni funzione prende come argomento una lista, supponendo che contenga oggetti di tipo float. Il Python, da buon linguaggio di scripting, non è tipato; l’interprete non effettua alcun controllo sui tipi, che è lasciato al programmatore. Questa apparente debolezza è in realtà una dei vantaggi che derivano dall’uso di linguaggi di scripting anziché di linguaggi di sistema fortemente tipati come C++ o Java. Un linguggio tipato richiede uno sforzo di programmazione maggiore a fronte di un controllo di correttezza già a tempo di compilazione, mentre un linguaggio non tipato permette uno sviluppo molto più rapido segnalando eventuali errori a runtime. Questo fatto, unito alle funzioni built-in e agli idiomi utilizzabili con le liste, fa che si possano esprimere con una o due righe calcoli anche complessi. Ad esempio nella funzione media() definita come
def media(sequence): “”"Calcola il valore medio della sequenza.”"” return sum(sequence) / len(sequence)
calcoliamo la somma degli elementi della lista con sum(), dividendola per la cardinalità ottenuta con len(), tutto in una chiara riga di codice, senza eseguire cicli.
Anche in frequenze() eseguiamo un calcolo complesso con due sole righe di codice Python, utilizzando altre collezioni predefinite di alto livello come insiemi e dizionari. I set sono liste di valori unici, secondo la semantica degli insiemi; i dizionari invece sono contenitori di coppie chiave/valore. La funzione frequenze() calcola il numero di occorrenze dei valori nella lista, restituendo come risultato un dizionario che ha come chiavi i singoli elementi e come valori le frequenze con cui compaiono. Per esempio il dizionario freq = {1.29: 2, 1.30: 3, 1.32: 5, 1.33: 4} indica che il valore 1.29 compare nella lista 2 volte, 1.30 compare 3 volte, ecc. Quindi la funzione
def frequenze(sequence): “”"Calcola la frequenza dei valori nella sequenza e li restituisce in forma di dizionario {valore:frequenza}.”"” valori = set(sequence) return dict(zip(valori, [sequence.count(v) for v in valori]))
ottiene innanzitutto una collezione di valori unici semplicemente creando un set dalla sequenza di valori. L’istruzione
[sequence.count(v) for v in valori]
è un’idioma per eseguire operazioni su ogni elemento di una lista, chiamato in gergo Python list comprehension; in questo caso si costruisce una nuova lista con il numero di occorrenze con cui ciascun valore compare nella sequenza di partenza, calcolato invocando il metodo count() sulla sequenza stessa. La funzione built-in zip() invece costruisce una lista di tuple (liste immutabili) mischiando opportunamente le sequenze passate come argomento; per esempio: zip([1, 2, 3], ['a','b','c']) restituisce la lista di tuple [(1, 'a'), (2, 'b'), (3, 'c')]. Passando a zip() l’insieme dei valori e le frequenze si ottiene una lista di tuple (valore, frequenza). Quindi la funzione dict() crea il dizionario delle frequenze utilizzando le tuple come coppie chiave/valore. Tutto con solo due righe di codice: come si intuisce è più difficile descriverlo che farlo.
In Python, usando creativamente i tipi di contenitore predefiniti (liste, insiemi, dizionari, …) e le funzioni per manipolarli, si possono eseguire operazioni complesse su collezioni di oggetti con poche righe di codice semplice e chiaro.
Un po’ di test
Ora che abbiamo un modello, come possiamo verificare che funzioni correttamente? Semplice: con un test case, una raccolta di test automatizzati. I test eseguono delle verifiche sulle funzionalità di un modulo e forniscono dei risultati. Possiamo così mettere alla prova il modello con dati di esempio e verificare che i risultati siano quelli attesi. Non solo: i test costituiscono anche documentazione, in quanto ogni test mostra un esempio pratico di utilizzo delle funzionalità del modulo. Ecco i test per il modello della calcolatrice.
import unittest from statistica import * class StatisticaTest(unittest.TestCase): def setUp(self): self.serie = [2.17, 2.18, 2.18, 2.18, 2.19, 2.19, 2.19, 2.19, 2.19, 2.19, 2.20, 2.20, 2.20, 2.20, 2.20, 2.20, 2.20, 2.21, 2.21, 2.21, 2.21, 2.21, 2.21, 2.22, 2.22, 2.22, 2.23] def test_media(self): self.assertAlmostEqual(2.20, media(self.serie), 2) def test_errore_semplice_medio(self): self.assertAlmostEqual(0.01, errore_semplice_medio(self.serie), 2) def test_varianza(self): self.assertAlmostEqual(0.0002, varianza(self.serie), 4) def test_deviazione_standard(self): self.assertAlmostEqual(0.0141, deviazione_standard(self.serie), 4) def test_frequenze(self): freq = {2.17: 1, 2.18: 3, 2.19: 6, 2.20: 7, 2.21: 6, 2.22: 3, 2.23: 1} self.assertEquals(freq, frequenze(self.serie)) if __name__ == ‘__main__’: suite = unittest.TestLoader().loadTestsFromTestCase(StatisticaTest) unittest.TextTestRunner(verbosity=2).run(suite)
Un test case è una classe che eredita da TestCase. Un test è ogni metodo il cui nome inizia con il prefisso test. Il metodo setUp() inizializza le strutture dati necessarie ad eseguire i test e viene eseguito prima di ciscuno di essi. Ogni test deve essere eseguito in isolamento rispetto agli altri test, per esempio non deve dipendere dal risultato di un test precedente, perciò prima di ciascun test vengono ricreate le stesse condizioni. Con il metodo assertEqual() viene verificato che il risultato di un’operazione sul modello (il secondo parametro) sia uguale ad un valore atteso (passato come primo parametro); trattandosi di numeri in virgola mobile, la variante assertAlmostEqual() verifica che il risultato sia quello atteso entro il numero di cifre decimali specificato come terzo parametro. Nel nostro test case il metodo setUp() inizializza una lista con dati di esempio. Il metodo test_media() verifica che il calcolo della media dei valori contenuti nella lista risulti uguale al valore atteso di 2.20, con una tolleranza di 2 cifre decimali. Invece il metodo test_frequenze() verifica e documenta la struttura del dizionario delle frequenze calcolato dalla funzione frequenze().
Lanciando il modulo con il test case vengono eseguiti tutti i test, segnalati eventuali errori, verificate tutte le istruzioni di tipo assert e mostrato un riepilogo. Nel momento in cui scriviamo del codice o apportiamo delle modifiche i test ci informano istantaneamente su eventuali errori introdotti, in modo da poterli correggere subito. Quando il codice “passa” tutti i test abbiamo la certezza che sia corretto. Lo sforzo per scrivere i test viene ampiamente ripagato dalla loro utilità.
Nell’archivio trovate i sorgenti. Nel prossimo articolo inizieremo a costruire con le Qt l’interfaccia utente per il modello che abbiamo creato.
