Archivio

Posts Tagged ‘Python’

Programma in PyQT con qualche spunto

24 agosto 2010

Aggiornamento: alla fine ho riscritto il programma in C++ (vedi post seguente) ;)

Questo programma si interfaccia con le API di Kiva (una piattaforma online per finanziare i progetti di microcredito) e, dato un utente, visualizza i prestiti da lui finanziati. La particolarità sta nel fatto che non usa alcuna libreria propria di Python (ad esclusione di sys), ma esclusivamente PyQt4 (nello specifico: QtCore, QtGui, QtNetwork, QtScript). In parole povere, troverete che:

  1. Non è stato usato urllib per prendere i dati dal web, ma QNetworkRequest, in modo tale che l’interfaccia grafica non si blocchi durante le operazioni di caricamento.
  2. Non è stato utilizzato il parser json dell’anonima libreria di Python, ma è stato analizzato il codice grazie a QScriptEngine. L’unico vantaggio ha riguardato la gestione degli errori.

Perché ho voluto “farmi del male”? :D Semplice: dato che PySide non supporta ancora Symbian, ho deciso di scrivere il codice in Python nelle modalità appena mostrate, per poi riscriverlo in C++ e fare un bel pacchettino .sis da installare sul telefono :) Avrei potuto farlo direttamente in C++, ma dato che sono “arrugginito” non volevo mettere troppa carne al fuoco :)

Qui sotto metterò il cuore del programma, mentre mi limiterò ad allegare il file gui.py relativo all’interfaccia.

#!/usr/bin/python
# -*- coding: utf-8 -*-
 
from gui import Ui_MainWindow
from PyQt4 import QtCore, QtGui, QtNetwork, QtScript
 
class Kiva(QtGui.QMainWindow):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        self.network = QtNetwork.QNetworkAccessManager()
        self.engine = QtScript.QScriptEngine()
        self.name = ""
        QtCore.QObject.connect(self.network,
            QtCore.SIGNAL("finished(QNetworkReply*)"), self.load);
        QtCore.QObject.connect(self.ui.pushButton,
            QtCore.SIGNAL("clicked()"), self.refresh)
    def add(self, name, status, progress):
        row = self.ui.tableWidget.rowCount()
        self.ui.tableWidget.insertRow(row)
        qstatus = QtGui.QTableWidgetItem(status)
        self.ui.tableWidget.setItem(row, 0, qstatus)
        progressBar = QtGui.QProgressBar()
        progressBar.setValue(progress)
        self.ui.tableWidget.setCellWidget(row, 1, progressBar)
        qname = QtGui.QTableWidgetItem(name)
        self.ui.tableWidget.setItem(row, 2, qname)
    def get(self, req, **attrs):
        url = QtCore.QUrl("http://api.kivaws.org/v1%s.json" % req)
        for key, value in attrs.items():
            url.addQueryItem(key, str(value))
        self.network.get(QtNetwork.QNetworkRequest(url))
    def load(self, data):
        sc = self.engine.evaluate("(%s)" % QtCore.QString(data.readAll()))
        current = int(sc.property("paging").property("page").toInteger())
        total = int(sc.property("paging").property("pages").toInteger())
        if sc.property("loans").isArray():
            cursor = QtScript.QScriptValueIterator(sc.property("loans"))
            if current == 1:
                self.ui.tableWidget.clearContents()
                self.ui.tableWidget.setRowCount(0)
            cursor.next()
            while cursor.hasNext():
                tmp = {}
                for attr in ["status", "name"]:
                    tmp[attr] = cursor.value().property(attr).toString()
                for attr in ["funded_amount", "loan_amount", "paid_amount"]:
                    tmp[attr] = cursor.value().property(attr).toInteger()
                cursor.next()
                self.parse(tmp)
            if current == total:
                text = "%d loan(s) loaded" % self.ui.tableWidget.rowCount()
                self.ui.statusbar.showMessage(text)
                self.ui.pushButton.setEnabled(True)
            else:
                text = "Page %d of %d loaded" % (current, total)
                self.ui.statusbar.showMessage(text)
                self.get("/lenders/%s/loans" % self.name, page = current+1)
        else:
            self.ui.statusbar.showMessage("User %s not found" % self.name)
            self.ui.pushButton.setEnabled(True)
    def parse(self, loan):
        status = loan["status"]
        if status in ["fundraising"]:
            progress = 100*loan["funded_amount"]/loan["loan_amount"]
        elif status in ["in_repayment", "paid"]:
            progress = 100*loan["paid_amount"]/loan["loan_amount"]
        elif status == "defaulted":
            progress = 0 # TODO: Is there a way to know paid_amount?
        elif status == "refunded":
            return # FIXME: what is that status?
        self.add(loan["name"], status.replace("_", " "), progress)
    def refresh(self):
        self.ui.pushButton.setEnabled(False)
        self.name = self.ui.lineEdit.text()
        self.ui.statusbar.showMessage("Connecting...")
        self.get("/lenders/%s/loans" % self.name)
 
