< Home
Stampa

Funzioni

Sommario

Una funzione è una porzione di un programma a cui viene assegnato un nome e che può essere richiamata in qualsiasi altra parte del programma. L’obiettivo è prima di tutto quello di evitare di ripetere codice, ed inoltre di organizzare il codice in modo da renderlo più semplice da realizzare. Il programmatore quindi suddivide il suo problema più complesso in problemi più semplici da risolvere singolarmente.

Parametri

Una funzione inoltre può ricevere parametri, in questo modo si può richiamare la stessa funzione con parametri diversi rendendola flessibile in base al contesto. Inoltre una funzione può opzionalmente essere definita per restituire dei risultati.

Qui la struttura generale:

def name(parameter1, parameter2, ...):
  body
  [return result]

Esempio:

def factorial(n):
   result = 1
   while n > 0:
      result *= n
      n -= 1
   return n

fact(5) # 120
fact(3) # 6

I parametri della funzione sono detti formali (cioè quelli presenti nella dichiarazione) e non vanno confusi con quelli reali (cioè quelli effettivamente passarti quando viene eseguita). La dichiarazione di funzione coi parametri viene chiamata firma.

def sum(a,b):
   return a+b;

a1 = 3
a2 = 4
print(sum(a1, a2)) # stampa 7

I parametri possono essere indicati in modo esplicito:

print(sum(a=3, b=4))

E possono anche essere opzionali, dando loro un valore di default:

def saluta(nome, saluto="ciao"):
   print(f'{saluto} {nome})

saluta("Mario") # "Ciao Mario"
saluta("Carlo", "salve")

Gli argomenti di default sono utili per non doversi ricordare di passare parametri usati la maggior parte delle volte.

E’ possibile anche passare una lista di parametri di lunghezza variabile, che vengono passati sotto forma di lista.

def maximum(*numbers):
   if len(numbers) == 0:
      return None
   else:
      maximum = numbers[0]
      for n in numbers[1:]:
         if maximum < n:
            maximum = n
      return maximum

maximum(1,3,2) # 3
maximum(5, -2, 4, 9, 2) # 5

Bisogna fare attenzione però al fatto che i parametri, a seconda del loro tipo, si comportano in modo differente:

  • se sono int, float o stringhe, la funzione riceve una copia dei valori questo significa che se sono modificati internamente alla funzione, questa modifica non ha effetti sull’originale;
  • se sono liste, dizionari o insiemi, la funzione riceve invece il riferimento agli oggetti, e quindi se sono modificati internamente alla funzione, questa vale sull’originale.

Ad esempio:

def filterOdds(list): 
   newList = []
   for n in list:
      if n%2==0: 
         newList.append(n)
   list = newList

list = [1,2,3,4,5]
filterOdds(list); 
print(list) # [2,4]

Come si vede in questo esempio, la lista viene modificata dalla funzione.

Visibilità delle variabili

Una variabile dichiarata all’esterno di una funzione (cioè globale) è visibile ovunque, anche dentro la funzione stessa. Tuttavia questa variabile può essere usata dentro la funzione in sola lettura: se viene modificata Python da errore.

Una variabile dichiarata invece dentro una funzione è visibile solo dentro la funzione, e quando la funzione termina, questa viene distrutta. Questa variabile viene chiamata locale.

Questo duplice meccanismo serve per evitare errori di programmazione in cui per sbaglio cerchiamo di modificare variabili globali (da dentro una funzione) o locali (dall’esterno della funzione).

E’ comunque possibile modificare esplicitamente questo comportamento con il modificatore global: se (da dentro una funzione) si ridichiara una variabile già presente con il modificatore global, allora sarà possibile modificarla

punteggio = 0  # ← Variabile GLOBALE

def aggiungi_punti():
    global punteggio  # dice a Python di usare la variabile globale
    punteggio += 10

aggiungi_punti()
print(punteggio) 

Rivediamo ora l’esempio dell’inventario della lezione precedente, usando le funzioni:

def stampa(inventario):
  print("NOME\tPREZZO\tQUANTITA'")
  for nome, prodotto in inventario.items():
    print(f"{nome}\t{prodotto["prezzo"]}\t{prodotto["quantità"]}")

def acquista(inventario):
  nome_prodotto = input("Inserisci nome prodotto: ")
  if nome_prodotto not in inventario:
    print("Prodotto non in inventario.\n")
  else:
    if inventario[nome_prodotto]["quantità"] == 0:
      print("Prodotto terminato.\n")
    else:
      inventario[nome_prodotto]["quantità"] -= 1
  stampa(inventario)

inventario = {
    "laptop": {"prezzo": 900, "quantità": 5},
    "mouse": {"prezzo": 25, "quantità": 10},
    "tablet": {"prezzo": 450, "quantità": 8},
    "monitor": {"prezzo": 200, "quantità": 3}
}

fine = False
while not fine:
  print('''\nMenu
  1: mostra prodotti
  2: acquista
  0: esci''')
  scelta = int(input("Inserisci scelta: "))
  fine = 
  if scelta == 0:
    fine = True
  elif scelta == 1:
    stampa(inventario)
  elif scelta == 2:
    acquista(inventario)
    

Come si può vedere il programma originale viene scomposto, e vengono trasformate in funzioni i due blocchi logici dell’applicazione (acquisto e stampa). Il programma risulta più semplice da implementare e più leggibile.

Le funzioni come variabili

Una funzione non è solo un blocco di istruzioni con un nome e dei parametri. infatti può essere salvata in una variabile:

def saluta(nome):
    return f"Ciao, {nome}!"

messaggio = saluta  # ← assegna la funzione ad una variabile

print(messaggio("Marco"))  # → "Ciao, Marco!"

Tutto questo consente di memorizzare le funzioni anche in liste, tuple, dizionari e set, oltre che ad oggetti più complessi, che vedremo più avanti. Questo rende possibile scrivere codice in modo più snello ed espressivo.

Lo vediamo in questo esempio:

def stampa(inventario):
  print("NOME\tPREZZO\tQUANTITA'")
  for nome, prodotto in inventario.items():
    print(f"{nome}\t{prodotto["prezzo"]}\t{prodotto["quantità"]}")
  return False

def acquista(inventario):
  nome_prodotto = input("Inserisci nome prodotto: ")
  if nome_prodotto not in inventario:
    print("Prodotto non in inventario.\n")
  else:
    if inventario[nome_prodotto]["quantità"] == 0:
      print("Prodotto terminato.\n")
    else:
      inventario[nome_prodotto]["quantità"] -= 1
  stampa(inventario)
  return False

def esci(inventario):
  return True

inventario = {
    "laptop": {"prezzo": 900, "quantità": 5},
    "mouse": {"prezzo": 25, "quantità": 10},
    "tablet": {"prezzo": 450, "quantità": 8},
    "monitor": {"prezzo": 200, "quantità": 3}
}

funzioni = {1: stampa, 2: acquista, 0: esci}
fine = False
while not fine:
  print('''\nMenu
  1: mostra prodotti
  2: acquista
  0: esci''')
  scelta = int(input("Inserisci scelta: "))
  fine = funzioni[scelta)[inventario)

In questo esempio sono state create 3 funzioni che ritornano un valore bool, che indica se l’elaborazione del programma è finita oppure no (la scelta 0 restituisce True, le altre False). Le funzioni poi sono inserite in un array ed infine dal menu richiamiamo direttamente la funzione presente nell’array, passandole come parametro l’inventario, in quanto le funzioni sono tutte state standardizzate con la stessa firma.

Come si vede inserire le funzioni in un array (o in un dizionario) consente di poter accedervi mediante indice (o chiave) e quindi ci consente di utilizzare variabili per accedere una funzione specifica di un elenco di funzioni. Molto più comodo di doverla indicare esplicitamente ogni volta nel codice. Questa pratica di programmazione viene chiamata DRY (“Dont’ repeat yourself”).

L’utilizzo delle funzioni come se fossero variabili implica che possono essere anche passate come argomento di altre funzioni:

def doFilter(list, criteria): 
   newList = []
   for n in list:
      if criteria(n): 
         newList.append(n)
   list = newList

def isEven(value):
   return value % 2 == 0

def isOdd(value):
   return value % 2 != 0:

list1 = [1,2,3,4,5]
list2 = [3,2,4,5,1]
doFilter(list1, isEven) # senza parentesi 
print(list1) # [2,4]
doFilter(list2, isOdd) 
print(list2) # [3,5,1]

Come si può vedere l’algoritmo di filtro è composito:

  • l’algoritmo interno (isEven o isOdd) stabilisce se un numero è pari o dispari;
  • l’algoritmo esterno (filter) cicla sugli elementi, applicando la regola dell’algoritmo interno

Questo tipo di scomposizione, chiamata “strategy“, consente di scomporre problemi complessi in algoritmi combinabili tra loro per avere la massima flessibilità e scrivere meno codice. Questi algoritmi sono presenti anche nelle funzioni di libreria, come sorted e len

words = ["mare", "stella", "prova", "oceano"]
sorted_words = sorted(words, key=len)
print(sorted_words)

Dove key è un parametro opzionale di sorted che indica la funzione che indica l’ordinamento.

Espressioni lambda

Una espressione lambda è una forma compatta per dichiarare una funzione molto breve

def quadrato(n):
   return n ** 2
##OPPURE
quadrato = lambda n: n ** 2

Le espressioni lambda sono comode non solo per scrivere meno codice, ma perché possono essere definite in modalità anonima nelle funzioni composte che abbiamo visto sopra. Osserviamo che per compattezza non dobbiamo indicare la parola chiave return.

def doFilter(list, criteria): 
   newList = []
   for n in list:
      if criteria(n): 
         newList.append(n)
   list = newList

list1 = [1,2,3,4,5]
list2 = [3,2,4,5,1]
doFilter(list1, lambda x: x % 2 == 0)
print(list1) # [2,4]
doFilter(list2, lambda x: x % 2 != 0) 
print(list2) # [3,5,1]

Closure

Abbiamo visto che le funzioni possono essere memorizzate in variabili (ed inserite in liste o dizionari). Per questa loro natura inoltre possono essere passate come parametro di un’altra funzione.

Infine possono anche essere risultato di una funzione:

def crea_sommatore():
  s = 0
  def somma(v):
    nonlocal s # siccome dobbiamo modificare s, lo dichiariamo nonlocal
    s += v
    return s
  return somma

sommatore = crea_sommatore()
sommatore(3) # 3
sommatore(5) # 8 (la funzione ricorda il valore di s)     

Questa funzione ha una precisa caratteristica: ad ogni chiamata ricorda il valore precedente della variabile s. Quindi se eseguiamo la funzione con nuovi valori questa continuerà ad addizionarli al valore già presente.

Siccome la funzione somma modifica s, abbiamo visto che viene usato l’attributo nonlocal. In altre parole se dentro ad una funzione dichiaro una funzione annidata, per poter usare le variabili della funzione esterna in quella annidata (variabile che non è globale, ma nemmeno locale della sottofunzione), bisogna anteporre l’attributo nonlocal. Questo vale però solo per i tipi int, float, string, ovvero i tipi valore immutabili. Le liste, set e dizionari sono tipi riferimento e quindi possono essere internamente modificati senza dichiararli nonlocal.

Chiarito questo aspetto veniamo alla parte più interessante: la variabile s non viene distrutta dopo ogni esecuzione. Però all’inizio di questa lezione avevamo scritto che le variabili interne di una funzione, quando questa viene conclusa, vengono distrutte. Com’è possibile che allora la funzione si ricordi la variabile?

La spiegazione sta nel modo in cui i computer memorizzano le funzioni. Per eseguirle viene usata una porzione speciale della memoria, chiamata Stack (in italiano “pila”) che viene riempita quando si richiama una funzione e durante la sua esecuzione, e viene svuotata quando questa finisce. Tuttavia nelle funzioni che restituiscono funzioni, le funzioni restituite hanno ancora bisogno delle variabili presenti nello Stack (nel nostro esempio la funzione somma ha ancora bisogno della variabile s) e quindi Python non può eliminare quella porzione di Stack, almeno finché esiste la variabile sommatore.

Questo tipo di funzioni che creano altre funzioni “con memoria” tra una esecuzione e l’altra, sono chiamate Closure.

La regola generale è questa:

  • se una funzione non restituisce nulla, o restituisce un oggetto contenente solo dati, tutte le sue variabili interne vengono distrutte (precisamente sono tolte dallo Stack);
  • se una funzione restituisce una funzione, oppure un oggetto come una lista, un dizionario o altro ancora che però contiene almeno una funzione che usa una variabile della funzione che la crea, allora non viene distrutto nulla, ma viene mantenuto in memoria (precisamente, rimane nello Stack).

Veniamo un esempio con un dizionario che contiene funzioni:

def crea_sommatore():
  s = 0

  def visualizza():
    return s

  def somma(v):
    nonlocal s # siccome dobbiamo modificare s, lo dichiariamo nonlocal
    s += v
    
  return {
      "somma": somma,
      "visualizza": visualizza
  }

sommatore = crea_sommatore()
sommatore["somma"](3) # 3
sommatore["somma"](5) # 8
print(sommatore["visualizza"]())

In questo esempio abbiamo creato un vero e proprio componente applicativo. L’oggetto restituito è un dizionario che contiene due proprietà:

  • somma: funzione che somma il suo argomento alla variabile s
  • visualizza: funzione che mostra il valore di s

La forza di questo sistema è che dall’esterno non si può, nemmeno per errore, accedere al valore di s, e quindi s viene considerata una variabile privata dell’oggetto sommatore. Invece le funzioni somma e visualizza sono le funzioni pubbliche dell’oggetto, perché accessibili dall’esterno. Questo sistema consente di ottenere quella che in programmazione ad oggetti viene chiamata incapsulazione: oggetti cioè con una parte privata (s) ed una pubblica (somma e visualizza).

Vediamo subito l’utilità di questo meccanismo con la nostra applicazione inventario.

def crea_gestione_inventario():
  inventario = {
    "laptop": {"prezzo": 900, "quantità": 5},
    "mouse": {"prezzo": 25, "quantità": 10},
    "tablet": {"prezzo": 450, "quantità": 8},
    "monitor": {"prezzo": 200, "quantità": 3}
  }

  def stampa():
    print("NOME\tPREZZO\tQUANTITA'")
    for nome, prodotto in inventario.items():
      print(f"{nome}\t{prodotto["prezzo"]}\t{prodotto["quantità"]}")

  def acquista(nome_prodotto):
    if nome_prodotto not in inventario:
      print("Prodotto non in inventario.\n")
    else:
      if inventario[nome_prodotto]["quantità"] == 0:
        print("Prodotto terminato.\n")
      else:
        inventario[nome_prodotto]["quantità"] -= 1
   
  return {
       "stampa": stampa,
       "acquista": acquista
  }
  
def stampa(gestione_inventario):
  gestione_inventario["stampa"]()
  return False

def acquista(gestione_inventario):
  nome_prodotto = input("Inserisci nome prodotto: ")
  gestione_inventario["acquista"](nome_prodotto)
  gestione_inventario["stampa"]()
  return False

def esci(gestione_inventario):
  return True

funzioni = {1: stampa, 2: acquista, 0: esci}
gestione_inventario = crea_gestione_inventario()
fine = False
while not fine:
  print('''\nMenu
  1: mostra prodotti
  2: acquista
  0: esci''')
  scelta = int(input("Inserisci scelta: "))
  fine = funzioni[scelta](gestione_inventario)

In questa ultima versione dell’applicativo l’applicazione risulta meglio divisa in due parti:

  • la gestione inventario: si tratta di un componente che si occupa di gestire l’inventario, sia memorizzando in modo privato il dizionario, sia offrendo ai componenti esterni le funzioni per l’elaborazione.
  • la parte di interfaccia utente: interagisce con l’utente, gestisce il menu e usa le funzioni di gestione inventario. Non ha accesso diretto ai dati.

Questo modello rende possibile costruire applicazioni molto più complesse, perché permette di separare in modo efficace parti diverse dell’applicazione, senza il rischio di manipolare i dati nel posto sbagliato, e con la possibilità di suddividere un problema più complesso in più problemi più semplici da risolvere singolarmente.

Conclusioni

Riassumiamo qui quanto abbiamo visto in questa lezione:

  • le funzioni sono sottoprogrammi che consentono di suddividere in modo efficace un programma;
  • esse sono definite da un nome e da dei parametri, e possono avere un valore di ritorno;
  • le variabili interne ad una funzione sono locali ed invisibili all’esterno;
  • le funzioni stesse sono veri e propri oggetti, memorizzabili in variabili, liste, dizionari, possono essere usate come parametri di altri funzioni, e possono essere restituite da una funzione;
  • esiste una forma compatta di definizione, la funzione lambda;
  • le funzioni possono diventare Closure, ovvero funzioni generatrici di oggetti o funzioni che possono memorizzare al loro interno variabili private non visibili all’esterno, diventando dei veri e propri componenti dell’applicazione.