Il popolare lin­guag­gio di pro­gram­ma­zio­ne Python è so­li­ta­men­te co­no­sciu­to per la pro­gram­ma­zio­ne orientata agli oggetti, ma è al­tret­tan­to adatto a essere usato per la pro­gram­ma­zio­ne fun­zio­na­le. In questo articolo potete scoprire quali sono le funzioni di­spo­ni­bi­li e come uti­liz­zar­le.

Per che cosa si con­trad­di­stin­gue la pro­gram­ma­zio­ne fun­zio­na­le?

Il termine “pro­gram­ma­zio­ne fun­zio­na­le” fa ri­fe­ri­men­to a uno stile di pro­gram­ma­zio­ne che impiega le funzioni come unità di base del codice. Si passa gra­dual­men­te da linguaggi puramente fun­zio­na­li, come Haskell o Lisp, a linguaggi multi-paradigma, come Python. Non ci sono quindi soltanto linguaggi di pro­gram­ma­zio­ne che sup­por­ta­no o che non sup­por­ta­no la pro­gram­ma­zio­ne fun­zio­na­le, esistono anche delle vie di mezzo.

Affinché la pro­gram­ma­zio­ne fun­zio­na­le sia possibile in un lin­guag­gio, questo deve trattare le funzioni come co­sid­det­ti 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 uti­liz­za­te come parametri da altre funzioni o re­sti­tui­te come valori di ritorno.

La pro­gram­ma­zio­ne fun­zio­na­le è di­chia­ra­ti­va

L’impiego della pro­gram­ma­zio­ne di­chia­ra­ti­va si basa sull’idea di de­scri­ve­re un problema, lasciando la sua soluzione all’ambiente di pro­gram­ma­zio­ne. Al contrario, l’approccio im­pe­ra­ti­vo si basa sulla de­scri­zio­ne di ogni singolo passaggio della soluzione. La pro­gram­ma­zio­ne fun­zio­na­le fa parte dell’approccio di­chia­ra­ti­vo. Tuttavia, Python consente di adottare entrambi gli approcci.

Vediamo un esempio concreto in Python: ri­por­tia­mo un elenco di numeri di cui vogliamo calcolare i numeri al quadrato cor­ri­spon­den­ti. Partiamo dall’approccio im­pe­ra­ti­vo:

# 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 com­pren­sio­ni di lista (in inglese: “List Com­pre­hen­sions”), ben com­bi­na­bi­li con le tecniche fun­zio­na­li, Python supporta un approccio di­chia­ra­ti­vo. Generiamo l’elenco dei numeri al quadrato senza un ciclo esplicito. Il codice che otteniamo è molto più snello e non necessita di in­den­ta­zio­ni:

# 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”) è pa­ra­go­na­bi­le alle funzioni ma­te­ma­ti­che di base. Il termine fa ri­fe­ri­men­to 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 col­la­te­ra­li (“side effects”).

In sintesi, queste ca­rat­te­ri­sti­che fanno sì che quando viene eseguita una funzione pura, il sistema cir­co­stan­te non subisca va­ria­zio­ni. Con­si­de­ria­mo l’esempio classico della funzione al quadrato f(x) = x x*. Questa può essere fa­cil­men­te im­ple­men­ta­ta come funzione pura in Python:

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

L’al­ter­na­ti­va alle funzioni pure sono le procedure, molto diffuse nei vecchi linguaggi di pro­gram­ma­zio­ne come Pascal e Basic. Proprio come una funzione, anche una procedura cor­ri­spon­de a un blocco di codice uti­liz­za­bi­le più e più volte. Tuttavia, una procedura non re­sti­tui­sce un valore. Le procedure accedono di­ret­ta­men­te alle variabili non locali e le mo­di­fi­ca­no a seconda delle necessità.

In C e Java le procedure sono rea­liz­za­te come funzioni che re­sti­tui­sco­no void come valore. In Python, invece, una funzione re­sti­tui­sce sempre un valore. Nel caso in cui non è di­spo­ni­bi­le una di­chia­ra­zio­ne di ritorno, viene re­sti­tui­to il valore speciale None. Pertanto, quando parliamo di procedure in Python, in­ten­dia­mo una funzione senza un’istru­zio­ne di ritorno.

Vediamo alcuni esempi di funzioni pure e impure in Python. La seguente funzione è impura perché re­sti­tui­sce 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 ri­fe­ri­men­to 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, in­fluen­zan­do il sistema cir­co­stan­te:

# 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 col­la­te­ra­li:

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

La ri­cor­si­vi­tà uti­liz­za­ta come al­ter­na­ti­va all’ite­ra­zio­ne

Nella pro­gram­ma­zio­ne fun­zio­na­le, la ri­cor­si­vi­tà è l’opposto dell’ite­ra­zio­ne. Una funzione ricorsiva fa ri­pe­tu­ta­men­te af­fi­da­men­to su sé stessa per ottenere un risultato. Affinché ciò avvenga senza che la funzione provochi un ciclo infinito, devono essere sod­di­sfat­te due con­di­zio­ni:

  1. La ri­cor­si­vi­tà deve terminare con il rag­giun­gi­men­to di un co­sid­det­to caso base.
  2. A ogni impiego della funzione deve seguire una riduzione del problema.

Python supporta le funzioni ricorsive. Vi mostriamo un esempio ab­ba­stan­za co­no­sciu­to: il calcolo della suc­ces­sio­ne di Fibonacci. Si tratta del co­sid­det­to approccio naive. Non è per­for­man­te per grandi valori di n, ma può essere ot­ti­miz­za­to 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 pro­gram­ma­zio­ne fun­zio­na­le?

