Funzioni#

Altro strumento di fondamentale importanza in praticamente qualunque linguaggio di programmazione sono le funzioni. Una funzione è un’oggetto che prende in ingresso dei valori (detti input), esegue delle operazioni su tali valori, e restituisce in output il risultato. La gran parte delle funzionalità di Python che abbiamo già visto sono funzione (ad esempio, la funzione print(), la funzione len() e la funzione type()).

Le funzioni sono definite in Python tramite il comando def:

def NOME_FUNZIONE(INPUT1, INPUT2):
    # Esecuzione di operazioni sugli input
    
    return OUTPUT 

Una volta definita la funzione, è possibile utilizzarla nel codice semplice richiamando il suo nome e inserendo tra parentesi tonde gli input su cui calcolarla.

Vediamo ad esempio come implementare in Python una funzione che prende in input due numeri e ne ritorna il prodotto.

def calcola_prodotto(n1: float, n2: float) -> float:
    r"""
    Dati in input due numeri n1 ed n2, restituisce il prodotto tra n1 e n2.

    Parameters:
    
    n1 (int): Primo numero
    n2 (int): Secondo numero

    Returns:
    (int): Risultato di n1 * n2
    """
    risultato: float = n1 * n2 # Computazione
    return risultato # Output

Un paio di osservazioni sulla funzione appena definita. Nella definizione della funzione, dopo il nome della funzione stessa, abbiamo elencato gli input (in questo caso, n1 e n2), che sono stati tipizzati come float. Come sempre, questa operazione è facoltativa, ma fortemente raccomandata poiché aiuta molto la lettura del codice da parte di un autore esterno. Allo stesso modo, il tipo dell’output che ci si aspetta è inserito subito prima dei :, dopo la freccia ->.

Inoltre, subito dopo la dichiarazione del nome della funzione e dei suoi input, abbiamo inserito una documentazione nella forma di un commento multilinea. La formattazione di tale documentazione (che ha lo scopo di spiegare cosa fa la funzione, oltre che del significato dei vari inputs), è a preferenza dello studente. Quanto proposto dall’esempio sopra è considerato lo standard di Python ad oggi.

Vediamo ora come utilizzare la funzione sopra all’interno di uno script.

# Definisco due numeri
n1: float = 3.2
n2: float = 2

n: float = calcola_prodotto(n1, n2)
print(f"{n1} x {n2} = {n}.")
3.2 x 2 = 6.4.

Input posizionali e non posizionali#

Consideriamo una funzione calcola_differenza(n1, n2), che calcola la differenza tra n1 e n2. Andiamo a definire due numeri a e b, e andiamo ad analizzare in che modo questi valori vengono passati alla funzione.

def calcola_differenza(n1: float, n2: float) -> float:
    r"""
    Dati in input due numeri n1 ed n2, restituisce la differenza tra n1 e n2.

    Parameters:
    
    n1 (int): Primo numero
    n2 (int): Secondo numero

    Returns:
    (int): Risultato di n1 - n2
    """
    risultato: float = n1 - n2 # Computazione
    return risultato # Output

# Definiamo due numeri
a: int = 5
b: int = 2
differenza = calcola_differenza(a, b)
print(f"{a} - {b} = {differenza}.")
5 - 2 = 3.

Notiamo come in questo caso, essendo la funzione stata chiamata sugli input a e b in questo ordine, ha associato n1 (primo parametro di input) ad a, mentre n2 (secondo parametro di input) a b. Questo comportamento avviene di default quando richiamiamo una funzione. I valori a e b, in questo caso, sono passati come valori posizionali, poiché inseriti nello stesso ordine in cui vengono chiamati dalla funzione.

E’ possibile anche passare parametri non posizionali ad una funzione. Per farlo, è sufficiente dichiarare esplicitamente quale variabile associare al valore di input n1, e quale al valore di input n2. In questo modo, è possibile anche scambiare l’ordine in cui vengono inseriti i valori, senza variare il risultato.

# Calcoliamo la differenza con input non posizionali
differenza_2 = calcola_differenza(n2=a, n1=b)
print(differenza_2)
-3

Come si vede, sebbene le variabili a e b siano state date in input nello stesso ordine di prima, specificando la corrispondenza delle variabili, la funzione ha associato n1 = b e n2 = a.

### Valori di default L’inserimento di input non posizionali diventa particolarmente utile quando si considerano funzioni con tanti valori di input. In questo caso, è spesso possibile assegnare dei valori di default ad alcune variabili di input, che assumeranno tale valore se non esplicitamente dichiarato.