if __name__ == "__main__":
    import sys
    app = QtGui.QApplication(sys.argv)
    kiva = Kiva()
    kiva.show()
    sys.exit(app.exec_())

frafra Python, QT , , , , ,

Il mio codice Python è scritto bene?

10 novembre 2009

Chiedilo a pep8.py ;)

Per chi non lo sapesse, la PEP 8 è una specifica di Python che indica come si dovrebbe scrivere il codice (definisce come deve essere l’indentazione ecc.).
Questo programma verifica se il codice scritto è conforme alla PEP 8 :)

Link: http://github.com/jcrocholl/pep8/raw/master/pep8.py

frafra Python ,

Scaricare l’ultima versione di un certo programma

10 novembre 2009

Vi è mai capitato di dover (o voler) scaricare l’ultima versione di uno o più programmi? O sapere semplicemente se è stato rilasciato un aggiornamento?
Bene, a Frafra queste cose capitano, soprattutto quando si parla di sorgenti, compilazione, e compagnia cantante :)

Ho creato un programma Python (>= 3.x, testato su 3.1.1), che risolve automaticamente questo problema, con l’uso di un parser e di un crawler :) Sono cento linee giuste giuste, parzialmente commentate, con tanto di licenza (questa volta metto il file per intero, perché in primo luogo ho raggiunto un numero di linee tondo tondo, e in secondo luogo perché la prima linea è molto importante).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# takeit.py
#
# Copyright 2009 Francesco Frassinelli <fraph24@gmail.com>
#
#     This program is free software: you can redistribute it and/or modify
#     it under the terms of the GNU General Public License as published by
#     the Free Software Foundation, either version 3 of the License, or
#     (at your option) any later version.
#    
#     This program is distributed in the hope that it will be useful,
#     but WITHOUT ANY WARRANTY; without even the implied warranty of
#     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
#     GNU General Public License for more details.
#    
#     You should have received a copy of the GNU General Public License
#     along with this program. If not, see <http://www.gnu.org/licenses/>.
 
""" This program shows you the latest version of a source package """
 
### HTMLParser patched version, see: http://bugs.python.org/issue755660
# $ cp /usr/local/lib/python3.1/html/parser.py HTMLParser.py
# $ wget http://bugs.python.org/file13041/htmlparser_error.diff
# $ patch -p0 HTMLParser.py < htmlparser_error.diff
import HTMLParser
import re, sys, urllib.request
 
servers = {
    "gnu":"http://ftp.gnu.org/gnu/",
    "linux":"http://ftp.kernel.org/pub/linux/",
    "vim":"http://ftp.vim.org/pub/vim/unix/",
} # List of available servers: add yours and report test them
 
class Parser(HTMLParser.HTMLParser):
    def __init__(self, page, callback):
        super(Parser, self).__init__()
        self.callback = callback
        self.page = page # Current page
    def handle_starttag(self, tag, attrs):
        if tag == "a":
            attrs = dict(attrs)
            if "href" in attrs:
                self.callback(self.page, attrs["href"])
    def error(self, message):
        pass # Overwriting default function, in order to skip any error
 
class Crawler:
    def __init__(self):
        self.source = re.compile(r"""^
            (.+?)-                  # package name
            ([\d\.-]+)              # version
            \.(tar\.bz2|tar\.gz)    # compression
            $""", re.VERBOSE) # VERBOSE is for multiline commented regex
        self.packages = dict()
    def inspect(self, page):
        print("I'm in:", page) # If there're errors, report it *always*
        with urllib.request.urlopen(page) as data:
            parser = Parser(page, self.callback)
            parser.feed(data.read().decode("utf_8", "ignore"))
            parser.close()
    def item(self, page, link):
        if self.source.match(link):
            res = self.source.search(link)
            package, version, compression = res.groups()
            if package == "0": # Should we use re.match("^\d+$", package)?
                return # Not properly detected: regex could be wrong
            if package not in self.packages:
                self.packages[package] = list()
            self.packages[package].append((version, page + link))
    def callback(self, page, link):
        # Special link / Backwards link / External link or absolute path
        useless = "?" in link or ".." in link or link.count("/") > 1
        if not useless:
            if link.endswith("/"):
                self.inspect(page + link) # Directory: recursive function
            else:
                self.item(page, link) # File: analize its name
    def results(self):
        for package, version in sorted(self.packages.items()):
            version, url = sorted(version, reverse = True)[0]
            yield package, version, url # Just to iterate it
 
def mycrawler(base, directory = ""):
    crawler = Crawler()
    crawler.inspect(base + directory)
    for package, version, url in crawler.results():
        print() # Newline ("\n")
        print("Name:", package)
        print(" -> Version:", version)
        print(" -> URL:", url)
 
if __name__ == "__main__":
    args = len(sys.argv)
    base      = (servers[sys.argv[1]],) if args > 1 else servers.values()
    directory = set(sys.argv[2:])       if args > 2 else ("",)
    for server in base:
        for page in directory:
            mycrawler(server, page)

frafra Linux, Python , ,

Tesina multimediale in PyQT4/XHTML/CSS

3 settembre 2009

Ecco che,  come promesso, ho pubblicato la mia tesina multimediale, scritta (e programmata) per la mia maturità.

Attualmente la struttura è funzionante, ma mancano alcune grosse correzioni che ho apportato il 10 luglio (la prima versione che ho appena pubblicato risale al 9 luglio). Le apporterò al più presto (grazie alla copia cartacea della tesina, dato che la versione definitiva che ho portato all’orale s’è volatilizzata).

La cosa interessante di questa tesina multimediale consiste nel fatto che è stata creata con le seguenti caratteristiche:

  • Il programma per visualizzare i contenuti è scritto in Python
  • La libreria grafica utilizzata è QT (>= 2.5) che, grazie a un widget particolare, permette di renderizzare le pagine web con webkit
  • I contenuti sono stati scritti in XHTML/CSS (validati dal correttore del W3C)

Questo mi ha permesso di portare una tesina diversa dal solito, che ha interessato i professori, al posto della noiosissima e poco efficace presentazione Powerpoint. Inoltre, avendola fatta in xhtml/css, l’ho potuta (con un semplice browser) salvare in pdf e stamparla, ottenendo così anche una versione cartacea).

L’ho caricata su github, all’indirizzo: github.com/frafra/Tesina-Frafra/tree

Potete scaricarla premendo su download (e scaricandola sottoforma di archivio compresso), oppure clonando il repository git. Tutto il codice è commentato.

frafra Frafra, Python, QT , , , ,

Gioco della vita di Conway

23 marzo 2009

Ho implementato il gioco della vita di Conway in Python. Avete in mente il simbolo hacker (il glider, quella griglia 3×3 con dei pallini neri)? Beh, sono strettramente legati :)
Questa implementazione permette svariate cose, tra cui:

  1. Una volta impostata una griglia, vedere l’evolversi della situazione.
  2. Definite le dimensioni della griglia, trovare (con un bruteforce) tutte le figure che si ripetono dopo N stadi.

Ecco il codice (commentato parzialmente in inglese):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#!/usr/bin/env python
# -*- coding: utf-8 -*-
 
try:
    import psyco
    from psyco.classes import __metaclass__
    psyco.full()
except:
    pass
 
class Life:
    """ Implementation of the Conway's game of life """
    def __init__(self, rows, columns):
        """ Inizializes 2d table """
        self.rows, self.columns = rows, columns
        self.board = [[0,]*self.columns for row in xrange(self.rows)]
    def get(self, row, column):
        """ Gets value or return 0 """
        check = 0 <= row < self.rows and 0 <= column < self.columns
        return (check and self.board[row][column] or 0)
    def add(self, row, column):
        """ Adds eight cells near current cell """
        return self.get(row - 1, column - 1) + \
            self.get(row - 1, column) + \
            self.get(row - 1, column + 1) + \
            self.get(row, column - 1) + \
            self.get(row, column + 1) + \
            self.get(row + 1, column - 1) + \
            self.get(row + 1, column) + \
            self.get(row + 1, column + 1)
    def next(self):
        """ Calculates the next stage """
        current = [row[:] for row in self.board] # It clones the table
        for r, row in enumerate(current): # For every row...
            for c, item in enumerate(row): # ...and for every column...
                near = self.add(r, c) # ...it calculates near cells
                if near not in (2, 3) and item: # Condition to death
                    current[r][c] = 0 # Let's kill it :(
                elif near == 3 and not item: # Condition to became alive
                    current[r][c] = 1 # Let's live! :D
        self.board = current # It overwrites the old stage
    def show(self, target = False):
        """ Shows the table """
        for row in (target or self.board):
            print("".join([column and "@" or "x" for column in row]))
 
def bruteforce(rows, columns):
    life = Life(rows, columns)
    possibilities = [list(i) for i in product((0, 1), repeat=columns)]
    for select in product(xrange(len(possibilities)), repeat=rows):
        life.board = [possibilities[i] for i in select]
        history = [life.board]
        while 1:
            life.next()
            if life.board in history:
                if life.board == history[0]:
                    life.show(history[0])
                    print("")
                break
            history.append(life.board)
 
if __name__ == "__main__":
    from itertools import product
    from sys import argv
    try:
        rows, columns = int(argv[1]), int(argv[2])
    except IndexError:
        print("Usage: %s [rows] [columns]" % argv[0])
    except ValueError:
        print("Usage: %s [rows] [columns]" % argv[0])
    else:
        bruteforce(rows, columns)
 
"""
# Example code: glider (spaceship, hacker symbol)
if __name__ == "__main__": # If you're executing this program...
    life = Life(6, 6) # It inizializes a 2d 6x6 table
    life.board[2][0] = 1 # 2, 0 set alive
    life.board[2][1] = 1 # 2, 1 set alive
    life.board[2][2] = 1 # 2, 2 set alive
    life.board[1][2] = 1 # 1, 2 set alive
    life.board[0][1] = 1 # 0, 1 set alive
    for time in range(12): # For n times...
        life.next() # ...it makes the next stage...
        life.show() # ...and it shows it :)
"""

La prima porzione di codice è la seguente:

4
5
6
7
8
9
try:
    import psyco
    from psyco.classes import __metaclass__
    psyco.full()
except:
    pass

In questa maniera, importiamo, se disponibile, la libreria psyco, che, su macchine x86, permette di eseguire operazioni molto onerose per la cpu, in maniera molto più veloce :)
Successivamente troviamo questa classe:

11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class Life:
    """ Implementation of the Conway's game of life """
    def __init__(self, rows, columns):
        """ Inizializes 2d table """
        self.rows, self.columns = rows, columns
        self.board = [[0,]*self.columns for row in xrange(self.rows)]
    def get(self, row, column):
        """ Gets value or return 0 """
        check = 0 <= row < self.rows and 0 <= column < self.columns
        return (check and self.board[row][column] or 0)
    def add(self, row, column):
        """ Adds eight cells near current cell """
        return self.get(row - 1, column - 1) + \
            self.get(row - 1, column) + \
            self.get(row - 1, column + 1) + \
            self.get(row, column - 1) + \
            self.get(row, column + 1) + \
            self.get(row + 1, column - 1) + \
            self.get(row + 1, column) + \
            self.get(row + 1, column + 1)
    def next(self):
        """ Calculates the next stage """
        current = [row[:] for row in self.board] # It clones the table
        for r, row in enumerate(current): # For every row...
            for c, item in enumerate(row): # ...and for every column...
                near = self.add(r, c) # ...it calculates near cells
                if near not in (2, 3) and item: # Condition to death
                    current[r][c] = 0 # Let's kill it :(
                elif near == 3 and not item: # Condition to became alive
                    current[r][c] = 1 # Let's live! :D
        self.board = current # It overwrites the old stage
    def show(self, target = False):
        """ Shows the table """
        for row in (target or self.board):
            print("".join([column and "@" or "x" for column in row]))

Questo codice è stato scritto con una particolare attenzione alle prestazioni. Cerchiamo di comprendere meglio le varie funzioni:

  1. __init__(self, rows, columns): dati due numeri interi, “disegna” una tabella delle dimensioni specificate
  2. get(self, row, column): è una funzione che viene usata per vedere se una cella è viva o morta. In caso questa cella non esista, ritorna semplicemente zero, come se fosse morta. Ho trovato questo metodo sensibilmente più veloce rispetto al fare un try/except (senza quindi controllare l’esistenza della cella in base alle coordinate), e molto più chiaro rispetto al creare una tabella con un bordo di una cella.
  3. add(self, row, column): date le coordinate di una cella, somma le otto celle accanto, utilizzando get().
  4. next(self): calcola lo stadio successivo.
  5. show(self, target=False): visualizza in maniera abbastanza “grezza” la nostra griglia sul terminale.

Qui finisce il “grosso” del codice. Successivamente abbiamo la funzione per il bruteforce:

47
48
49
50
51
52
53
54
55
56
57
58
59
60
def bruteforce(rows, columns):
    life = Life(rows, columns)
    possibilities = [list(i) for i in product((0, 1), repeat=columns)]
    for select in product(xrange(len(possibilities)), repeat=rows):
        life.board = [possibilities[i] for i in select]
        history = [life.board]
        while 1:
            life.next()
            if life.board in history:
                if life.board == history[0]:
                    life.show(history[0])
                    print("")
                break
            history.append(life.board)

Questa funzione fa uso di itertools per calcolare le combinazioni possibili. Bisogna prestare particolarmente attenzione a piccoli accorgimenti, come il calcolare volta per volta ogni possibile combinazione, semplicemente “scegliendo” tra le “righe” generate. Ho trovato questo metodo particolarmente efficente e veloce.
Per sapere se una figura è già apparsa, crea una lista degli stadi, finché qualcosa non si ripete. Questo deve sempre avvenire, in quanto anche se non capitassimo di fronte a una figura “particolare”, dopo qualche passaggio tutte le celle morirebbero, e le ritroveremmo nuovamente morte nello stadio successivo, provocando una ripetizione (che, in questo caso, non verrà però visualizzata).
Il blocco successivo serve a rendere operativo il bruteforce:

62
63
64
65
66
67
68
69
70
71
72
if __name__ == "__main__":
    from itertools import product
    from sys import argv
    try:
        rows, columns = int(argv[1]), int(argv[2])
    except IndexError:
        print("Usage: %s [rows] [columns]" % argv[0])
    except ValueError:
        print("Usage: %s [rows] [columns]" % argv[0])
    else:
        bruteforce(rows, columns)

