Folder Open Duotone Icon

Le funzioni definite da utente in Python

Avatar Mario Giagnotti

·

·

13 min read

Python

In questo articolo, “Le funzioni definite da utente in Python” ci occupiamo di funzioni in Python definite da utente e di come invocarle

In questo articolo, “Le funzioni definite da utente in Python” ci occupiamo di funzioni in Python definite da utente e di come invocarle

Definizione di funzioni

Python, come molti altri, dispone di una soluzione specifica per evitare la replicazione del codice: le funzioni.

In Python le funzioni si definiscono con la parola chiave def, utilizzata per definire le funzioni. Seguono il nome e la lista dei parametri in ingresso che costituiscono la firma (signature) della funzione.

La firma si conclude con il simbolo «:» che introduce un blocco di codice indentato che costituisce il corpo della funzione

Un programma Python che calcola le combinazioni di n elementi presi a gruppi di k può essere, pertanto, realizzato come segue:

n = int(input("Numero di elementi totali: "))
k = int(input("Numero di elementi di ogni gruppo: "))
nk = n - k;
nFattoriale = 1
kFattoriale = 1
nkFattoriale = 1

while(n > 0):
   nFattoriale = nFattoriale * n
   n = n - 1

while(k > 0):
   kFattoriale = kFattoriale * k
   k = k - 1 

while(nk > 0):
   nkFattoriale = nkFattoriale * nk
   nk = nk - 1

print("Numero combinazioni:", int(nFattoriale/(kFattoriale * nkFattoriale)))

ma definendo una funzione fattoriale, possiamo richiamarla all’occorrenza riducendo la complessità del codice:

def fattoriale(n):
   f = 1
   while(n > 0):
      f = f * n
      n = n - 1
   return f

n = int(input("Numero di elementi totali: "))
k = int(input("Numero di elementi di ogni gruppo: "))

print(fattoriale(n)/(fattoriale(k) * fattoriale(n - k)))

Nel secondo caso la funzione sarà invocata con il nome fattoriale, riceverà in input un parametro denominato n e restituirà in output come risultato un valore di tipo numerico.

Il nome della funzione è a tutti gli effetti un identificatore e pertanto, in analogia a quanto appreso per i nomi delle variabili, dovrà rispettare le regole previste da Python perché risulti sintatticamente corretto:

  • non sono ammessi simboli speciali come «$», «!», «@», «#», «%», …;
  • deve iniziare con una lettera o con il carattere «_»;
  • deve essere privo di spazi;
  • non può coincidere con una delle parole chiave del linguaggio.

La lista dei parametri consta di una sequenza di nomi di variabile separati dal simbolo «,» e racchiusi tra i simboli «(» e «)». È possibile anche definire funzioni che non necessitano di parametri e, in tal caso, nella definizione della funzione, il nome sarà seguito solo dai simboli «(» e «)».

In generale, la definizione di una funzione ha la seguente struttura:

Invocazione di una funzione

L’invocazione di una funzione avviene specificandone il nome e i parametri su cui deve essere eseguito il calcolo che essa implementa; normalmente il risultato è assegnato a una variabile.


ESEMPIO

L’istruzione

alfa = fattoriale(5)

assegna alla variabile alfa il risultato del calcolo del fattoriale del valore 5.

Allo stesso modo, l’istruzione

numero = 5

alfa = fattoriale(numero)

esegue il calcolo del fattoriale del valore numerico che la variabile numero assume al momento dell’invocazione della funzione stessa.


Oltre che nel contesto di un assegnamento del risultato a una variabile, una funzione può essere invocata prevedendone un suo utilizzo diretto.

Gli esempi seguenti mostrano un’invocazione di funzione all’interno di un’altra istruzione o di un’espressione

print(fattoriale(n))

oppure:

combinazioni = fattoriale(n)/(fattoriale(k) * fattoriale(n – k))