Consideriamo ad esempio una funzione calcola_operazione(n1, n2, op) che, inserita un’operazione sottoforma di stringa per la variabile op, applica l’operazione corrispondente tra i numeri a e b. Supponaiamo di volere associare da op il valore "somma", di default.

def calcola_operazione(n1: float, n2: float, op: str = "somma") -> float:
    r"""
    Dati in input due numeri n1 ed n2 e un'operazione, restituisce il risultato
    dell'operazione applicata ad n1 e n2.

    Parameters:
    
    n1 (int): Primo numero
    n2 (int): Secondo numero
    op (str): Operazione

    Returns:
    (int): Risultato di op(n1, n2)
    """
    if op == "somma":
        risultato: float = n1 + n2
    elif op == "differenza":
        risultato: float = n1 - n2
    elif op == "prodotto":
        risultato: float = n1 * n2
    elif op == "rapporto":
        if n2 != 0:
            risultato: float = n1 / n2
        else:  
            print("Errore: divisione per 0.")
            risultato = None
    else:
        print(f"Operazione {op} non definita.")
        risultato = None
    
    return risultato # Output

# Definiamo due numeri
a: int = 3
b: int = 1
somma = calcola_operazione(a, b, op="somma")
differenza = calcola_operazione(a, b, op="differenza")
prodotto = calcola_operazione(a, b, op="prodotto")
rapporto = calcola_operazione(a, b, op="rapporto")
print(somma, differenza, prodotto, rapporto)
4 2 3 3.0

La documentazione e la funzione help#

Come detto, lo scopo della documentazione è quello di permettere ad utenti terzi di utilizzare la funzione senza necessariamente conoscerne l’implementazione. Tutte le funzioni di Python presentano una documentazione sul loro utilizzo.

Tale documentazione può essere visualizzata in due modi: tramite la funzione help(), oppure (nei moderni editor di codice come VSCode), tramite la finestra che appare automaticamente una volta dichiarato il nome della funzione. Ad esempio:

# Visualizziamo l'help della funzione da noi definita
print(help(calcola_prodotto))
Help on function calcola_prodotto in module __main__:

calcola_prodotto(n1: float, n2: float) -> float
    Dati in input due numeri n1 ed n2, restituisce il prodotto tra n1 e n2.

    Parameters:

    n1 (int): Primo numero
    n2 (int): Secondo numero

    Returns:
    (int): Risultato di n1 * n2

None

Funzioni senza output#

Chiaramente, non tutte le funzioni resistuiscono un output nella forma di una variabile. Ad esempio, la funzione print() non restituisce alcun valore di output, ma stampa soltanto la stringa passata in input nella console. Stessa cosa vale per la funzione help().

Costruire una funzione che non ritorna alcun valore è semplice, basta omettere il comando return. Tale funzione verrà eseguita senza ritornare nulla come output. Di default, questo equivale a dire che se il risultato della funzione viene assegnato ad una variabile, questo assumerà il valore None.

Quando si definice una funzione che non ritorna nulla, è buona usanza tipizzare il suo output specificando che varrà None.

Costruiamo ad esempio una funzione che, presa in input un nome proprio, stampa a schermo il testo Ciao, NOME, senza ritornare alcun valore.

def saluti(nome: str) -> None:
    r"""
    Stampa a schermo dei saluti diretti al nome inserito in input.

    Parameters:
    nome (str): Il nome da salutare.
    """
    print(f"Ciao, {nome}!")

# Esempio
nome: str = "Davide"
saluti(nome)
Ciao, Davide!

E, se associamo una variabile al risultato della funzione, come detto questo varrà None.

output = saluti(nome)
print(output)
Ciao, Davide!
None

Funzioni multi-output#

In alcune occasioni, è necessario definire funzioni che restituiscono più di un output. Dal punto di vista implementativo, questo si può fare semplicemente elencando tutte le variabili da restituire di output, separate da una virgola.

Ad esempio, definiamo una funzione che, presi in input due numeri, ritorna la loro somma e il loro prodotto.

def somma_e_prodotto(n1: float, n2: float) -> float:
    r"""
    Presi in input i numeri n1 e n2, ne ritorna somma e prodotto.

    Parameters:
    n1 (float): Primo numero
    n2 (float): Secondo numero

    Returns:
    (float): n1 + n2
    (float): n1 * n2
    """
    somma = n1 + n2
    prodotto = n1 * n2
    return somma, prodotto

