Jump to content
  • #6 Introduzione a Python: Map, Filter, Reduce, Iteratori, Generatori


     Share

    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:

    1. eseguire alcune operazioni per ogni elemento
    2. 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

    1. Quando si chiama una funzione regolare, essa genera un namespace privato con le sue variabili locali
    2. Quando esegue l'istruzione return, distrugge il namespace e restituisce il valore di ritorno al suo chiamante
    3. 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]
    >>>

     

     Share


    User Feedback

    Recommended Comments

    There are no comments to display.


×
×
  • Create New...

Important Information

Terms of Use Privacy Policy Guidelines We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.