Nel caso in cui non sia previsto un valore di ritorno, il corpo della funzione può terminare con un’istruzione return senza valore oppure l’istruzione return può essere omessa e, in questo caso, il programma continua con la prima istruzione appartenente al livello di indentazione più esterno.

OSSERVAZIONE Una funzione che non restituisce alcun risultato viene invocata direttamente, senza dover dipendere da istruzioni di assegnamento o di altro tipo; ha normalmente lo scopo di modificare il valore di una o più variabili esterne alla funzione, oppure di eseguire un’operazione come la scrittura di un file.


ESEMPIO

La seguente funzione visualizza un messaggio che indica se il valore passato come argomento è positivo o negativo:

a = int(input("inserisci un numero: "))
def visualizzaSegno(a):
   if(a > 0):
      print("positivo")

   else:
      print("negativo")

print("il numero", a, "è")
visualizzaSegno(a);

L’invocazione di una funzione prevede i seguenti passi:

  • invocazione della funzione e memorizzazione del punto corrente di esecuzione del codice nella funzione invocante;
  • trasferimento dell’esecuzione alla prima istruzione della funzione invocata e copia dei valori forniti nelle variabili che rappresentano i parametri della funzione;
  • esecuzione del codice della funzione invocata;
  • ripresa dell’esecuzione dal punto di sospensione nel codice della funzione invocante memorizzato al momento dell’invocazione con eventuale assegnazione del valore calcolato restituito dalla funzione invocata.

L’esecuzione del codice di una funzione f2 invocata dal codice di una funzione f1 può essere rappresentata come in figura:

Essendo la generazione della funzione un meccanismo attivato mediante l’esecuzione dell’istruzione def, consegue che:

  • è possibile definire una funzione in qualsiasi porzione del programma in cui è lecito inserire un’istruzione con l’attenzione che questa preceda la sua invocazione; è pertanto possibile dichiarare una funzione anche, per esempio, all’interno del corpo di un’altra funzione o di una struttura di selezione, come nell’esempio seguente:

Il seguente programma calcola l’area di un triangolo o di un rettangolo definendo una funzione area a seconda dell’opzione scelta dall’utente:

print("Calcolo area")
poligono = input("scegliere tra triangolo o rettangolo: ")

if(poligono == "triangolo"):
   def area(b, h):
      return (b * h)/2

elif(poligono == "rettangolo"):
   def area(b, h):
      return (b * h)

else:
   def area(b, h):
      return None

base = float(input("fornire base: "))

altezza = float(input("fornire altezza: "))

print("L’area del", poligono, "è:", area(base, altezza))
Copied!
è sempre possibile assegnare a una variabile il nome di una funzione e invocarla utilizzando il nome della nuova variabile, come per esempio:
def area_trapezio(base_minore, base_maggiore, altezza):
   return ((base_minore + base_maggiore) * altezza)/2
   
a = area_trapezio(2, 3, 6)     
area = area_trapezio a = area(2, 3, 6)
Copied!
in caso di dichiarazioni di funzioni con lo stesso nome (anche se eterogenee nel numero di parametri e corpo) e appartenenti al medesimo blocco di istruzioni, al nome della funzione verrà associata solo l’ultima definizione in ordine di dichiarazione.
Copied!
È possibile dichiarare una funzione che preveda come parametro una funzione. L’esempio seguente mostra una funzione area che accetta come parametro il nome della funzione che implementa il calcolo richiesto per la figura geometrica desiderata.
def area_triangolo(b, h):
   return (b * h)/2

def area_rettangolo(l1, l2):
   return (l1 * l2)

def area_parallelogramma(b, h):
   return (b * h)

def area(funz, x, y):
   return funz(x, y)

base = int(input("base: "))
altezza = int(input("altezza: "))
area_t = area(area_triangolo, base, altezza)
area_r = area(area_rettangolo, base, altezza)
area_p = area(area_parallelogramma, base, altezza)