# Esempio
n1: float = 4.0
n2: float = - 2.0
somma, prodotto = somma_e_prodotto(n1, n2)

print(f"{n1} + {n2} = {somma}. {n1} x {n2} = {prodotto}.")
4.0 + -2.0 = 2.0. 4.0 x -2.0 = -8.0.

Che cosa succede se associamo l’output di una funzione multi-output ad una sola variabile?

# Esempio
n1: float = 4.0
n2: float = - 2.0
risultato = somma_e_prodotto(n1, n2)

print(risultato)
(2.0, -8.0)

Come già spiegato, la virgola , che separa gli output della funzione, definisce in Python una tupla. Il multi-output sarà quindi una tupla, che contiene come elementi gli output della funzione, ordinati.

Funzioni come oggetti#

Una proprietà interessante di Python è che le funzioni possono essere associate a delle variabili, e utilizzate come dei comuni oggetti, al pari di stringhe, interi e simili.

In particolare, è possibile passare funzioni in input ad altre funzioni. Vediamo un esempio.

# Definiamo una funzione che preso in input un numero x, ritorna il suo quadrato
def quadrato(x: float) -> float:
    r"""
    Ritorna il quadrato del valore preso in input.

    Parameters:
    x (float): Numero in input

    Returns:
    (float) x^2
    """
    risultato = x**2
    return risultato

# Ora, associamo la funzione ad una variabile, f
f = quadrato
print(quadrato)
print(f(3))
<function quadrato at 0x1085d0220>
9

Come è possibile vedere, la variabile f rappresenta ora la funzione. Vediamo come usarla come input di un’ulteriore funzione che, presa in input una funzione f e una tupla x = (x1, x2, ..., xn), ritorna il valore della funzione f applicata a ciascun elemento della tupla.

def elemento_per_elemento(f, x: tuple[float]) -> list[float]:
    r"""
    Presa in input una funzione f e una tupla di float x, ritorna una lista
    della stessa lunghezza di x, i cui elementi sono gli f(xi).

    Parameters:
    f (function): La funzione da applicare elemento per elemento
    x (tuple): Una tupla di numeri float

    Returns:
    (list): Una tupla contenente gli f(xi)
    """
    # Inizializiamo la tupla di output (per salvare memoria)
    y = [0] * len(x)

    # Cicliamo sugli elementi di x
    for i in range(len(x)):
        xi = x[i] # Estraiamo l'i-esimo elemento di x

        y[i] = f(xi) # Calcoliamo f su xi
    
    return y

Il vantaggio di tale approccio, è che la funzione sopra definita può essere ri-utilizzata su qualunque altra funzione che vogliamo applicare elemento per elemento ad una tupla di numeri.

def quadrato(x: float) -> float:
    r"""
    Ritorna il quadrato del valore preso in input.

    Parameters:
    x (float): Numero in input

    Returns:
    (float) x^2
    """
    risultato = x**2
    return risultato

def cubo(x: float) -> float:
    r"""
    Ritorna il cubo del valore preso in input.

    Parameters:
    x (float): Numero in input

    Returns:
    (float) x^3
    """
    risultato = x**3
    return risultato

# Esempio
x_vec: tuple[float] = (1, 2, 3, 4, 5)

x_vec_quadrato = elemento_per_elemento(quadrato, x_vec)
x_vec_cubo = elemento_per_elemento(cubo, x_vec)

print(x_vec_quadrato)
print(x_vec_cubo)
[1, 4, 9, 16, 25]
[1, 8, 27, 64, 125]

Funzioni lambda#

In alcune situazioni si vuole definire una funzione molto semplice, che possa essere scritta su una sola riga, in modo da semplificare la lettura del codice. Per farlo, in Python è possibile utilizzare le funzioni lambda. La sintassi è molto semplice:

# Definiamo una funzione che, preso in input un valore x, ritorna x**2
quadrato = lambda x: x**2
print(quadrato(3))
9

Infatti, è sufficiente richiamare il comando lambda, seguito dai valori di input, separati da una virgola, i :, e infine l’operazione da eseguire. Assegnando questa scrittura ad una qualunque variabile, questa viene interpretata come una funzione.

Variabili locali vs variabili globali#

Una caratteristica delle funzioni a cui è necessario prestare attenzione è la differenza tra le variabili locali (cioè definite all’interno della funzione) e le variabili globali (definite all’esterno della funzione).

Infatti, è possibile accedere dall’interno della funzione a tutte le variabili definite all’esterno, come se fossero passate in input. Viceversa, non è possibile accedere dall’esterno della funzione alle variabili definite al suo interno, che verranno create al momento in cui la funzione viene richiamata, e poi eliminate subito dopo.

Vediamo un esempio:

def saluti(cognome: str) -> None:
    print(f"Ciao, {nome} {cognome}.")
    return None

# Definiamo una variabile globale "nome"
nome: str = "Davide"
saluti(cognome="Evangelista")

# La variabile locale "cognome" non è più in memoria
print(cognome)
Ciao, Davide Evangelista.
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[15], line 10
      7 saluti(cognome="Evangelista")
      9 # La variabile locale "cognome" non è più in memoria
---> 10 print(cognome)

NameError: name 'cognome' is not defined

Note

E’ importante inoltre ricordare che, se una variabile locale è definita con lo stesso nome di una variabile locale, all’interno della funzione avrà valore solo la variabile locale, che andrà a sovrascrivere temporaneamente la corrispondente variabile locale.

def saluti(nome: str) -> None:
    print(f"Ciao, {nome}.")
    return None

# Definiamo una variabile globale "nome"
nome: str = "Davide"
saluti("Luca")
Ciao, Luca.

Warning

Modificare una variabile globale all’interno di una funzione, attraverso un’operazione che modifica l’oggetto senza riassegnarlo (come ad esempio il metodo .append() o .insert() per le liste), mantiene le modifiche anche all’esterno della funzione!

Fare molta attenzione ad utilizzare operazioni che modificano la variabile all’interno delle funzioni!

def fibonacci_step(valori: list) -> None:
    r"""
    Data una lista di valori, contenente una porzione della sequenza di Fibonacci, 
    aggiunge il successivo elemento della sequenza, in testa a tale lista.

    Parameters:
    valori (list): La lista con gli attuali valori della sequenza di Fibonacci
    """
    valori.append(valori[-2] + valori[-1])
    return None

# Definisco una lista iniziale
valori = [1, 1]
print(valori)

# Steps
fibonacci_step(valori)
fibonacci_step(valori)
print(valori)
[1, 1]
[1, 1, 2, 3]

Approfondimento: *args e **kwargs#

In alcune situazioni, si vuole definire una funzione a cui è possibile passare valori di input variabile, non fissati a priori. Sebbene sia questa una situazione molto rara, è possibile che ciò succeda quando si vogliono definire degli algoritmi numerici con molti parametri di input, alcuni dei quali sono facoltativi.

Quando ciò accade, vengono in aiuto i parametri *args e **kwargs, indicanti rispettivamente un numero non specificato di input posizionali e non posizionali. Vediamone un paio di esempi:

def somma(*args):
    r"""
    Dato in input un elenco di valori, ritorna la loro somma.
    """
    # Nota: il numero variabile di input viene caricato dentro una variabile di nome
    # "args", che praticamente rappresenta una tupla di valori.
    print(f"Somma di {args}:", end=" ")

    # Eseguo la somma dei valori
    risultato = 0
    for valore in args:
        risultato = risultato + valore
    return risultato

# Chiamata alla funzione con un numero variabile di argomenti
print(somma(1, 2, 3))
print(somma(5, 10))
Somma di (1, 2, 3): 6
Somma di (5, 10): 15

Nell’esempio, la funzione somma può accettare qualsiasi numero di parametri grazie all’input *args.

Similmente, con **kwargs, possiamo passare un numero variabile di argomenti non posizionali con nome a una funzione. Questi argomenti vengono ricevuti sotto forma di dizionario (e non tupla, come avveniva con *args).

def info_studente(**kwargs):
    for chiave, valore in kwargs.items():
        print(f"{chiave}: {valore}")

# Chiamata alla funzione con argomenti con nome
info_studente(nome="Luca", età=21, corso="Calcolo Numerico")
nome: Luca
età: 21
corso: Calcolo Numerico

E’ chiaramente possibile passare ad una funzione sia il valore *args che il valore **kwargs, così che possa prendere in ingresso un numero non specificato sia di valori posizionali che non posizionali.

def visualizza_args(input_1, input_2, *args, **kwargs):
    print(f"inputs: {input_1}, {input_2}.")
    print(f"*args: {args}")
    print(f"**kwargs: {kwargs}")

visualizza_args("input1", "input2", 1, 2, 3, nome="Luca", voto="30")
inputs: input1, input2.
*args: (1, 2, 3)
**kwargs: {'nome': 'Luca', 'voto': '30'}