Python è un lin­guag­gio multi-paradigma, ossia che consente di adottare diversi paradigmi di pro­gram­ma­zio­ne durante la com­pi­la­zio­ne di programmi. Oltre alla pro­gram­ma­zio­ne fun­zio­na­le, è possibile im­ple­men­ta­re fa­cil­men­te la pro­gram­ma­zio­ne orientata agli oggetti in Python.

Python vanta un’ampia gamma di strumenti per la pro­gram­ma­zio­ne fun­zio­na­le. Tuttavia, la sua portata risulta limitata rispetto ai linguaggi puramente fun­zio­na­li come Haskell. Il grado di pro­gram­ma­zio­ne fun­zio­na­le di un programma Python dipende in primis dal pro­gram­ma­to­re. Di seguito vi forniamo un riepilogo delle più im­por­tan­ti ca­rat­te­ri­sti­che fun­zio­na­li 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 lin­guag­gio le funzioni possono essere uti­liz­za­te ovunque sono ammessi altri oggetti. Diamo un’occhiata a un esempio concreto: pro­gram­mia­mo una cal­co­la­tri­ce che supporti varie ope­ra­zio­ni ma­te­ma­ti­che.

Per prima cosa mostriamo l’approccio im­pe­ra­ti­vo, che utilizza gli strumenti classici della pro­gram­ma­zio­ne strut­tu­ra­ta, come i rami con­di­zio­na­li e le istru­zio­ni di as­se­gna­zio­ne:

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 di­chia­ra­ti­vo applicato allo stesso problema. Invece del ramo if, mappiamo le ope­ra­zio­ni con la funzione dict di Python. In questo caso, i simboli delle ope­ra­zio­ni rimandano a oggetti cor­ri­spon­den­ti, che im­por­tia­mo dall’operatore modulo. Il codice è così più chiaro e non presenta ra­mi­fi­ca­zio­ni:

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 di­chia­ra­ti­va di calcolo. Le istru­zio­ni 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 lin­guag­gio prevede anche le co­sid­det­te funzioni lambda. Si tratta di funzioni brevi e anonime, cioè senza nome, che de­fi­ni­sco­no un’espres­sio­ne con dei parametri. Le lambda possono essere uti­liz­za­te ovunque può essere usata una qualsiasi funzione o possono essere assegnate a un nome:

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

Uti­liz­zan­do le lambda possiamo mi­glio­ra­re la nostra funzione calculate. Invece di co­di­fi­ca­re le ope­ra­zio­ni di­spo­ni­bi­li all’interno della funzione, uti­liz­zia­mo dict usando le funzioni lambda come valori. Questo ci permette di ag­giun­ge­re fa­cil­men­te nuove ope­ra­zio­ni 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 uti­liz­za­te in relazione a funzioni di ordine superiore, comemap() e filter(). Questo consente di tra­sfor­ma­re 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. Ri­con­si­de­ria­mo il problema relativo alla ge­ne­ra­zio­ne 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 re­sti­tui­sco­no 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, com­pren­sio­ni e ge­ne­ra­to­ri

Gli iterabili sono un concetto fon­da­men­ta­le di Python. Si tratta di un’astra­zio­ne sulle col­le­zio­ni, i cui elementi possono essere con­si­de­ra­ti sin­go­lar­men­te. Ne fanno parte stringhe, tuple, liste e dict, che seguono tutti le stesse regole. Un iterabile può essere in­ter­ro­ga­to 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 com­pren­sio­ni, che risultano adatte alla pro­gram­ma­zio­ne fun­zio­na­le e che hanno am­pia­men­te so­sti­tui­to 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 pro­gram­ma­zio­ne puramente fun­zio­na­li, Python offre un approccio per la va­lu­ta­zio­ne pigra grazie ai suoi co­sid­det­ti ge­ne­ra­to­ri. Ciò significa che i dati vengono generati soltanto al momento dell’accesso, per­met­ten­do di ri­spar­mia­re molta RAM. Di seguito vi mostriamo un’espres­sio­ne 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’istru­zio­ne yield, in Python si possono rea­liz­za­re le funzioni pigre. Scriviamo una funzione che re­sti­tui­sce i numeri positivi fino a un limite pre­sta­bi­li­to:

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

Quali sono le al­ter­na­ti­ve a Python per la pro­gram­ma­zio­ne fun­zio­na­le?

La pro­gram­ma­zio­ne fun­zio­na­le gode da tempo di grande po­po­la­ri­tà e si è affermata come prin­ci­pa­le corrente al­ter­na­ti­va alla pro­gram­ma­zio­ne orientata agli oggetti. La com­bi­na­zio­ne di strutture dati im­mu­ta­bi­li (“immutable”) con funzioni pure porta a un codice fa­cil­men­te pa­ral­le­liz­za­bi­le. La pro­gram­ma­zio­ne fun­zio­na­le risulta perciò par­ti­co­lar­men­te in­te­res­san­te per la tra­sfor­ma­zio­ne dei dati in pipeline di dati.

Par­ti­co­lar­men­te adatti risultano cer­ta­men­te i linguaggi puramente fun­zio­na­li con sistemi di ti­piz­za­zio­ne forte, come Haskell o il dialetto Lisp Clojure. Ma anche Ja­va­Script può essere con­si­de­ra­to un lin­guag­gio fun­zio­na­le. Ty­pe­Script rap­pre­sen­ta un’al­ter­na­ti­va moderna con ti­piz­za­zio­ne forte.

Consiglio

De­si­de­ra­te lavorare online con Python? Allora no­leg­gia­te lo spazio web per il vostro progetto.

Vai al menu prin­ci­pa­le