print("Area triangolo: ", area_t)
print("Area rettangolo: ", area_r)
print("Area parallelogramma: ", area_p)

Il passaggio dei parametri

Nell’uso delle funzioni si distinguono i parametri formali dai parametri attuali: i primi sono quelli definiti nella firma della funzione, mentre i secondi sono quelli specificati al momento dell’invocazione della funzione e sono anche noti come argomenti della funzione.

Al momento dell’invocazione della funzione il valore dei parametri attuali viene assegnato alle variabili che costituiscono i parametri formali della funzione stessa ed è in base a questo valore che viene eseguita la computazione.

Il passaggio dei parametri a una funzione prevede che gli oggetti in memoria, associati alle variabili usate come argomento della funzione al momento della sua invocazione, vengano associati alle «etichette» usate per i parametri formali nella definizione della funzione. Il meccanismo è analogo alla procedura di assegnamento di un valore a una variabile.

ESEMPIO

La seguente funzione incremento restituisce il valore ricevuto, aumentato di un’unità:

def incremento(x):
    x = x + 1
    return x
    
a = 1
b = incremento(a)

Al momento dell’invocazione, il valore del parametro attuale (argomento) a è assegnato alla variabile x che costituisce il parametro formale della funzione, ne consegue che a e x riferiscono la stessa porzione di memoria

Durante l’esecuzione della funzione incremento lo stato della memoria e le associazioni delle variabili mutano come esemplificato in figura:

L’esecuzione dell’istruzione return restituisce il valore calcolato all’istruzione che ha invocato la funzione. Tale valore viene associato alla variabile b

Il meccanismo appena descritto, grazie all’immutabilità di molti oggetti, ha come conseguenza che il passaggio dei parametri a una funzione avviene con una modalità assimilabile a quella generalmente indicata come passaggio per valore: qualsiasi modifica del valore dei parametri, effettuata nel codice della funzione invocata, non modifica in alcun modo il valore delle variabili usate come argomento della funzione invocante.

La funzione scambia, apparentemente, effettua lo scambio del contenuto delle due variabili passate come argomenti:

def scambia(a, b):
   t = a
   a = b
   b = t

n = input("Inserire il primo valore: ")
m = input("Inserire il secondo valore: ")
print("n =", n, "m =", m)

scambia(n, m);

print("n =", n, "m =", m) #anche dopo l'esecuzione della funzione i valori non cambiano

Visibilità

L’organizzazione del codice mediante funzioni introduce un nuovo aspetto caratterizzante le variabili: l’ambito visibilità, cioè l’insieme delle istruzioni in cui la variabile stessa è utilizzabile.

È possibile, quindi, classificare le variabili in base alla loro visibilità, come indicato nel seguito.

  • Variabile globale: è valida dal punto in cui è definita fino al termine del codice contenuto nel file. Una variabile è globale (cioè visibile in tutto il codice successivo alla definizione) solo se è definita esternamente a qualsiasi blocco costituente il corpo di una funzione.
  • Variabile locale: è visibile internamente al corpo della funzione in cui essa è definita e non è possibile accedervi (per modificarne o utilizzarne il valore) al di fuori di esso. L’ambiente interno di una funzione è locale alla funzione stessa: le variabili definite nel corpo della funzione (così come quelle che costituiscono i parametri formali) hanno una visibilità limitata al tempo di esecuzione delle istruzioni della funzione stessa.

ESEMPIO: Dato il seguente programma che calcola l’area del triangolo o del rettangolo in base alla selezione operata dall’utente è possibile individuare le variabili con visibilità locale e globale:

Copied!
non è possibile utilizzare una variabile locale all’esterno della funzione in cui è dichiarata:
Copied!
il corpo di una funzione può utilizzare le variabili definite nel blocco chiamante la funzione stessa; le variabili globali che il corpo della funzione può usare sono tutte le variabili definite dal blocco chiamante fino all’invocazione stessa della funzione

