Une fonction f est dite récursive si son exécution peut provoquer un ou plusieurs appels de la fonction f elle-même.
La conception d’une fonction récursive est analogue à un raisonnement par récurrence.
Dans les exemples ci-dessous, on propose une version itérative et une ou plusieurs versions récursives.
On considère la suite définie par \(u_0 = 1\) et \(u_n = 2 u_{n-1}\). (Rq : on vérifie sans peine que \(u_n = 2^n\)).
Dans le code d'une fonction récursive f, deux points sont essentiels :
- la présence d'un test d'arrêt, la fonction retourne (instruction return) la valeur correpondant à l'initialisation ;
- l'appel récursif (précédé de l'instruction return) qui correspond à la relation de récurrence
Remarquer dans le code ci-dessous que les itérations (calculs répétés) effectuées grâce à la boucle for dans la fonction itérative sont remplacées par les appels successifs dans la fonction récursive (il n'y a pas de boucle dans la fonction récursive dans cet exemple).
Le test d'arrêt doit impérativement précéder l'appel récursif sous peine d'appels imbriqués conduisant à une erreur :
RecursionError: maximum recursion depth exceeded
.
Remarque : le nombre d'appels récursifs est limité par défaut à 1000 par python.
Il est possible de modifier ce nombre :
import sys
sys.setrecursionlimit(5000)
.
Cliquer sur le bouton "Next" dans l'animation ci-dessous et visualiser les appels successifs (n diminue) suivis de la phase de remontée : lorsque n vaut 0, la fonction renvoie la valeur 1 qui est transmise à l'appel précédent et ainsi de suite.
Les appels successifs sont empilés dans une pile au fur et à mesure de l'exécution :
Etat de la pile | ||||||||
---|---|---|---|---|---|---|---|---|
1er appel | 2ème appel | 3ème appel | 4ème appel | 5ème appel | Dépilage u(0) | Dépilage u(1) | Dépilage u(2) | Dépilage u(3) |
u(0) → 1 | ||||||||
u(1)=2*u(0) | u(1)=2*u(0) | u(1)=2*1 → 2 | ||||||
u(2)=2*u(1) | u(2)=2*u(1) | u(2)=2*u(1) | u(2)=2*u(1) | u(2)=2*2 → 4 | ||||
u(3)=2*u(2) | u(3)=2*u(2) | u(3)=2*u(2) | u(3)=2*u(2) | u(3)=2*u(2) | u(3)=2*u(2) | u(3)=2*4 → 8 | ||
u(4)=2*u(3) | u(4)=2*u(3) | u(4)=2*u(3) | u(4)=2*u(3) | u(4)=2*u(3) | u(4)=2*u(3) | u(4)=2*u(3) | u(4)=2*u(3) | u(4)=2*8 → 16 |
Il est impératif de sauvegarder régulièrement le code en le collant dans un fichier texte (Notepad++) ou dans l'éditeur de pyzo.
sommeI(n:int)->int
(cette écriture signifie que le paramètre n est un entier et que la fonction renvoie un entier)
renvoyant \(\displaystyle\sum_{i=0}^{n} i \).sommeR(n:int)->int
renvoyant \(\displaystyle\sum_{i=0}^{n} i \). L'écriture def f(paramètre:type)->type
s'appelle une annotation, elle permet
de préciser le type des paramètres et le type de la grandeur renvoyée afin de faciliter la compréhension deu code (sans effet sur l'exécution : aucune vérification n'est effectuée).
Rappels - Slicing : L[i:]
renvoie la sous-liste composée des éléments de L à partir de l'index i ; L[-1]
renvoie le dernier élément de L.
En déduire ce que renvoie L[:-1]
puis vérifier sur un exemple.
presentI(L:list,e)->bool
renvoyant un booléen (True ou False) suivant que l'élément e est présent ou non dans la liste L.presentR(L:list,e)->bool
effectuant le même travail. Plusieurs solutions sont envisageables suivant que
l'élément enlevé de la liste est le premier ou le dernier.Tester les commandes suivantes : False + 5 ; True + 5. Conclusion ?
Que fait la fonction suivante ? Expliquer son fonctionnement.
Remarque les parenthèses autour de L[0]==e
sont indispensables pour forcer l'évaluation.
On pourrait aussi écrire int((L[0]==e))
.
Algorithme : pgcd(a, 0) = a et pgcd(a, b) = pgcd(b, a%b)
« Pour calculer le pgcd de a et de b, on remplace a par b et b par a%b tant que b n'est pas nul »
Rq : au cours de cet algorithme a et b sont modfiés.
Rappel : a%b
est le reste dans la division Euclidienne de a par b.
pgcdI(a,b:int)->int
.pgcdR(a,b:int)->int
. Rappel : 'a'*4
renvoie aaaa.
Ecrire une fonction récursive f(n) renvoyant, avec n = 5 :
*****
****
***
**
*
Ecrire une fonction récursive g(n) renvoyant, avec n = 5 :
*
**
***
****
*****
On considère la suite définie par \(u_0 = 2\) et \(u_n = \displaystyle\frac{1}{2} \left( u_{n-1} + \displaystyle\frac{3}{u_{n-1}} \right)\).
Pour quelle raison le code suivant est-il terriblement maladroit ?
Le problème est resolu en utilisant une variable permettant de stocker le résultat de \(u_{n-1}\) :
Prolongement : exercice sur la suite de Fibonacci (plus loin) pour un exemple de suite où \(u_n\) est définie à patir de \(u_{n-1}\) et \(u_{n-2}\).
Que fait la fonction suivante ? Expliquer son fonctionnement.
factorielleI(n:int)->int
renvoyant \(n !\).factorielleR(n:int)->int
.On souhaite comparer les performances de différentes fonctions programmées en langage python (factorielle, puissance...) à celles des bibliothèques scientifiques
scipy et numpy.
On utilise la fonction perf_counter() du module time qui renvoie une durée écoulée depuis un instant origine : on accède à une durée en effectuant la différence entre deux valeurs consécutives
renvoyées par perf_counter().
La fonction "temps" ci-dessous permet de calculer un temps moyen d'exécution d'une fonction. L'ordinateur étant un système multitâche, il n'est pas évident que
toutes les ressources disponibles (processeur, mémoire...) soient les mêmes d'un calcul à l'autre, on effectue donc une moyenne sur un grand nombre d'exécutions.
La fonction "temps()" ci-dessous est utilisée pour évaluer des temps de calcul : regarder le code pour en comprendre les étapes essentielles (ignorer la ligne print(...)) et observer l'exemple d'utilisation.
Pour évaluer la moyenne de 1000 exécutions de la fonction \(f(x,y)\) avec les arguments x = -2 et y = 5, le code à saisir est :
temps(f,-2,5)
Opérateur splat : def f(*args):
permet de définir une fonction avec un nombre variable de paramètres.
Comparer les temps d'exécution de factorielleI, factorielleR, math.factorial, numpy.math.factorial et scipy.math.factorial avec n = 200.
Rappel : \(\left\lfloor a \right\rfloor\) désigne la partie entière de a. En python, \(\left\lfloor \frac{a}{2} \right\rfloor\) est obtenu par a//2.
Pour tester la parité d'un entier n, il suffit de calculer le reste dans la division Euclidienne de n par 2 : n % 2
en python.
puissanceI(x:float,n:int)->float
renvoyant \(x^n\) sans utiliser l'opérateur ** ou une fonction python prédéfinie.puissanceR(x:float,n:int)->float
.puissance2(x:float,n:int)->float
en utilisant l'algorithme d'exponentiation rapide.Comparer les temps d'exécution de puissanceI, puissanceR, puissance2, math.pow, numpy.math.pow et scipy.math.pow avec x = 2.5 et n = 500.
Rappels - Matrices
La suite de Fibonacci est définie par : \(\left\{ \begin{array}{l}{F_0} = 1\\{F_1} = 1\\{F_n} = {F_{n - 1}} + {F_{n - 2}}\,\,\,\,\,n \ge 2\end{array} \right.\).
fibonacciI(n:int)->int
.fibonacciR(n:int)->int
.fibonacciM(n:int)->int
plus efficace.Comparer les temps d'exécution de fibonacciI et fibonacciM.