Table of Contents
La Recursion en Python : Comprendre et Optimiser
Introduction
La recursion est l’une des techniques de programmation les plus elegantes et puissantes, mais aussi l’une des plus mal comprises. Elle consiste a resoudre un probleme en le decomposant en sous-problemes identiques mais plus petits, jusqu’a atteindre un cas de base trivial.
En Python, la recursion est omnipresente : des algorithmes de tri comme le quicksort aux parcours d’arbres, en passant par le traitement de structures de donnees imbriquees comme le JSON. Comprendre comment Python gere les appels recursifs au niveau des environnements d’execution est essentiel pour ecrire du code performant et eviter les pieges classiques comme le depassement de pile.
Dans cet article, nous allons explorer en profondeur :
- Le fonctionnement interne des environnements d’execution Python
- Les differentes formes de recursion (directe, indirecte, tail recursion)
- Les techniques d’optimisation essentielles
- Les bonnes pratiques et erreurs a eviter
Comment La Recursion Fonctionne
Les Environnements d’Execution
Pour comprendre comment fonctionne la recursion, il est essentiel de maitriser les notions fondamentales d’environnement et de variables locales. Chaque fois qu’une fonction est appelee, Python cree un nouveau frame (cadre d’execution) qui contient :
- Les variables locales de la fonction
- Les parametres passes a la fonction
- Une reference vers l’environnement parent (pour la portee des variables)
- L’adresse de retour apres l’execution
Voici un exemple classique avec la fonction factorielle :
def factorial(n):
"""Calcule le factoriel de n de maniere recursive."""
# Cas de base : condition d'arret
if n == 0:
return 1
# Cas recursif : on se rapproche du cas de base
else:
return n * factorial(n - 1)
# Exemple d'utilisation
print(factorial(5)) # Affiche: 120
Visualisation de la Pile d’Appels
Lorsque la fonction factorial est appelee avec l’argument 3, voici ce qui se passe dans la pile d’appels :
# Visualisation de la pile d'appels pour factorial(3)
#
# Appel 1: factorial(3) -> n=3, attend factorial(2)
# Appel 2: factorial(2) -> n=2, attend factorial(1)
# Appel 3: factorial(1) -> n=1, attend factorial(0)
# Appel 4: factorial(0) -> n=0, retourne 1
# Appel 3: retourne 1 * 1 = 1
# Appel 2: retourne 2 * 1 = 2
# Appel 1: retourne 3 * 2 = 6
# Pour visualiser la pile en temps reel :
import sys
def factorial_debug(n, depth=0):
"""Version avec affichage de la pile d'appels."""
indent = " " * depth
print(f"{indent}factorial({n}) appele")
if n == 0:
print(f"{indent}Cas de base atteint, retourne 1")
return 1
else:
result = n * factorial_debug(n - 1, depth + 1)
print(f"{indent}factorial({n}) retourne {result}")
return result
factorial_debug(4)
Techniques d’Optimisation
La recursion peut entrainer des problemes de performance et de memoire si elle n’est pas geree correctement. Voici les principales techniques d’optimisation :
1. Memoization avec lru_cache
Le decorateur lru_cache du module functools permet de mettre en cache les resultats des appels precedents, evitant ainsi les calculs redondants :
from functools import lru_cache
@lru_cache(maxsize=None)
def fibonacci(n):
"""Fibonacci optimise avec memoization."""
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
# Sans cache : O(2^n) - extremement lent
# Avec cache : O(n) - tres rapide
print(fibonacci(100)) # Instantane grace au cache
2. Tail Recursion (Recursion Terminale)
La recursion terminale est une forme ou l’appel recursif est la derniere operation de la fonction. Bien que Python n’optimise pas automatiquement cette forme, elle facilite la conversion en boucle :
def factorial_tail(n, accumulator=1):
"""Factoriel en recursion terminale."""
if n == 0:
return accumulator
# L'appel recursif est la derniere operation
return factorial_tail(n - 1, accumulator * n)
# Equivalent iteratif :
def factorial_iterative(n):
"""Version iterative equivalente."""
result = 1
for i in range(1, n + 1):
result *= i
return result
3. Conversion en Iteration
Pour eviter le depassement de pile, convertissez les fonctions recursives en boucles :
def fibonacci_iterative(n):
"""Fibonacci sans recursion - O(n) temps, O(1) espace."""
if n <= 1:
return n
prev, current = 0, 1
for _ in range(2, n + 1):
prev, current = current, prev + current
return current
Exemples Pratiques
Parcours d’Arbre Recursif
def parcours_arbre(node, depth=0):
"""Parcours en profondeur d'une structure arborescente."""
if node is None:
return
indent = " " * depth
print(f"{indent}{node['name']}")
for child in node.get('children', []):
parcours_arbre(child, depth + 1)
# Exemple d'utilisation
arbre = {
'name': 'racine',
'children': [
{'name': 'enfant1', 'children': [
{'name': 'petit-enfant1'},
{'name': 'petit-enfant2'}
]},
{'name': 'enfant2'}
]
}
parcours_arbre(arbre)
Aplatissement de Listes Imbriquees
def flatten(nested_list):
"""Aplatit une liste imbriquee de maniere recursive."""
result = []
for item in nested_list:
if isinstance(item, list):
result.extend(flatten(item))
else:
result.append(item)
return result
# Exemple
data = [1, [2, 3, [4, 5]], 6, [7, [8, [9]]]]
print(flatten(data)) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
Recherche Binaire Recursive
def binary_search(arr, target, low=0, high=None):
"""Recherche binaire recursive."""
if high is None:
high = len(arr) - 1
if low > high:
return -1 # Element non trouve
mid = (low + high) // 2
if arr[mid] == target:
return mid
elif arr[mid] > target:
return binary_search(arr, target, low, mid - 1)
else:
return binary_search(arr, target, mid + 1, high)
# Exemple
sorted_list = [1, 3, 5, 7, 9, 11, 13, 15]
print(binary_search(sorted_list, 7)) # 3
Bonnes Pratiques
1. Toujours Definir un Cas de Base Clair
# Bon : cas de base explicite et atteignable
def countdown(n):
if n <= 0: # Cas de base clair
print("Termine!")
return
print(n)
countdown(n - 1)
2. Verifier la Limite de Recursion
import sys
# Afficher la limite actuelle
print(sys.getrecursionlimit()) # Par defaut: 1000
# Augmenter si necessaire (avec precaution)
sys.setrecursionlimit(2000)
# Mieux : verifier avant d'executer
def safe_recursive_function(n):
if n > sys.getrecursionlimit() - 100:
raise ValueError(f"Valeur trop grande : {n}")
# ... reste de la fonction
3. Utiliser des Accumulateurs pour Reduire la Complexite
def sum_list(lst, accumulator=0):
"""Somme avec accumulateur - evite de creer des frames inutiles."""
if not lst:
return accumulator
return sum_list(lst[1:], accumulator + lst[0])
4. Preferer l’Iteration pour les Grandes Donnees
# Eviter pour de grandes donnees :
def recursive_sum(lst):
if not lst:
return 0
return lst[0] + recursive_sum(lst[1:])
# Preferer :
def iterative_sum(lst):
return sum(lst) # Utilise les built-ins Python optimises
5. Documenter le Comportement Recursif
def quicksort(arr):
"""
Tri rapide recursif.
Complexite:
- Temps moyen: O(n log n)
- Temps pire cas: O(n^2)
- Espace: O(log n) pour la pile d'appels
Args:
arr: Liste a trier
Returns:
Nouvelle liste triee
"""
if len(arr) <= 1:
return arr
pivot = arr[len(arr) // 2]
left = [x for x in arr if x < pivot]
middle = [x for x in arr if x == pivot]
right = [x for x in arr if x > pivot]
return quicksort(left) + middle + quicksort(right)
Pieges Courants
1. Oublier le Cas de Base
# ERREUR : pas de cas de base -> RecursionError
def infinite_loop(n):
return infinite_loop(n - 1) # Ne s'arrete jamais !
# CORRECT :
def finite_loop(n):
if n <= 0:
return 0 # Cas de base
return finite_loop(n - 1)
2. Cas de Base Inatteignable
# ERREUR : le cas de base n'est jamais atteint pour n negatif
def bad_factorial(n):
if n == 0:
return 1
return n * bad_factorial(n - 1)
bad_factorial(-5) # RecursionError !
# CORRECT :
def good_factorial(n):
if n < 0:
raise ValueError("n doit etre positif")
if n == 0:
return 1
return n * good_factorial(n - 1)
3. Complexite Exponentielle non Detectee
# ERREUR : complexite O(2^n) - extremement lent
def naive_fibonacci(n):
if n <= 1:
return n
return naive_fibonacci(n - 1) + naive_fibonacci(n - 2)
# naive_fibonacci(40) prend plusieurs secondes !
# CORRECT : utiliser la memoization
from functools import lru_cache
@lru_cache(maxsize=None)
def fast_fibonacci(n):
if n <= 1:
return n
return fast_fibonacci(n - 1) + fast_fibonacci(n - 2)
4. Modification de Variables Mutables Partagees
# ERREUR : resultat partage entre tous les appels
def bad_collect(node, result=[]): # Liste par defaut mutable !
result.append(node['value'])
for child in node.get('children', []):
bad_collect(child, result)
return result
# CORRECT :
def good_collect(node, result=None):
if result is None:
result = [] # Nouvelle liste a chaque appel initial
result.append(node['value'])
for child in node.get('children', []):
good_collect(child, result)
return result
Conclusion
La recursion est un outil puissant qui, bien maitrise, permet d’ecrire du code elegant et expressif pour resoudre des problemes complexes. Les points cles a retenir :
- Comprenez les environnements d’execution : chaque appel cree un nouveau frame sur la pile
- Definissez toujours un cas de base clair et assurez-vous qu’il est atteignable
- Optimisez avec
lru_cachepour les fonctions avec des appels redondants - Convertissez en iteration pour les grandes donnees ou la performance critique
- Surveillez la limite de recursion avec
sys.getrecursionlimit()
Pour Aller Plus Loin
- Langages fonctionnels : Explorez Haskell ou Scheme ou la recursion est naturelle et optimisee
- Trampolining : Technique pour simuler la tail call optimization en Python
- Generateurs recursifs : Combinez
yieldet recursion pour traiter de grandes structures sans surcharger la memoire - Divide and Conquer : Approfondissez les algorithmes recursifs classiques (mergesort, quicksort, etc.)
En maitrisant ces concepts, vous serez capable d’utiliser la recursion de maniere efficace et securisee dans vos projets Python.
In-Article Ad
Dev Mode
Tags
Mahmoud DEVO
Senior Full-Stack Developer
I'm a passionate full-stack developer with 10+ years of experience building scalable web applications. I write about Vue.js, Node.js, PostgreSQL, and modern DevOps practices.
Enjoyed this article?
Subscribe to get more tech content delivered to your inbox.
Related Articles
Recherche de valeurs dans les listes, tuples et dictionnaires Python
Apprenez a rechercher des elements dans les sequences Python : methode index(), mot-cle in, recherche dans les dictionnaires et algorithme bisect pour listes triees.
Expressions Regulieres en Python : Guide Complet pour Maitriser le Module re
Apprenez a maitriser les expressions regulieres en Python avec le module re. Decouvrez comment extraire des donnees, valider des formats, manipuler des chaines et eviter les pieges courants avec des exemples pratiques.
Gestion des packages Python : creer et utiliser requirements.txt efficacement
Guide complet pour gerer vos dependances Python avec pip. Apprenez a creer un fichier requirements.txt, utiliser les environnements virtuels et maitriser la gestion des packages pour des projets Python professionnels et reproductibles.