Ricorsione
Funzioni e Stack
http://www.dia.uniroma3.it/~roselli/
[email protected]
Credits
• Materiale a cura del Prof. Franco Milicchio
Funzioni
• Nella lezione precedente abbiamo introdotto le funzioni
• Nella trattazione sono state omesse le note implementative
• Ovvero, come Python esegue una chiamata a funzione
• Una funzione è un blocco di istruzioni: ogni blocco può avere variabili locali
• In Python un blocco può avere accesso a tutte le variabili del blocco che lo
contiene, ovviamente se definite in precedenza
• In altre parole, i blocchi figli possono accedere ai simboli definiti nei blocchi
padri, ma non viceversa
Scope
La funzione f può
accedere a q
import math
q = 3
def f(i):
return i * math.sqrt(q)
print f(3)
p = 6
La funzione f non ha
accesso a p
Scope
• Lo scope di una variabile, talvolta tradotto come “visibilità”, è la zona di
visibilità di una variabile
• Ogni identificatore, in realtà, ha uno scope, sia esso una variabile o una
funzione
• In Python ne esistono tre tipi di scope: Funzione, Modulo, e Globale
• Se un identificatore viene definito all’interno di un sotto-blocco, oscura la
visibilità di quello precedente
• Quindi lo scope predilige la località
Scope
import math
q = 3
La q locale oscura la q
nel blocco precedente
def f(i):
q = 4
return i * math.sqrt(q)
p = 6
Scope di q
print f(3), q
Scope di i
6.0 3
La q globale è invariata
Stack
• Python è in realtà una macchina a stack
• Per eseguire qualunque istruzione, Python utilizza una zona privata di
memoria detta stack, talvolta tradotto in italiano con “pila”
• È una memoria LIFO, Last In First Out, ovvero l’ultimo dato messo in memoria
è il primo ad essere eliminato
• Le operazioni principali su uno stack sono push e pop
Stack
push 6
Stack
push 1
pop
push 9
1
9
6
Stack Diagram
• Per comprendere come Python esegue un programma, osserviamo il
diagramma dello stack
• Ogni volta che viene incontrata una chiamata a funzione, Python usa lo stack
• Nello stack Python immette (push), in un modello semplificato:
• Linea di codice della chiamata a funzione
• Argomenti passati alla funzione
• Quando esce dalla funzione, Python rimuove (pop) i valori, e trova la riga dove
continuare l’esecuzione del codice
Esecuzione
import math
Esegue
math.sqrt
Stack
q = 3
def f(i):
return i * math.sqrt(q)
p = 6
print f(-2), p, q
3
linea 6
-2
linea 10
Funzioni
• Abbiamo dunque visto come Python esegue le chiamate a funzione
• Ovviamente è un modello estremamente semplificato
• Questo meccanismo funziona bene, e fornisce una potente macchina per
eseguire funzioni complesse
• Tra queste, alcune interessanti e utilizzate funzioni matematiche, come la
funzione fattoriale
• La funzione fattoriale è usata in ambiti diversi, dall’analisi al calcolo delle
probabilità
Fattoriale
Caso base
n! :=
⇢
1,
n(n
Ricorsione
1)!,
n=0
n>0
Funzioni Ricorsive
• Senza formalizzare in modo estremo, definiamo funzione o algoritmo ricorsivo
una funzione che richiama se stessa
• L’esempio del fattoriale è chiaro, ma vi sono molte altre funzioni ricorsive
(e.g., Serie di Fibonacci, Massimo Comune Divisore)
• Il codice per definire il fattoriale rispecchia la definizione matematica
• Una macchina a stack può eseguire funzioni ricorsive
def fattoriale(n):
if n == 0:
return 1
return n * fattoriale(n - 1)
Ricorsione e Stack
import math
def fattoriale(n):
if n == 0:
return 1
return n * fattoriale(n - 1)
print fattoriale(6)
Stack
0
…
4
linea 8
5
linea 8
6
linea 10
Caveat
• Una funzione ricorsiva usa intensivamente lo stack
• La memoria comunque è limitata, e lo stack è una risorsa preziosa
• Cosa succede se si esaurisce lo stack?
• Python ha un limite massimo di chiamate a funzioni ricorsive
• Se si eccede il limite, Python interrompe il programma
• Questa situazione può accadere per errore, oppure perché la dimensione del
problema è troppo grande per un approccio ricorsivo naïve
Passaggio di Parametri
import math
def fattoriale(n):
if n == 0:
return 1
Il caso base non
verrà mai eseguito
return n * fattoriale(n - 1)
print fattoriale(-6)
Traceback (most recent call last):
File "/Users/sensei/Desktop/aaa/a.py", line 10, in <module>
print fattoriale(-6)
File "/Users/sensei/Desktop/aaa/a.py", line 8, in fattoriale
return n * fattoriale(n - 1)
File "/Users/sensei/Desktop/aaa/a.py", line 8, in fattoriale
return n * fattoriale(n - 1)
...
File "/Users/sensei/Desktop/aaa/a.py", line 8, in fattoriale
return n * fattoriale(n - 1)
RuntimeError: maximum recursion depth exceeded
Ricerca in Lista
• Altro esempio di funzione ricorsiva è la ricerca in una lista ordinata
• Il problema è come segue: sia l una lista di numeri ordinati (ad esempio in
senso crescente)
• Scrivere una funzione che, dato un numero q, ritorni la posizione all’interno
della lista di q, -1 se il numero non è presente nella lista
• Confrontiamo due algoritmi ricorsivi, la ricerca lineare e quella binaria
• Vedremo presto che quest’ultimo algoritmo è più efficiente
Ricerca Lineare
l
l
l[0] == q
lin(l, n) :=
˜l = (li ), 8i
⇢
0,
1 + lin(˜l, n
1
1),
l0 = q
l0 6= q
Ricerca Lineare
def lin(l, n, q):
if l[0] == q:
return 0
else:
return 1 + lin(l[1:], n - 1, q)
p = [1, 2, 3, 4, 5, 6]
print lin(p, len(p), 4)
Quali errori sono presenti in questo codice?
Ricerca Binaria
• Questa ricerca appena introdotta non è efficiente
• Prima di tutto, la lista è ordinata, ma questo non viene sfruttato
• Sarebbe quindi più efficiente un algoritmo divide et impera, ovvero dividere lo
spazio di ricerca in due porzioni
• Se la lista è ordinata, vi è una sottolista maggiore ed una minore dell’elemento
da ricercare
• Le due sottoliste sono una partizione, non hanno elementi in comune
Ricerca Binaria
l
l1
l2
l[n/2] == q
8
lm = q
< 0,
bin(l1 , q), lm > q
bin(l, n) :=
:
bin(l2 , q), lm  q
l1 = (li ), 8i  len(l)/2
l2 = (li ), 8i > len(l)/2
Ricerca Binaria
def bin(l, q):
if len(l) <= 0:
return -1
d = len(l) / 2
if l[d] == q:
print ">> Trovato"
return d
if q < l[d]:
return bin(l[:d], q)
else:
return d + bin(l[d:], q)
p = [0, 1, 2, 3, 4, 5, 6]
print bin(p, 6)
Quali errori sono presenti in questo codice?
Complessità
• Ogni algoritmo ha una complessità asintotica
• Questa è una proprietà di ogni algoritmo, e ne misura il comportamento limite
• Ciò che si misura in un algoritmo è il tempo, e lo spazio occupato in memoria
• Vedremo presto nuovamente questi due algoritmi e ne studieremo la
complessità