Funzioni ricorsive

Si può osservare che

Copied!
n! = n · (n – 1)!

ovvero che il fattoriale di un numero naturale n è il prodotto tra n stesso e il fattoriale del numero precedente. Ricordando che 0! = 1 e 1! = 1, applicando a ritroso il ragionamento appena illustrato possiamo impostare in maniera intuitiva una funzione per il calcolo di n! (per n ≥ 0):

def fattoriale(n):
   if (n <= 1):
      return 1

   else:
      return n * fattoriale(n – 1)

Prima di entrare nel dettaglio di questa soluzione è possibile osservare che, per com’è formulata, la funzione fattoriale richiama se stessa: questo meccanismo è alla base del concetto di ricorsione

Una funzione può invocare altre funzioni; in generale, quindi, è da considerare valida anche la possibilità che questa possa auto-invocarsi.

Nel caso di una funzione che si auto-invoca si parla di programmazione ricorsiva e l’azione di auto-invocazione è definita ricorsione.

Questo fatto può sembrare paradossale o di scarso rilievo pratico, ma in realtà non è così e, anzi, la ricorsione introduce un livello di astrazione molto impiegato nella programmazione.

ESEMPIO

Nel caso della somma dei primi numeri naturali si potrebbe quindi utilizzare la seguente funzione ricorsiva, dove si è adottato il principio secondo cui «la somma dei primi N numeri è data da N più la somma dei primi N − 1 numeri naturali»

def sommaNum(n):
   if(n == 1):
      return n

   else:
      return (n + sommaNum(n - 1))
   
print(sommaNum(4)) #stampa 10

A ogni invocazione della funzione viene allocata un’area di memoria distinta per l’istanza corrente della funzione stessa, dove i valori delle variabili hanno una validità locale. Schematicamente, nell’esecuzione dell’esempio precedente, per N = 4 si ottiene la rappresentazione grafica seguente:

  • 1 è la chiamata di sommaNum da parte di una funzione esterna;
  • 2, 3, 4 rappresentano le invocazioni ricorsive di sommaNum a se stessa (il return del ramo else rimane sospeso per l’impossibilità di una valutazione immediata di sommaNum (N – 1));
  • 5, 6, 7, 8 costituiscono il percorso inverso di de-allocazione degli spazi di memoria usati durante la ricorsione (adesso è possibile calcolare, passo per passo, la somma tra il valore corrente di N e quello restituito dall’invocazione effettuata al passo precedente).

Costruire un algoritmo ricorsivo per risolvere un dato problema corrisponde, quindi, a dare una definizione per induzione della sua soluzione.

Non a caso due sono le condizioni necessarie, ma non sufficienti, affinché un algoritmo ricorsivo sia corretto (in pratica perché non produca una sequenza infinita di invocazioni):

  • deve esistere almeno una modalità di terminazione che evita l’invocazione ricorsiva (questa condizione corrisponde alla base dell’induzione);
  • almeno un’invocazione ricorsiva deve prevedere la soluzione di un sottoproblema ridotto rispetto all’originale (questa condizione corrisponde all’ipotesi di induzione).

OSSERVAZIONE Un aspetto caratterizzante degli algoritmi ricorsivi è l’assenza dei cicli, presenti invece nelle versioni iterative degli stessi: un’invocazione ricorsiva corrisponde infatti a un’iterazione. Ed è proprio l’assenza di cicli espliciti che contribuisce a una maggiore leggibilità, eleganza e astrattezza dei programmi ricorsivi rispetto a quelli iterativi.

In teoria è possibile implementare qualsiasi algoritmo senza ricorrere a strutture iterative, ma solo in modo ricorsivo: esistono linguaggi di programmazione (in particolare i cosiddetti linguaggi funzionali) che privilegiano l’uso della ricorsione rispetto all’iterazione.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *