Menu Chiudi

Il lato “funzionale” del Pitone

Di Maurizio Lupo e il team di ReLabs

Di Maurizio Lupo e il team di ReLabs

Python è un linguaggio multiparadigma. Possiamo programmare con uno stile imperativo (una istruzione dopo l’altra) oppure ad oggetti (incapsulando funzioni e dati in un unica entità). Non tutti però sanno che Python ha preso in prestito e implementato molte idee della programmazione funzionale. Dicendo questo non voglio sottointendere che Python sia un linguaggio funzionale puro come Lisp o Haskell. Ad esempio non privilegia l’utilizzo della ricorsione come struttura primaria di controllo ma integra alcuni costrutti funzionali nella propria sintassi in modo elegante.

Ma andiamo per ordine, questo articolo vuole essere una panoramica di come l’aspetto funzionale si integra con il linguaggio. Prima di tutto è il caso di definire cos’è la programmazione funzionale!

La programmazione funzionale

Il paradigma funzionale vede le “funzioni” come le protagoniste! Possono essere passate come argomento ad altre funzioni, combinate, utilizzate per elaborare sequenze di dati, costruite a partire da altre funzioni. Le strutture di controllo sono spesso demandate a espressioni booleane (vedi gli operatori corto circuito più in basso), i cicli sono spesso sostituiti dalla ricorsione. Il codice assume l’aspetto di una grossa espressione piuttosto che di una sequenza di comandi.

Ad un occhio non abituato, forse, può risultare un po’ più difficile da leggere ma usato senza eccedere può rendere il nostro codice più compatto, chiaro: la ricchezza del linguaggio ci dà gli strumenti per scrivere del codice semplice ed elegante utilizzando sempre il paradigma che più si adatta al risultato che vogliamo ottenere.

Andiamo per ordine ed esploriamo gli aspetti “funzionali” del linguaggio e come si integrano con la sintassi imperativa e ad oggetti

Le funzioni sono oggetti

Siamo abituati a pensare che le funzioni siano delle operazioni che restituiscono un valore. In effetti è così. Ma la differenza più evidente tra una funzione ed un oggetto è che un oggetto ha uno stato (il valore dei suoi attributi) mentre una funzione non ce l’ha.

In Python invece le funzioni sono oggetti “di prima classe” è possono tranquillamente avere degli attributi.

>>> def somma(a,b):

return a+b

>>> somma(2,3)

5

>>> somma.nuovoattributo=10

>>> print somma.nuovoattributo

10

Normalmente non utilizziamo questi attributi ma possono tornare utili in qualche occasione. Ecco un esempio un po’ più utile:

 

>>> def sommatore(a):

if not hasattr(sommatore,”totale”):

sommatore.totale=a

else:

sommatore.totale+=a

return sommatore.totale

 

>>> sommatore(1)

1

>>> sommatore(2)

3

>>> sommatore(5)

8

 

Oggetti richiamati come funzioni

Le classi Python possono avere un metodo speciale __call__. Questo viene richiamato quando un oggetto viene utilizzato come una funzione, senza specificare il metodo o l’attributo.

Ad esempio:

 

>>> class ClasseSomma(object):

def __call__(self,a,b):

return a+b

 

>>> sommatore = ClasseSomma()

>>> sommatore(2,3)

5

 

Funzioni passate come argomento

Visto e dimostrato che le funzioni sono oggetti possiamo quindi utilizzarle come tali:

 

>>> def somma(a,b):

… return a+b

 

>>> altra_somma=somma

>>> altra_somma(3,4)

7

 

Possiamo anche passarle come argomento:

 

>>> def quadrato(x):

… return x*x

 

>>> def cubo(x):

… return x*x*x

 

>>> def somma_e_eleva(a,b,potenza):

… return potenza(a+b)

 

>>> somma_e_eleva(3,4,quadrato)

49

 

>>> somma_e_eleva(3,4,cubo)

343

 

Passando la funzione quadrato o cubo a somma_e_eleva possiamo e aggiungere in seguito altri tipi di operazione (ad esempio elevare alla quarta) senza dover modificare il codice scritto fino ad ora.

Le funzioni Lambda

L’istruzione lambda ci permette di creare delle funzioni anonime che possono essere utilizzate o passate come argomento.

La sintassi è :

lambda parametri : istruzione

La funzione lambda non può contenere più di una istruzione. Il risultato di questa istruzione verrà restituito senza bisogno del comando “return”.