Come già accennato precedentemente, importiamo itertools.product (assieme a sys.argv), e prendiamo gli argomenti passati da linea di comando, che diamo in pasto a bruteforce().
L’ultima parte è commentata, e mostra un utilizzo alternativo della classe Life:

74
75
76
77
78
79
80
81
82
83
84
85
86
"""
# Example code: glider (spaceship, hacker symbol)
if __name__ == "__main__": # If you're executing this program...
    life = Life(6, 6) # It inizializes a 2d 6x6 table
    life.board[2][0] = 1 # 2, 0 set alive
    life.board[2][1] = 1 # 2, 1 set alive
    life.board[2][2] = 1 # 2, 2 set alive
    life.board[1][2] = 1 # 1, 2 set alive
    life.board[0][1] = 1 # 0, 1 set alive
    for time in range(12): # For n times...
        life.next() # ...it makes the next stage...
        life.show() # ...and it shows it :)
"""

Si inizializza una griglia col famoso simbolo hacker, e si mostrano gli stadi successivi :)

Simpatico, no? :)

P.S. Ho omesso la licenza per problemi di spazio… Ovviamente è GPLv3 :)

frafra Python , ,

Controllo dei valori – prima revisione

22 marzo 2009

E’ possibile migliorare il codice scritto precedentemente in Controllo dei valori, riducendo il numero di linee e rendendo più dettagliato l’errore. Nel vecchio sorgente, noi possiamo sapere su quale variabile fallisce il controllo, ma non quale controllo fallisce. Inoltre, vorremmo veder visualizzato un errore personalizzato. Questo è possibile con questa nuova versione, che, tra l’altro, non fa più uso di un dizionario (come poteva essere locals()) per richiamare le variabili, permettendo così di inserire, nella nostra tabella di regole, direttamente un valore, senza dover sapere come si chiama la variabile.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/env python
# -*- coding: utf-8 -*-
 
check = lambda rules: rule[0](*rule[1:]) for rule in rules
 
if __name__ == "__main__":
    def integer(x): assert isinstance(x, int), "%s non è intero" % x
    def positive(x): assert 0 <= x, "%i non è positivo" % x
    def between(f, x, t): assert f <= x < t, "%x non è tra %f e %t" % (f, x, t)
    height = 3; width = 2; row = 1; column = 2
    check((
        (integer, height), (positive, height),
        (integer, width), (positive, width),
        (integer, row), (between, 0, row, height),
        (integer, column), (between, 0, column, width),
        ))

Andiamo ad analizzarlo meglio:

7
8
9
    def integer(x): assert isinstance(x, int), "%s non è intero" % x
    def positive(x): assert 0 <= x, "%i non è positivo" % x
    def between(f, x, t): assert f <= x < t, "%x non è tra %f e %t" % (f, x, t)

In questo passaggio definiamo tre regole: a livello di funzionamento, solo l’ultima varia dalla versione precedente (è più generica, in quanto permette di stabilire un minimo che, nell’altra versione, era impostato su zero). La sintassi però è diversa:

  1. Si usa “assert” (ed è qui il vero cambiamento del controllo del valore): non dobbiamo creare una classe a parte per il nostro errore, e…
  2. Possiamo specificare, dopo la condizione, un nostro messaggio di errore, che ci renda immediata l’individuazione del punto dove il controllo fallisce
11
12
13
14
15
16
    check((
        (integer, height), (positive, height),
        (integer, width), (positive, width),
        (integer, row), (between, 0, row, height),
        (integer, column), (between, 0, column, width),
        ))

Abbiamo cambiato anche il modo di definire le regole: ogni regola è separata dalle altre, mentre precedentemente più regole erano associate a una variabile da controllare. Inoltre i parametri della funzione, li mettiamo come elementi della nostra regola, e non come argomenti della funzione stessa.
Riepilogando, ogni regola è formata da:

  1. Una funzione, che risulta essere il primo elemento
  2. Una o più variabili, che risultano trovarsi dal secondo elemento in avanti

