🔄 Funzioni Ricorsive e Decoratori
Scopri come le funzioni possono chiamare sé stesse e come estenderne le funzionalità con i decoratori
🎯 Funzioni Ricorsive
Le funzioni ricorsive sono funzioni che chiamano sé stesse per risolvere problemi che possono essere suddivisi in sotto-problemi più piccoli.
📝 Struttura di una funzione ricorsiva:
- Caso base → la condizione che interrompe la ricorsione (evita chiamate infinite)
- Caso ricorsivo → la funzione richiama sé stessa con un problema più piccolo
🔢 Esempio 1: Calcolare il Fattoriale
Il fattoriale di un numero è il prodotto di tutti i numeri interi positivi minori o uguali a quel numero.
La formula matematica è: n! = n × (n-1)!
Definizione: 0! = 1 (caso base), 5! = 5 × 4!, 4! = 4 × 3!, …
def fattoriale(n):
"""
Calcola il fattoriale di un numero in modo ricorsivo.
Parametri:
- n (int): Numero intero positivo di cui calcolare il fattoriale.
Ritorna:
- int: Fattoriale di n.
"""
if n == 0:
return 1 # Caso base: il fattoriale di 0 è 1
return n * fattoriale(n - 1) # Caso ricorsivo: n × (n-1)!
# Test della funzione
print(fattoriale(5)) # Output: 120
print(fattoriale(0)) # Output: 1
print(fattoriale(3)) # Output: 6
💡 Spiegazione dettagliata:
- Se
n == 0, restituisce1perché per definizione 0! = 1 - Se
n > 0, la funzione chiama sé stessa conn-1, moltiplicando i risultati - La chiamata ricorsiva continua finché
nnon diventa 0 (caso base)
🔢 Esempio 2: Serie di Fibonacci
La Serie di Fibonacci è una successione numerica in cui ogni numero è la somma dei due precedenti:
F(n) = F(n-1) + F(n-2)
Valori iniziali: F(0) = 0, F(1) = 1, F(2) = 1, F(3) = 2, F(4) = 3, F(5) = 5, …
def fibonacci(n):
"""
Calcola il numero di Fibonacci alla posizione n in modo ricorsivo.
Parametri:
- n (int): La posizione nella serie di Fibonacci.
Ritorna:
- int: Il numero di Fibonacci corrispondente.
"""
if n == 0:
return 0 # Caso base 1
elif n == 1:
return 1 # Caso base 2
return fibonacci(n - 1) + fibonacci(n - 2) # Somma i due numeri precedenti
# Test della funzione
print(fibonacci(7)) # Output: 13
print(fibonacci(5)) # Output: 5
print(fibonacci(10)) # Output: 55
💡 Spiegazione dettagliata:
- Se
n == 0, la funzione restituisce0 - Se
n == 1, la funzione restituisce1 - Per
n > 1, la funzione calcolaF(n-1) + F(n-2), sommandoli
⚠️ Problema di Performance:
Questa versione è lenta per valori grandi di n perché ripete calcoli già effettuati.
➡ Soluzione: Ottimizziamola con la tecnica di memoization usando un decoratore.
🎭 Decoratori in Python
Un decoratore è una funzione che modifica il comportamento di un’altra funzione senza alterarne il codice originale.
✅ Utilizzi comuni dei decoratori:
- Misurazione del tempo di esecuzione
- Caching (salvataggio dei risultati per evitare ricalcoli)
- Controllo accessi e autorizzazioni
- Logging e debugging
⚡ Esempio 3: Fibonacci Ottimizzato con Decoratore
Ottimizziamo il calcolo di Fibonacci usando un decoratore per caching (functools.lru_cache).
from functools import lru_cache
@lru_cache(maxsize=None) # Memorizza i risultati per evitare ricalcoli
def fibonacci_ottimizzato(n):
"""
Calcola il numero di Fibonacci alla posizione n in modo efficiente.
Parametri:
- n (int): La posizione nella serie di Fibonacci.
Ritorna:
- int: Il numero di Fibonacci corrispondente.
"""
if n == 0:
return 0
elif n == 1:
return 1
return fibonacci_ottimizzato(n - 1) + fibonacci_ottimizzato(n - 2)
# Test della funzione ottimizzata
print(fibonacci_ottimizzato(35)) # Output: 9227465 (molto veloce)
print(fibonacci_ottimizzato(40)) # Output: 102334155 (istantaneo)
💡 Spiegazione dettagliata:
@lru_cache(maxsize=None): memorizza i risultati delle chiamate precedenti per riutilizzarli- Velocità: la funzione evita di ripetere gli stessi calcoli, rendendola molto più rapida
maxsize=None: memorizza tutti i risultati senza limiti di dimensione
🛠️ Esercitazione Guidata
📝 Esercizio: Somma degli elementi di una lista
Scrivi una funzione somma_lista(lista) che calcoli la somma di tutti gli elementi di una lista usando la ricorsione, senza usare sum().
📋 Passaggi suggeriti:
- Se la lista è vuota, restituisci
0(caso base) - Se la lista ha elementi, somma il primo elemento al risultato della chiamata ricorsiva sulla lista rimanente
def somma_lista(lista):
"""
Calcola la somma degli elementi di una lista usando la ricorsione.
Parametri:
- lista (list): Lista di numeri da sommare.
Ritorna:
- int/float: Somma degli elementi della lista.
"""
if not lista: # Se la lista è vuota
return 0 # Caso base: somma di lista vuota è 0
# Caso ricorsivo: primo elemento + somma del resto della lista
return lista[0] + somma_lista(lista[1:])
# Test della funzione
print(somma_lista([1, 2, 3, 4, 5])) # Output: 15
print(somma_lista([10, 20, 30])) # Output: 60
print(somma_lista([])) # Output: 0
🎯 10 Tracce di Esercizi con Soluzioni
1️⃣ Contare le occorrenze di un carattere in una stringa
def conta_occorrenze(stringa, carattere):
"""
Conta quante volte un carattere compare in una stringa usando la ricorsione.
Parametri:
- stringa (str): La stringa in cui cercare.
- carattere (str): Il carattere da contare.
Ritorna:
- int: Numero di occorrenze del carattere.
"""
if not stringa: # Caso base: stringa vuota
return 0
# Caso ricorsivo: 1 se il primo carattere matcha, più il conteggio nel resto
return (1 if stringa[0] == carattere else 0) + conta_occorrenze(stringa[1:], carattere)
# Test
print(conta_occorrenze("programmazione", "a")) # Output: 3
print(conta_occorrenze("python", "z")) # Output: 0
2️⃣ Merge Sort (ordinamento ricorsivo)
def merge_sort(lista):
"""
Ordina una lista usando l'algoritmo Merge Sort ricorsivo.
Parametri:
- lista (list): Lista da ordinare.
Ritorna:
- list: Lista ordinata.
"""
if len(lista) <= 1:
return lista # Caso base: lista vuota o con un elemento è già ordinata
metà = len(lista) // 2
sinistra = merge_sort(lista[:metà]) # Ordina la prima metà
destra = merge_sort(lista[metà:]) # Ordina la seconda metà
# Unisce le due metà ordinate
return unisci(sinistra, destra)
def unisci(sinistra, destra):
"""Unisce due liste ordinate in una lista ordinata."""
risultato = []
i = j = 0
while i < len(sinistra) and j < len(destra):
if sinistra[i] <= destra[j]:
risultato.append(sinistra[i])
i += 1
else:
risultato.append(destra[j])
j += 1
# Aggiungi gli elementi rimanenti
risultato.extend(sinistra[i:])
risultato.extend(destra[j:])
return risultato
# Test
print(merge_sort([64, 34, 25, 12, 22, 11, 90])) # Output: [11, 12, 22, 25, 34, 64, 90]
3️⃣ Invertire una stringa
def inverti_stringa(s):
"""
Inverte una stringa usando la ricorsione.
Parametri:
- s (str): Stringa da invertire.
Ritorna:
- str: Stringa invertita.
"""
if len(s) <= 1:
return s # Caso base: stringa vuota o con un carattere
# Caso ricorsivo: ultimo carattere + inversione del resto
return inverti_stringa(s[1:]) + s[0]
# Test della funzione
print(inverti_stringa("python")) # Output: "nohtyp"
print(inverti_stringa("hello")) # Output: "olleh"
print(inverti_stringa("a")) # Output: "a"
💡 Spiegazione: La funzione prende l'ultimo carattere (s[0]) e lo mette alla fine della stringa invertita del resto (s[1:]).
Il processo continua ricorsivamente finché non rimane un solo carattere.
4️⃣ Potenza di un numero
def potenza(base, esp):
"""
Calcola la potenza di un numero usando la ricorsione.
Parametri:
- base (int/float): Base della potenza.
- esp (int): Esponente (numero intero non negativo).
Ritorna:
- int/float: Risultato della potenza.
"""
if esp == 0:
return 1 # Caso base: qualsiasi numero elevato a 0 è 1
# Caso ricorsivo: base * base^(esp-1)
return base * potenza(base, esp - 1)
# Test della funzione
print(potenza(2, 3)) # Output: 8
print(potenza(5, 2)) # Output: 25
print(potenza(10, 0)) # Output: 1
print(potenza(3, 4)) # Output: 81
💡 Spiegazione: La funzione sfrutta la proprietà matematica che base^esp = base × base^(esp-1).
Il caso base è quando l'esponente è 0, dove qualsiasi numero elevato a 0 restituisce 1.
5️⃣ Trovare il massimo in una lista
def massimo_lista(lista):
"""
Trova il valore massimo in una lista usando la ricorsione.
Parametri:
- lista (list): Lista di numeri.
Ritorna:
- int/float: Valore massimo nella lista.
"""
if len(lista) == 1:
return lista[0] # Caso base: lista con un solo elemento
# Caso ricorsivo: confronta il primo elemento con il massimo del resto
max_del_resto = massimo_lista(lista[1:])
return lista[0] if lista[0] > max_del_resto else max_del_resto
# Test della funzione
print(massimo_lista([3, 1, 4, 1, 5, 9, 2])) # Output: 9
print(massimo_lista([-5, -2, -10, -1])) # Output: -1
print(massimo_lista([42])) # Output: 42
💡 Spiegazione: La funzione confronta ricorsivamente il primo elemento con il massimo del resto della lista.
Quando la lista ha un solo elemento, quel elemento è il massimo (caso base).
6️⃣ Contare le cifre di un numero
def conta_cifre(n):
"""
Conta il numero di cifre in un numero intero usando la ricorsione.
Parametri:
- n (int): Numero di cui contare le cifre.
Ritorna:
- int: Numero di cifre.
"""
if n < 0:
return conta_cifre(-n) # Gestione numeri negativi
if n < 10:
return 1 # Caso base: numero con una cifra
# Caso ricorsivo: 1 + conteggio cifre del numero senza l'ultima cifra
return 1 + conta_cifre(n // 10)
# Test della funzione
print(conta_cifre(12345)) # Output: 5
print(conta_cifre(7)) # Output: 1
print(conta_cifre(0)) # Output: 1
print(conta_cifre(-987)) # Output: 3
💡 Spiegazione: La funzione rimuove l'ultima cifra dividendo per 10 (n // 10) e conta ricorsivamente.
Il caso base è quando il numero è minore di 10 (ha una sola cifra). Gestisce anche i numeri negativi.
7️⃣ Somma delle cifre di un numero
def somma_cifre(n):
"""
Calcola la somma delle cifre di un numero usando la ricorsione.
Parametri:
- n (int): Numero di cui sommare le cifre.
Ritorna:
- int: Somma delle cifre.
"""
if n < 0:
return somma_cifre(-n) # Gestione numeri negativi
if n == 0:
return 0 # Caso base: numero 0
# Caso ricorsivo: ultima cifra + somma delle cifre rimanenti
return (n % 10) + somma_cifre(n // 10)
# Test della funzione
print(somma_cifre(12345)) # Output: 15 (1+2+3+4+5)
print(somma_cifre(987)) # Output: 24 (9+8+7)
print(somma_cifre(0)) # Output: 0
print(somma_cifre(-456)) # Output: 15 (4+5+6)
💡 Spiegazione: La funzione estrae l'ultima cifra con n % 10 e somma ricorsivamente con le cifre rimanenti (n // 10).
Il caso base è quando il numero è 0. Gestisce anche i numeri negativi.
8️⃣ Verificare se una stringa è un palindromo
def is_palindromo(s):
"""
Verifica se una stringa è un palindromo usando la ricorsione.
Parametri:
- s (str): Stringa da verificare.
Ritorna:
- bool: True se la stringa è un palindromo, False altrimenti.
"""
# Pulisci la stringa (rimuovi spazi e converti in minuscolo)
s = s.replace(" ", "").lower()
if len(s) <= 1:
return True # Caso base: stringa vuota o con un carattere
# Caso ricorsivo: primo e ultimo carattere uguali e il resto è palindromo
if s[0] != s[-1]:
return False
return is_palindromo(s[1:-1])
# Test della funzione
print(is_palindromo("radar")) # Output: True
print(is_palindromo("python")) # Output: False
print(is_palindromo("Amore Roma")) # Output: True
print(is_palindromo("a")) # Output: True
print(is_palindromo("")) # Output: True
💡 Spiegazione: La funzione confronta il primo e l'ultimo carattere. Se sono uguali, verifica ricorsivamente se la sottostringa interna è un palindromo.
Il caso base è quando la stringa ha 0 o 1 carattere (sempre palindromo). Gestisce anche spazi e maiuscole/minuscole.
9️⃣ Moltiplicazione ricorsiva
def moltiplica(a, b):
"""
Moltiplica due numeri interi usando la ricorsione e l'addizione.
Parametri:
- a (int): Primo numero.
- b (int): Secondo numero.
Ritorna:
- int: Prodotto di a e b.
"""
if b == 0:
return 0 # Caso base: qualsiasi numero moltiplicato per 0 è 0
if b < 0:
return -moltiplica(a, -b) # Gestione numeri negativi
# Caso ricorsivo: a + a × (b-1)
return a + moltiplica(a, b - 1)
# Test della funzione
print(moltiplica(5, 3)) # Output: 15
print(moltiplica(7, 4)) # Output: 28
print(moltiplica(0, 5)) # Output: 0
print(moltiplica(6, -2)) # Output: -12
print(moltiplica(-3, 4)) # Output: -12
💡 Spiegazione: La funzione implementa la moltiplicazione come addizione ripetuta.
a × b = a + a × (b-1). Il caso base è quando b è 0. Gestisce anche i numeri negativi.
🔟 Ricerca binaria ricorsiva
def ricerca_binaria(lista, target, inizio=0, fine=None):
"""
Cerca un elemento in una lista ordinata usando la ricerca binaria ricorsiva.
Parametri:
- lista (list): Lista ordinata in cui cercare.
- target: Elemento da trovare.
- inizio (int): Indice di inizio della ricerca.
- fine (int): Indice di fine della ricerca.
Ritorna:
- int: Indice dell'elemento se trovato, -1 altrimenti.
"""
if fine is None:
fine = len(lista) - 1
if inizio > fine:
return -1 # Caso base: elemento non trovato
mezzo = (inizio + fine) // 2
if lista[mezzo] == target:
return mezzo # Elemento trovato
elif lista[mezzo] > target:
# Cerca nella metà sinistra
return ricerca_binaria(lista, target, inizio, mezzo - 1)
else:
# Cerca nella metà destra
return ricerca_binaria(lista, target, mezzo + 1, fine)
# Test della funzione
numeri = [1, 3, 5, 7, 9, 11, 13, 15]
print(ricerca_binaria(numeri, 7)) # Output: 3
print(ricerca_binaria(numeri, 1)) # Output: 0
print(ricerca_binaria(numeri, 15)) # Output: 7
print(ricerca_binaria(numeri, 8)) # Output: -1 (non trovato)
💡 Spiegazione: La ricerca binaria divide ricorsivamente la lista a metà. Se l'elemento al centro è il target, restituisce l'indice.
Se è maggiore, cerca nella metà sinistra; se è minore, cerca nella metà destra. Molto efficiente per liste ordinate (O(log n)).