Programmazione Funzionale
Esistono tre meccanismi principali ereditati dalla programmazione funzionale:
- Map
- Filter
- Reduce
Map
Map applica una funzione func a tutti gli elementi di una input_list
-> map(func, input_list)
items=[1,2,3,4,5] squared=[] for i in items: squared.append(i**2) #con map items=[1,2,3,4,5] squared=list(map(lambda x: x**2, items))
-> funzione len(), lista['how', 'are', 'you']
>>> print(list(map(len, ["how", "are", "you?"]))) [3, 3, 4]
Una definizione più corretta:
-> map(func, *iterables)
* significa che posso passare un numero x di oggetti iterabili (pari al numero di parametri che richiede la funzione)
In Python 3 la funzione map ritorna un oggetto di tipo map object che è un generator object
In Python 2 restituiva una lista
Per ritornare una lista usare il metodo list
my_pets=['alfred','tabitha','william','arla'] uppedred_pets=[] for pet in my_pets: pet_=pet.upper() uppered_pets.append(pet_) print(uppered_pets) #con map my_pets=['alfred','tabitha','william','arla'] uppered_pets=list(map(str.upper, my_pets)) print(uppered_pets)
my_pets=['alfred','tabitha','william','arla'] uppered_pets=map(str.upper, my_pets) print(uppered_pets) print(type(uppered_pets)) for i in uppered_pets:print(i) ========== <map object at 0x10..> <class 'map'> ALFRED TABITHA WILLIAM ARLA >>>
Esempio map con oggetti iterabili più di uno
circle_areas=[3.56773, 5.57668, 4.00914 23.0101, 9.01344, 32.00013] result=list(map(round, circle_areas, range(1,7))) print(result) =========== RESTART: [3.6, 5.58, 4.009, 56.2424, 9.01344, 32.00013] >>>
Filter
Filter() restituisce un oggetto iteratore composto dagli elementi per i quali la funzione restituisce true
richiede che la funzione restituisca valori booleani e "filtra" via gli elementi falsi
filter(func, iterable)
Filter vs. map
A differenza di map(), è richiesto solo un iterabile
L'argomento func deve restituire un tipo booleano.
In caso contrario, filter restituisce semplicemente l'iterabile passato a func
Poichè è richiesto un solo oggetto iterabile, è implicito che func debba accettare solo un argomento
scores=[60, 90, 55, 59, 76, 70, 88, 70, 81, 65] def filtering(score): return score>70 over_70=filter(filtering, scores) over_70_list=list(over_70) print(over_70_list) print(type(over_70)) =========== [90, 76, 88, 81] <class 'filter'> >>>
Se la funzione è None, viene assunta la funzione identità, ovvero vengono rimossi tutti gli elementi falsi dell'iterabile
score=[60, 0, 55, 59, 76, 70, 88, 70, 81, 65] over_70=list(filter(None, scores)) print(over_70) ========== [60, 55, 59, 76, 70, 88, 70, 81, 65] >>>
L'espressione filter(function, iterable) è quindi equivalente a:
(item for item in iterable if function(item)) # if function is not None (item for item in iterable if item) #if function is None
Curiosità: itertools.filterfalse(function,iterable)
- Crea un iteratore che filtra gli elementi da iterabile restituendo solo quelli per i quali il predicato è Falso
Reduce
Reduce applica cumulativamente una funzione di due argomenti agli elementi di un iterabile, opzionalmente iniziando con un argomento iniziale.
-> reduce(func, iterable[, initial]
func è la funzione a cui viene applicato cumulativamente ogni elemento nell'iterabile
Initial è il valore facoltativo che viene inserito prima degli elementi dell'iterabile nel calcolo e funge da valore predefinito quando l'iterabile è vuoto
- func richiede due argomenti, il primo dei quali è il primo elemento in iterable (se initial non è fornito)
- il secondo è il secondo elemento in itarable
se initial è fornito, diventa il primo argomento da utilizzare e il primo elemento in iterable diventa il secondo elemento.
- Reduce() "riduce" iterable in un unico valore
from functools import reduce number=[3,4,6,9,34,12] def custom_sum(first, second): return first+second result=reduce(custom_sum, numbers) print(result) ========== 68 >>>
Lambda functions
E' possibile utilizzare le funzioni lambda
Conosciute anche come anonymouse functions
Sono funzioni create al 'volo' senza nome
Il corpo della funzione lambda può contenere una sola espressione
Non contiene l'istruzione return perchè il corpo viene automaticamente restituito
-> lambda input-parameters: expression
Le funzioni lambda sono trattate come le normali funzioni a livello di interprete Python
Consentono di fornire una funzione come parametro ad un'altra funzione (ad esempio, in map, filter, reduce ecc.)
In tali casi l'utilizzo di lambda offre un modo elegante per creare una funziona singola e passarla come parametro
from functools import reduce li=[5,8,10,20,50,100] sum=reduce((lambda x, y:x+y), li) print(sum) ======== 193 >>>
Lambda function e sorting
# Sort case-independent
L=['a', 'Andew', 'from', 'is', 'string', 'test', 'This'] L.sort(key=str.lower)
# Sort by third field
students=[('john','A',15), ('Jane','B',12), ('tom','B',10) students.sort(key=lambda student: student[2])
# Sort by distance from orgin, from closer to further
points=[Point(1,2), Point(3,4), Point(4,1)] points.sort(key=lambda point.distanceFromOrigin())
Functools
Il modulo functools fornisce strumenti per adattare od estendere funzioni ed altri oggetti chiamabili, senza riscriverli completamente
Lo abbiamo usato nella reduce, introduce la classe partial
Iterators
Un iteratore è un oggetto che rappresenta un flusso di dati, questo oggetto restituisce i dati un elemento alla volta. Un iteratore Python deve supportare un metodo chiamato __next__() che non accetta argomenti e restituisce sempre l'elemento successivo dello stream
Se non ci sono più elementi nello stream, __next__() deve sollevare l'eccezione StopIteration
La funzione built-in iter() accetta un oggetto e cerca di restituire un iteratore che restituirà il contenuto o gli elementi dell'oggetto
Solleva TypeError se l'oggetto non supporta l'iterazione
Diversi tipi di dati built-it in Python supportano l'iterazione, come liste, stringhe e dizionari
- I dizionari possono creare diversi oggetti iteratori keys(), values(), items()
Un oggetto viene chiamato iterabile se è possibile ottenere un iteratore per esso
Alcuni data type built-in di Python che supportano l'iterazione
for element in [1,2,3]: print(element) for element in (1,2,3): print(element) for key in {'one':1,'two':2}: print(key) for char in "123": print(element) for line in open("myfile.txt"): print(line, end=' ')
Iterator protocol
L'istruzione for chiama iter() sull'oggetto container
La funzione restituisce un oggetto iteratore (iterator object) che definisce il metodo __next__() che accede agli elementi nel contenitore, uno alla volta
Quando non ci sono più elementi, __next__() solleva un'eccezione StopIteration che dice al ciclo for di terminare
>>> s="abc" >>> it=iter(s) >>> it <str_iterator object at 0x00..> >>> next(it) 'a' >>> next(it) 'b' >>> next(it) 'c' >>> next(it) StopIteration
Sequence unpacking support gli iteratori
>>> L=[1, 2, 3] >>> iterator = iter(L) >>> a, b, c = iterator >>> a, b, c (1, 2, 3)
Max(), min(), in, not in supportano gli iteratori
Si può solo andare avanti in un iteratore; non c'è modo di ottenere l'elemento precedente, ripristinare l'iteratore o crearne una copia
Gli oggetti Iterator possono facoltativamente fornire queste funzionalità aggiuntive, ma l'iterator protocol specifica solo il metodo __next__()
Una funzione può quindi consumare tutto l'output dell'iteratore ma se devi fare qualcosa di diverso con lo stesso stream, devi creare un nuovo iteratore
Generatori
Due operazioni comuni sull'output di un iteratore sono:
- eseguire alcune operazioni per ogni elemento
- selezionare un sottoinsieme di elementi che soddisfano alcune condizioni
List comprehensions e generator expressions sono una notazione concisa per queste operazioni
Generator expressions vs. list comprehensions
line_list=['line 1\n', 'line 2\n',...] #Generator expression -- returns iterator stripped_iter=(line.strip() for line in line_list) #List comprehension -- returns list stripped_list=[line.strip() for line in line_list]
Sintassi: per le generation expressions usiamo le parentesi tonde () e non quadre []
Generator expressions vs list comprehension
Con la list comprehension si ottiene una lista Python
stripped_list è una lista contenente le righe risultanti, non un iteratore
Le generator expressions restituiscono un iteratore che calcola i valori secondo necessità, senza bisogno di materializzare tutti i valori contemporaneamente
Ciò significa che la list comprehension non è efficiente quando si lavora con iteratori che restituiscono un flusso infinito o una grande quantità di dati
Per questi casi si usano le generator expressions
La forma generica di una generator expression:
( expression for expr in sequence1 if condition1 for expr2 in sequence2 if condition2 for expr3 in sequence3 if condition3 for exprN in sequenceN if conditionN )
I generatori sono una classe speciale di funzioni che semplificano il compito di scrivere iteratori
Restituiscono un iteratore che restituisce un flusso di valori
Le funzioni regolari (quelle 'normali') calcolano un valore e lo restituiscono
- Quando si chiama una funzione regolare, essa genera un namespace privato con le sue variabili locali
- Quando esegue l'istruzione return, distrugge il namespace e restituisce il valore di ritorno al suo chiamante
- Se la funzione viene richiamata: punto 1
Qualsiasi funzione contenente la keyword yield è una funzione generatore
L'istruzione yield viene usata solo quando si definisce una funzione generatore e solo nel corpo della funzione generatore
Quando viene chiamata restituisce un oggetto generatore iteratore
Il corpo della funzione generatore viene eseguito da una chiamata reiterata al metodo next() del generatore fino al sollevamento di un'eccezione
>>> def generate_ints(N): for i in range(N): yield i
La generator function restituisce un oggetto generatore che supporta l'iterator protocol
Quando si esegue l'istruzione yield, il generatore genera il valore di i
Yield diverso da return
Quando viene eseguita l'istruzione yield, lo stato del generatore viene congelato ed il valore dello yield viene restituito alla chiamata next()
Per "congelato" si intende che tutto lo stato locale viene conservato, inclusi i legami correnti delle variabili locali, il puntatore all'istruzione e lo stack di valutazione interno
Al prossimo next(), la funzione procede esattamente come se l'istruzione yield fosse chiamata dall'esterno
>>> gen=generate_ints(3) >>> gen <generator object generate_ints at ...> >>> next(gen) 0 >>> next(gen) 1 >>> next(gen) 2 >>> next(gen) StopIteration
Esempio: leggere righe da CSV
def csv_reader(file_name): fil=open(file_name) result=file.read().split("\n") return result
Codice corretto ma:
open() ritorna un generator object che può iterare in maniera 'lazy'
file.read().split() carica tutto in memoria contemporaneamente e se il fil è molto grande può causare un MemoryError
Utilizzando un generator function:
def csv_reader(file_name): for row in open(file_name, "r"): yield row
Si apre il file, si scorre attraverso di esse e si produce una riga (yield)
Non carica tutto il file in memoria
Oppure, con generator expression:
csv_gen=(row for row in open(file_name))
Esempio: permutazioni
Generare tutte le possibili combinazioni di stringhe alfanumeriche lunghe 8 caratteri:
from itertools import permutations options='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890' possibilities=[w for w in permutations(options, 8)]
Con le liste è lentissimo, meglio usare i generatori
from itertools import permutations options='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890' possibilities=[w for w in permutations(options, 8)] ======= >>> possibilities.__next()__() ('A','B','C','D','E','F','G','H') >>>
Passing values into a generator
Per passare valori in un generatore: yield diventa un'esrepssione, restituendo un valore che può essere assegnato ad una variabile:
-> val = (yield i)
Utilizzare il metodo generator.send(value)
def counter(maximum): i=0; while i<maximum: val=(yield i) # If value provided, change counter if val is not None: i=val else: i += 1
Se viene chiamato il metodo __next__(), yield restituisce None
Generator.send(value)
Riprende l'esecuzione e "invia" un valore nella funzione generatore
L'argomento value diventa il risultato dell'espressione dello yield corrente
Il metodo send() restituisce il valore successivo prodotto dal generatore o genera StopIteration se il generatore esce senza produrre un altro valore
Quando send() viene chiamato per avviare il generatore, deve essere chiamato con None come argomento, poichè non esiste ancora un'espressione di yield in grado di ricevere il valore
Altri metodi: throw, close
Itertools
Il modulo itertools della standard library offre molti strumenti per lavorare con gli iteratori
Ad esempio chain concatena più iteratori:
>>> it1=iter([1,2,3]) >>> it2=iter([4,5,6]) >>> itertools.chain(it1, it2) [1,2,3,4,5,6]
ZIP(*iterables)
Crea un iteratore che aggrega elementi da ciuscono degli iterabili
Restituisce un iteratore di tuple, in cui la i-esima tupla contiene l'i-esimo elemento di ciascuna delle sequenze di iterabili
L'iteratore si arresta quando viene raggiunta la fine dell'iterabile più breve
Con un singolo argomento iterabile, restituisce un iteratore di 1 tupla
Senza argomenti, restituisce un iteratore vuoto
*zip 'to unzip' un zip
#zip object is an iterator object x=[1,2,3,4] y=[5,6,7,8] zipped=zip(x,y) print(type(zipped)) =========== <class 'zip'> >>> zipped.__next__() (1,5) >>> next(zipped) (2,6) >>>
x=[1,2,3,4] x=[5,6,7,8] # to unzip x2, y2 = zip(*zip(x,y)) x==list(x2) and y==list(y2) print(x) print(y) ======== [1,2,3,4] [5,6,7,8] >>>
Recommended Comments
There are no comments to display.