Infine, abbiamo definito una nuova funzione per la verifica:

4
check = lambda rules: rule[0](*rule[1:]) for rule in rules

…in una sola linea, di facile comprensione :)

frafra Python

Controllo dei valori

21 marzo 2009

Accade che ci si ritrovi a dover verificare un valore (spesso inserito dall’utente), per evitare spiacevoli sorprese (ad esempio, dobbiamo controllare che l’utente abbia inserito un numero intero maggiore di zero, altrimenti il programma crasha). Ora, vi propongo una soluzione… ;)


Primo problema… Come specificare le regole? Attraverso delle parole chiave (per esempio, associare al valore una lista di parole chiave, come “intero”, “maggiore_di_zero”)? Il problema principale di questo approccio consiste nel fatto che per permettere una validazione utilizzabile in vari ambiti, dovremo scrivere tante regole, con tonnellate di “if”, e svariate decine (o centinaia) di righe.
Come fare? Semplice, la lista delle regole che andremo a creare, non è altro che una lista di una o più funzioni, che restituiscono vero (nel caso la condizione sia soddisfatta) o falso (nel caso non lo sia).
Prima vi mostro il codice sorgente completo del programmino-libreria, che poi andremo ad analizzare pezzo per pezzo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/usr/bin/env python
# -*- coding: utf-8 -*-
 
class ValidationError(Exception):
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return "validation error at '%s'" % self.value
 
def check(rules, values):
    for name, rule in rules.iteritems():
        if not all(f(values[name]) for f in rule):
            raise ValidationError, name
 
if __name__ == "__main__":
    integer = lambda var: type(var) == int
    positive = lambda var: 0 <= var
    from_zero_to_num = lambda num: lambda var: 0 <= var <= num-1
    height = 3; width = 2; row = 1; column = 2
    check({
        "height":(integer, positive),
        "width":(integer, positive),
        "row":(integer, from_zero_to_num(height)),
        "column":(integer, from_zero_to_num(width)),
        }, locals())

Analizziamo ora il primo blocco di istruzioni eseguito all’avvio del programma:

16
17
18
    integer = lambda var: type(var) == int
    positive = lambda var: 0 <= var
    from_zero_to_num = lambda num: lambda var: 0 <= var <= num-1

Qui sopra definiamo tre regole d’esempio, che poi verranno utilizzate nella lista delle regole da verificare associate alla variabile:

  1. La prima funzione restituisce True se il var è un numero intero
  2. La seconda se var è maggiore/uguale a zero
  3. La terza se var è maggiore/uguale a zero e minore/uguale di num-1

Semplice, no? Adesso diamo dei valori a qualche variabile e definiamo la nostra lista delle regole:

20
21
22
23
24
25
26
    height = 3; width = 2; row = 1; column = 2
    check({
        "height":(integer, positive),
        "width":(integer, positive),
        "row":(integer, from_zero_to_num(height)),
        "column":(integer, from_zero_to_num(width)),
        }, locals())

La funzione di controllo si chiama check, e ha per argomenti:

  1. Un dizionario, dove il nome di una variabile è associato a delle regole
  2. Un dizionario, dove sono presenti queste variabili (vedi locals())

ndr.: è consigliabile porre le regole in ordine: dalla meno restrittiva alla più restrittiva.
Questa è la funzione di controllo:

10
11
12
13
def check(rules, values):
    for name, rule in rules.iteritems():
        if not all(f(values[name]) for f in rule):
            raise ValidationError, name

Con all(…) verifichiamo che tutte le funzioni restituiscano True. Ricordiamo che all() è una funzione short-circuit.
E, infine, ecco la nostra eccezione “ValidationError”:

4
5
6
7
8
class ValidationError(Exception):
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return "validation error at '%s'" % self.value

Spero possa esservi utile ;)

frafra Python