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à