La Recursion en Python : Comprendre les Environnements et Optimiser vos Fonctions

Maitrisez la recursion en Python : comprenez les environnements d'execution, les variables locales, et decouvrez les techniques d'optimisation comme lru_cache et la tail recursion.

Mahmoud DEVO
Mahmoud DEVO
December 27, 2025 7 min read
La Recursion en Python : Comprendre les Environnements et Optimiser vos Fonctions

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_cache pour 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 yield et 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.

Advertisement

In-Article Ad

Dev Mode

Share this article

Mahmoud DEVO

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