Questo costrutto si rivela utilissimo quando dobbiamo passare una semplice funzione come argomento.

Vediamo come poter rifattorizzare il codice precedente utilizzando questa sintassi:

 

>>> somma_e_eleva(3,4,lambda x: x*x)

 

>>> somma_e_eleva(3,4,lambda x: x*x*x)

 

Questo esempio ci mostra che, nel caso di funzioni molto semplici e richiamate solo in un punto, la funzione lambda ci permette di risparmiare alcune righe di codice e complessivamente risulta più leggibile.

Closures e decoratori

Python ci permette di definire delle funzioni all’interno di altre funzioni. Queste prendono il nome di closures:

 

>>> def elabora(numero,lista_addendi):

def somma(a,b):

return a+b

lista_output=[]

for addendo in lista_addendi:

lista_output.append(somma(numero,addendo))

return lista_output

 

>>> print elabora(5,[1,2,3])

[6,7,8]

 

Le closures rendono possibile la creazione dei decoratori: i decoratori costituiscono un classico design pattern della programmazione. Sono delle funzioni che aggiungono o modificano il comportamento di altre funzioni. Vediamo un esempio classico: il decoratore memoize:

 

>>> def memoize(func):

def _memoize(*args):

if not hasattr(func,’cache’):

func.cache={}

if args not in func.cache:

func.cache[args]=func(*args)

return func.cache[args]

return _memoize

 

Ho definito una funzione memoize che prende come argomento una funzione. Definisco una closure di nome _memoize. Quest’ultima cerca in un attributo della funzione in input un dizionario “cache” e lo crea se non esiste. In questo dizionario verranno messi da parte i valori di ritorno della funzione “func” con determinati argomenti in input. Memoize restituisce infine la closure.

Questo decoratore permette di memorizzare i risultati di una funzione richiamata con determinati argomenti e di restituirne i risultati senza dover richiamare nuovamente la funzione.

Il decoratore si può applicare in questo modo:

 

>>> def add(a,b):

return a+b

 

>>> add = memoize(add)

 

A questo punto richiamando add(3,4), per due volte, la seconda volta il risultato verrà prelevato dalla cache.

Questa sintassi però ha un problema di leggibilità dovuto al fatto che la decorazione avviene al fondo e, nel caso di funzioni lunghe, non è subito evidente.

Fortunatamente in Python c’è una sintassi su misura per questo caso ricorrente:

 

>>> @memoize

def add(a,b):

return a+b

 

“Corto circuito”

Gli operatori logici “and” e “or” sono detti anche operatori “corto circuito”. Questo soprannome è dovuto al modo in cui si comportano in base al valore booleano delle espressioni che collegano. Ricordo che Python considera falso: “”, None, False, [], {}. Tutti gli altri valori sono considerati veri. Facciamo un paio di esempi:

 

espressione1 and espressione2

 

In questo caso viene prima valutata espressione1. Se risulta vera il risultato finale dell’espressione sarà espressione2, altrimenti espressione1.

 

espressione1 or espressione2

 

In questo caso se espressione1 risulta vera espressione2 non viene valutata e il risultato sarà espressione1. In caso contrario il risultato sarà espressione2 . Vediamo un esempio pratico:

 

a = x or 5

 

Se x è falso a verrà valorizzato con 5 altrimenti con il valore di x.

Utilizzando l’usuale programmazione imperativa possiamo scrivere:

 

def pari_o_dispari(x)

if x % 2:

return “Numero pari!”

else:

return “Numero dispari!”

 

print pari_o_dispari(16)

Numero pari!

print pari_o_dispari(15)

Numero dispari!

 

Con lambda e gli operatori corto circuito invece:

 

pari_o_dispari=lambda x: x % 2 and “Numero pari!” or “Numero dispari!”

 

print pari_o_dispari(16)

Numero pari!

print pari_o_dispari(15)

Numero dispari!

 

Una volta abituati questa sintassi risulterà più sintetica ma anche chiara ed elegante.

Manipolazione di liste tramite funzioni

Spesso risulta necessario effettuare delle operazioni su una serie argomenti all’intero di una lista. Più comunemente può essere necessario filtrare gli elementi di una lista in base alla valutazione di una determinata funzione o ottenere una lista di risultati. Questo si ottiene con un paio di costrutti: map e filter.

 

nuova_lista=map(funzione,lista)

 

è l’equivalente di

 

nuova_lista=[]

