La programmazione funzionale in Python

Il popolare linguaggio di programmazione Python è solitamente conosciuto per la programmazione orientata agli oggetti, ma è altrettanto adatto a essere usato per la programmazione funzionale. In questo articolo potete scoprire quali sono le funzioni disponibili e come utilizzarle.

Per che cosa si contraddistingue la programmazione funzionale?

Il termine “programmazione funzionale” fa riferimento a uno stile di programmazione che impiega le funzioni come unità di base del codice. Si passa gradualmente da linguaggi puramente funzionali, come Haskell o Lisp, a linguaggi multi-paradigma, come Python. Non ci sono quindi soltanto linguaggi di programmazione che supportano o che non supportano la programmazione funzionale, esistono anche delle vie di mezzo.

Affinché la programmazione funzionale sia possibile in un linguaggio, questo deve trattare le funzioni come cosiddetti First Class Citizens. Questo è quello che fa Python, dove le funzioni sono oggetti, proprio come le stringhe, i numeri e gli elenchi. Le funzioni possono essere utilizzate come parametri da altre funzioni o restituite come valori di ritorno.

La programmazione funzionale è dichiarativa

L’impiego della programmazione dichiarativa si basa sull’idea di descrivere un problema, lasciando la sua soluzione all’ambiente di programmazione. Al contrario, l’approccio imperativo si basa sulla descrizione di ogni singolo passaggio della soluzione. La programmazione funzionale fa parte dell’approccio dichiarativo. Tuttavia, Python consente di adottare entrambi gli approcci.

Vediamo un esempio concreto in Python: riportiamo un elenco di numeri di cui vogliamo calcolare i numeri al quadrato corrispondenti. Partiamo dall’approccio imperativo:

# Calculate squares from list of numbers
def squared(nums):
    # Start with empty list
    squares = []
    # Process each number individually
    for num in nums:
        squares.append(num ** 2)
    return squares
Python

Grazie al metodo delle comprensioni di lista (in inglese: “List Comprehensions”), ben combinabili con le tecniche funzionali, Python supporta un approccio dichiarativo. Generiamo l’elenco dei numeri al quadrato senza un ciclo esplicito. Il codice che otteniamo è molto più snello e non necessita di indentazioni:

# Numbers 0–9
nums = range(10)
# Calculate squares using list expression
squares = [num ** 2 for num in nums]
# Show that both methods give the same result
assert squares == squared(nums)
Python

Le funzioni pure vengono preferite alle procedure

Una funzione pura (in inglese: “pure function”) è paragonabile alle funzioni matematiche di base. Il termine fa riferimento a una funzione che soddisfa i seguenti criteri:

• La funzione fornisce lo stesso risultato se gli argomenti sono gli stessi.

• La funzione ha accesso solamente agli argomenti di cui è composta.

• La funzione non causa effetti collaterali (“side effects”).

In sintesi, queste caratteristiche fanno sì che quando viene eseguita una funzione pura, il sistema circostante non subisca variazioni. Consideriamo l’esempio classico della funzione al quadrato f(x) = x x*. Questa può essere facilmente implementata come funzione pura in Python:

def f(x):
    return x * x
# let’s test
assert f(9) == 81
Python

L’alternativa alle funzioni pure sono le procedure, molto diffuse nei vecchi linguaggi di programmazione come Pascal e Basic. Proprio come una funzione, anche una procedura corrisponde a un blocco di codice utilizzabile più e più volte. Tuttavia, una procedura non restituisce un valore. Le procedure accedono direttamente alle variabili non locali e le modificano a seconda delle necessità.

In C e Java le procedure sono realizzate come funzioni che restituiscono void come valore. In Python, invece, una funzione restituisce sempre un valore. Nel caso in cui non è disponibile una dichiarazione di ritorno, viene restituito il valore speciale None. Pertanto, quando parliamo di procedure in Python, intendiamo una funzione senza un’istruzione di ritorno.

Vediamo alcuni esempi di funzioni pure e impure in Python. La seguente funzione è impura perché restituisce un risultato diverso ogni volta che viene eseguita:

# Function without arguments
def get_date():
    from datetime import datetime
    return datetime.now()
Python

La seguente procedura è impura perché fa riferimento a dati definiti al di fuori della funzione:

# Function using non-local value
name = 'John'
def greetings_from_outside():
    return(f"Greetings from {name}")
Python

La seguente funzione è impura poiché, quando eseguita, modifica un argomento mutabile, influenzando il sistema circostante:

# Function modifying argument
def greetings_from(person):
print(f"Greetings from {person['name']}")
    # Changing `person` defined somewhere else
    person['greeted'] = True
    return person
# Let's test
person = {'name': "John"}
# Prints `John`
greetings_from(person)
# Data was changed from inside function
assert person['greeted']
Python

La seguente funzione è pura poiché per lo stesso argomento fornisce sempre lo stesso risultato, senza originare effetti collaterali:

# Pure function
def squared(num):
    return num * num
Python

La ricorsività utilizzata come alternativa all’iterazione

Nella programmazione funzionale, la ricorsività è l’opposto dell’iterazione. Una funzione ricorsiva fa ripetutamente affidamento su sé stessa per ottenere un risultato. Affinché ciò avvenga senza che la funzione provochi un ciclo infinito, devono essere soddisfatte due condizioni:

  1. La ricorsività deve terminare con il raggiungimento di un cosiddetto caso base.
  2. A ogni impiego della funzione deve seguire una riduzione del problema.

Python supporta le funzioni ricorsive. Vi mostriamo un esempio abbastanza conosciuto: il calcolo della successione di Fibonacci. Si tratta del cosiddetto approccio naive. Non è performante per grandi valori di n, ma può essere ottimizzato per il caching.

def fib(n):
    if n == 0 or n == 1:
        return n
    else:
        return fib(n - 2) + fib(n - 1)
Python

Quanto è adatto Python alla programmazione funzionale?

Python è un linguaggio multi-paradigma, ossia che consente di adottare diversi paradigmi di programmazione durante la compilazione di programmi. Oltre alla programmazione funzionale, è possibile implementare facilmente la programmazione orientata agli oggetti in Python.

Python vanta un’ampia gamma di strumenti per la programmazione funzionale. Tuttavia, la sua portata risulta limitata rispetto ai linguaggi puramente funzionali come Haskell. Il grado di programmazione funzionale di un programma Python dipende in primis dal programmatore. Di seguito vi forniamo un riepilogo delle più importanti caratteristiche funzionali di Python.

Le funzioni in Python sono First Class Citizens

In Python vige la regola “ogni cosa è un oggetto” e questo vale anche per le funzioni. All’interno del linguaggio le funzioni possono essere utilizzate ovunque sono ammessi altri oggetti. Diamo un’occhiata a un esempio concreto: programmiamo una calcolatrice che supporti varie operazioni matematiche.

Per prima cosa mostriamo l’approccio imperativo, che utilizza gli strumenti classici della programmazione strutturata, come i rami condizionali e le istruzioni di assegnazione:

def calculate(a, b, op='+'):
    if op == '+':
        result = a + b
    elif op == '-':
        result = a - b
    elif op == '*':
        result = a * b
    elif op == '/':
        result = a / b
    return result
Python

Vediamo ora un approccio dichiarativo applicato allo stesso problema. Invece del ramo if, mappiamo le operazioni con la funzione dict di Python. In questo caso, i simboli delle operazioni rimandano a oggetti corrispondenti, che importiamo dall’operatore modulo. Il codice è così più chiaro e non presenta ramificazioni:

def calculate(a, b, op='+'):
    # Import operator functions
    import operator
    # Map operation symbols to functions
    operations = {
        '+': operator.add,
        '-': operator.sub,
        '*': operator.mul,
        '/': operator.truediv,
    }
    # Choose operation to carry out
    operation = operations[op]
    # Run operation and return results
    return operation(a, b)
Python

Testiamo quindi la nostra funzione dichiarativa di calcolo. Le istruzioni assert mostrano che il nostro codice funziona:

# Let's test
a, b = 42, 51
assert calculate(a, b, '+') == a + b
assert calculate(a, b, '-') == a - b
assert calculate(a, b, '*') == a* b
assert calculate(a, b, '/') == a / b
Python

Le funzioni Lambda sono anonime in Python

Oltre al metodo classico di definire le funzioni in Python tramite l’utilizzo della parola chiave def, il linguaggio prevede anche le cosiddette funzioni lambda. Si tratta di funzioni brevi e anonime, cioè senza nome, che definiscono un’espressione con dei parametri. Le lambda possono essere utilizzate ovunque può essere usata una qualsiasi funzione o possono essere assegnate a un nome:

squared = lambda x: x * x
assert squared(9) == 81
Python

Utilizzando le lambda possiamo migliorare la nostra funzione calculate. Invece di codificare le operazioni disponibili all’interno della funzione, utilizziamo dict usando le funzioni lambda come valori. Questo ci permette di aggiungere facilmente nuove operazioni in un secondo momento:

def calculate(a, b, op, ops={}):
    # Get operation from dict and define noop for non-existing key
    operation = ops.get(op, lambda a, b: None)
    return operation(a, b)
# Define operations
operations = {
    '+': lambda a, b: a + b,
    '-': lambda a, b: a - b,
}
# Let’s test
a, b, = 42, 51
assert calculate(a, b, '+', operations) == a + b
assert calculate(a, b, '-', operations) == a - b
# Non-existing key handled gracefully
assert calculate(a, b, '**', operations) == None
# Add a new operation
operations['**'] = lambda a, b: a** b
assert calculate(a, b, '**', operations) == a** b
Python

Le funzioni di ordine superiore in Python

Le lambda sono spesso utilizzate in relazione a funzioni di ordine superiore, comemap() e filter(). Questo consente di trasformare gli elementi di un iterabile senza l’uso di cicli. La funzione map() prende come parametri una funzione e un iterabile, eseguendo la funzione per ogni elemento dell’iterabile. Riconsideriamo il problema relativo alla generazione di numeri al quadrato partendo da una lista di numeri:

nums = [3, 5, 7]
squares = map(lambda x: x ** 2, nums)
assert list(squares) == [9, 25, 49]
Python
N.B.

Con funzioni di ordine superiore (in inglese: “higher order functions”) si intendono le funzioni che accettano funzioni come parametri o che restituiscono una funzione come risultato.

La funzione filter() serve afiltrare gli elementi di un iterabile. Ampliamo il nostro esempio in modo da generare solamente numeri al quadrato pari:

nums = [1, 2, 3, 4]
squares = list(map(lambda num: num ** 2, nums))
even_squares = filter(lambda square: square % 2 == 0, squares)
assert list(even_squares) == [4, 16]
Python

Iterabili, comprensioni e generatori

Gli iterabili sono un concetto fondamentale di Python. Si tratta di un’astrazione sulle collezioni, i cui elementi possono essere considerati singolarmente. Ne fanno parte stringhe, tuple, liste e dict, che seguono tutti le stesse regole. Un iterabile può essere interrogato con la funzione len():

name = 'Walter White'
assert len(name) == 12
people = ['Jim', 'Jack', 'John']
assert len(people) == 3
Python

Basandosi sugli iterabili si possono usare le comprensioni, che risultano adatte alla programmazione funzionale e che hanno ampiamente sostituito l’uso delle lambda con map() e filter().

# List comprehension to create first ten squares
squares = [num ** 2 for num in range(10)]
Python

Come avviene con i linguaggi di programmazione puramente funzionali, Python offre un approccio per la valutazione pigra grazie ai suoi cosiddetti generatori. Ciò significa che i dati vengono generati soltanto al momento dell’accesso, permettendo di risparmiare molta RAM. Di seguito vi mostriamo un’espressione di questo tipo che, in seguito all’accesso, calcola ogni singolo numero al quadrato:

# Generator expression to create first ten squares
squares = (num ** 2 for num in range(10))
Python

Con l’aiuto dell’istruzione yield, in Python si possono realizzare le funzioni pigre. Scriviamo una funzione che restituisce i numeri positivi fino a un limite prestabilito:

def N(limit):
    n = 1
    while n <= limit:
        yield n
        n += 1
Python

Quali sono le alternative a Python per la programmazione funzionale?

La programmazione funzionale gode da tempo di grande popolarità e si è affermata come principale corrente alternativa alla programmazione orientata agli oggetti. La combinazione di strutture dati immutabili (“immutable”) con funzioni pure porta a un codice facilmente parallelizzabile. La programmazione funzionale risulta perciò particolarmente interessante per la trasformazione dei dati in pipeline di dati.

Particolarmente adatti risultano certamente i linguaggi puramente funzionali con sistemi di tipizzazione forte, come Haskell o il dialetto Lisp Clojure. Ma anche JavaScript può essere considerato un linguaggio funzionale. TypeScript rappresenta un’alternativa moderna con tipizzazione forte.

Consiglio

Desiderate lavorare online con Python? Allora noleggiate lo spazio web per il vostro progetto.

Per offrirti una migliore esperienza di navigazione online questo sito web usa dei cookie, propri e di terze parti. Continuando a navigare sul sito acconsenti all’utilizzo dei cookie. Scopri di più sull’uso dei cookie e sulla possibilità di modificarne le impostazioni o negare il consenso.