for elemento in lista:

nuova_lista.append(funzione(elemento))

 

In pratica otteniamo dentro nuova_lista una lista di tutti i risultati ottenuti applicando la funzione agli elementi della lista.

 

nuova_lista=filter(funzione,lista)

è l’equivalente di

nuova_lista=[]

for elemento in lista:

if funzione(elemento):

nuova_lista.append(elemento)

 

Otteniamo dentro nuova_lista una lista di tutti gli elementi di lista che danno risultato valutato come vero se applicati a funzione. Facciamo un esempio utilizzando le funzioni lambda:

 

quadrati=map(lambda x: x*x,lista)

 

La lista quadrati conterrà il quadrato di tutti gli elementi di lista.

 

dispari=filter(lambda x: x % 2,lista)

 

La lista pari conterrà tutti i numeri dispari della lista.

Python fornisce un costrutto più flessibile che riunisce entrambe le funzionalità:

 

quadrati=[x*x for x in lista]

dispari=[x for x in lista if x % 2]

 

Posso anche ottenere tutti i quadrati dei numeri dispari facendo:

 

quadrati_dei_dispari=[x*x for x in lista if x % 2]

 

Le costruzioni di lista consentono anche di ottenere liste risultate dalla combinazioni di più liste:

 

coordinate=[(x,y) for x in lista1 for y in lista2]

 

Gli iteratori

Un iteratore è un oggetto che consente di visitare tutti gli elementi contenuti in un altro oggetto. In Python sono interscambiabili con le sequenze (liste, stringhe e tuple) con la differenza che restituiscono un valore ad ogni iterazione anziché mantenere una lista di valori in memoria. In questo modo sono molto più efficienti.

La sintassi di un iteratore è equivalente a quella di una funzione ma, al posto di “return” sarà presente “yield”.

Con yield la funzione ritorna un valore ma rimane in attesa fino alla prossima iterazione e poi prosegue (gli iteratori si usano ovviamente all’interno di cicli).

 

def numeri_dispari(max):

c=0

while c < max:

if c % 2:

yield c

c+=1

 

for n in numeri_dispari(25):

print n

 

Questo esempio stampa tutti i numeri pari fino a 25 usando l’iteratore numeri_dispari.

Map, filter e costruzioni di liste hanno anche una variante usando gli iteratori. Nel modulo itertools sono infatti presenti imap e ifilter che sono equivalenti ma restituiscono un iteratore al posto di una lista.

Inoltre le costruzioni di lista si possono trasformare in iteratori sostituendo le parentesi quadre con quelle tonde (con questa sintassi prendono il nome di genexp).

 

Partial

Questa decoratore del modulo functools, data in input una funzione dotata di vari argomenti, restituisce una funzione in cui uno o più di questi diventano costanti.

 

>>> from functools import partial

>>> def somma(a,b):

return a+b

 

>>> somma10=partial(somma,b=10)

>>> somma10(5)

15

 

Utilità sulle liste

L’ultimo, ma non meno importante, aspetto da considerare sono le funzioni di utilità disponibili per le liste. Abbiamo visto infatti che, spesso, l’oggetto del nostro codice funzionale sono appunto le liste:

len(lista) – restituisce la lunghezza di una lista

all(lista) – restituisce vero se tutti gli elementi della lista sono veri

any(lista) – restituisce vero se almeno un elemento della lista è vero

sum(lista) – restituisce la somma di tutti gli elementi della lista

max(lista) – restituisce l’elemento maggiore della lista

min(lista) – restituisce l’elemento minore della lista

 

Conclusione

L’aspetto più interessante della programmazione funzionale in Python è come questa si integri in modo naturale con gli altri paradigmi di programmazione utilizzati dal linguaggio. L’utilizzo sinergico dei vari stili di programmazione produce quello che spesso è definito come codice “pythonico”: un codice fortemente idiomatico, compatto, chiaro ed elegante.

 

Webgrafia:

http://it.wikipedia.org/wiki/Paradigma_di_programmazione

http://it.wikipedia.org/wiki/Programmazione_orientata_agli_oggetti

http://it.wikipedia.org/wiki/Programmazione_imperativa

http://it.wikipedia.org/wiki/Programmazione_funzionale

http://www.python.it/doc/articoli/funct.html

http://www.norvig.com/python-lisp.html

http://docs.python.org/index.html

http://docs.python.org/library/itertools.html

WordPress Appliance - Powered by TurnKey Linux