Skip to main content

Code that compiles on the first try! CodeWithMpia wishes you very happy holidays.✨

Aller au contenu principal

La Récursivité en Python : Quand une fonction s'appelle elle-même

Guide pratique qui explique pas à pas la récursivité en Python (cas de base, pile d’exécution, exemples concrets et exercices) pour apprendre à raisonner récursivement et écrire des fonctions claires, efficaces et robustes.

M
Mpia
12/14/2025 30 min
74 vues
0 comme.
#Python#Code#Recursion
La Récursivité en Python : Quand une fonction s'appelle elle-même

Vous êtes-vous déjà demandé comment fonctionne réellement la récursivité ? Ce concept qui peut sembler abstrait au premier abord est en réalité l'un des piliers de la programmation. Dans cet article, nous allons décortiquer la récursivité pas à pas, comprendre ce qui se passe vraiment en mémoire, et voir comment l'utiliser efficacement en Python.

Comprendre la récursivité : au-delà de la définition

Qu'est-ce que la récursivité, vraiment ?

La récursivité, c'est une fonction qui s'appelle elle-même pour résoudre un problème. Mais ce n'est pas tout. Le vrai pouvoir de la récursivité vient de sa capacité à décomposer un problème complexe en sous-problèmes identiques mais plus simples.

Prenons une analogie du quotidien. Imaginez que vous devez ranger une pile de 100 livres sur une étagère. Vous pourriez :

  • Approche itérative : prendre chaque livre un par un, 100 fois

  • Approche récursive : prendre le premier livre, puis se dire "maintenant je dois ranger 99 livres", puis "maintenant je dois ranger 98 livres", jusqu'à n'avoir plus aucun livre

La différence peut sembler subtile, mais elle change fondamentalement la façon dont on pense le problème.

L'anatomie d'une fonction récursive

Toute fonction récursive repose sur deux piliers absolument essentiels :

1. Le cas de base : c'est la condition d'arrêt, le moment où on arrête de s'appeler soi-même. Sans cas de base, votre fonction tourne indéfiniment.

2. Le cas récursif : c'est l'appel de la fonction à elle-même, mais avec des paramètres différents qui nous rapprochent du cas de base.

Voyons cela avec l'exemple le plus simple possible.

Premier exemple : compter jusqu'à zéro

def compte_a_rebours(n):
    # Cas de base : on s'arrête à 0
    if n <= 0:
        print("Décollage ! 🚀")
        return

    # On affiche le nombre actuel
    print(n)

    # Cas récursif : on s'appelle avec n-1
    compte_a_rebours(n - 1)

# Test
compte_a_rebours(5)

Résultat :

5
4
3
2
1
Décollage ! 🚀

Que se passe-t-il vraiment ?

Décomposons l'exécution étape par étape :

  1. compte_a_rebours(5) s'exécute, affiche 5, puis appelle compte_a_rebours(4)

  2. compte_a_rebours(4) s'exécute, affiche 4, puis appelle compte_a_rebours(3)

  3. compte_a_rebours(3) s'exécute, affiche 3, puis appelle compte_a_rebours(2)

  4. compte_a_rebours(2) s'exécute, affiche 2, puis appelle compte_a_rebours(1)

  5. compte_a_rebours(1) s'exécute, affiche 1, puis appelle compte_a_rebours(0)

  6. compte_a_rebours(0) atteint le cas de base, affiche "Décollage !", et retourne

C'est comme une cascade d'appels qui s'empilent les uns sur les autres.

La pile d'exécution : comprendre ce qui se passe en mémoire

Voici un concept crucial : la pile d'exécution (ou call stack en anglais). C'est la structure de données que Python utilise pour gérer les appels de fonctions.

Visualisons la pile

Prenons un exemple simple avec la factorielle :

def factorielle(n):
    print(f"→ Entrée dans factorielle({n})")

    if n <= 1:
        print(f"← Sortie de factorielle({n}) = 1")
        return 1

    resultat = n * factorielle(n - 1)
    print(f"← Sortie de factorielle({n}) = {resultat}")
    return resultat

# Test
print(f"\nRésultat final : {factorielle(4)}\n")

Sortie :

→ Entrée dans factorielle(4)
→ Entrée dans factorielle(3)
→ Entrée dans factorielle(2)
→ Entrée dans factorielle(1)
← Sortie de factorielle(1) = 1
← Sortie de factorielle(2) = 2
← Sortie de factorielle(3) = 6
← Sortie de factorielle(4) = 24

Résultat final : 24

Ce qui se passe vraiment

Phase descendante (empilement) :

factorielle(4) attend le résultat de factorielle(3)
  factorielle(3) attend le résultat de factorielle(2)
    factorielle(2) attend le résultat de factorielle(1)
      factorielle(1) retourne 1 ✓

Phase ascendante (dépilement) :

      factorielle(1) retourne 1
    factorielle(2) calcule 2 * 1 = 2, retourne 2
  factorielle(3) calcule 3 * 2 = 6, retourne 6
factorielle(4) calcule 4 * 6 = 24, retourne 24

Chaque appel de fonction crée un nouveau contexte en mémoire avec ses propres variables locales. C'est comme si chaque appel était une copie indépendante de la fonction.

Application pratique : manipuler des listes de films

Passons maintenant à un exemple concret qui montre toute la puissance de la récursivité. Nous allons travailler avec des données de films.

Problème 1 : Associer des informations

Vous avez deux listes séparées :

  • Une liste de films avec leurs réalisateurs

  • Une liste de budgets

Vous voulez les combiner. Voici comment faire ça de manière récursive :

def associer_listes(liste_films, liste_budgets):
    """
    Fusionne deux listes en ajoutant chaque budget au film correspondant.

    Exemple :
        films = [["Inception", "Nolan"], ["Pulp Fiction", "Tarantino"]]
        budgets = [160000000, 8000000]

        Résultat : [["Inception", "Nolan", 160000000], 
                    ["Pulp Fiction", "Tarantino", 8000000]]
    """
    # Cas de base : si une des listes est vide, on arrête
    if not liste_films or not liste_budgets:
        return []

    # On prend le premier film et on lui ajoute le premier budget
    premier_element = liste_films[0] + [liste_budgets[0]]

    # On traite récursivement le reste des listes
    reste = associer_listes(liste_films[1:], liste_budgets[1:])

    # On combine le premier élément avec le reste traité
    return [premier_element] + reste

Traçons l'exécution

Avec films = [["A"], ["B"], ["C"]] et budgets = [100, 200, 300] :

associer_listes([["A"], ["B"], ["C"]], [100, 200, 300])
│
├─ premier_element = ["A", 100]
├─ appelle associer_listes([["B"], ["C"]], [200, 300])
│  │
│  ├─ premier_element = ["B", 200]
│  ├─ appelle associer_listes([["C"]], [300])
│  │  │
│  │  ├─ premier_element = ["C", 300]
│  │  ├─ appelle associer_listes([], [])
│  │  │  └─ retourne []  (cas de base)
│  │  │
│  │  └─ retourne [["C", 300]]
│  │
│  └─ retourne [["B", 200], ["C", 300]]
│
└─ retourne [["A", 100], ["B", 200], ["C", 300]]

Testez-le !

films = [
    ["Inception", "Christopher Nolan"],
    ["Pulp Fiction", "Quentin Tarantino"],
    ["Le Parrain", "Francis Ford Coppola"],
    ["Parasite", "Bong Joon-ho"]
]

budgets = [160000000, 8000000, 6000000, 11400000]

resultat = associer_listes(films, budgets)

print("Films avec leurs budgets :")
for film in resultat:
    print(f"  • {film[0]} de {film[1]} : {film[2]:,}€")

Calculer avec la récursivité : sommes et moyennes

La somme récursive

Comment additionner tous les nombres d'une liste sans utiliser de boucle ?

def somme_recursive(liste):
    """
    Calcule la somme des éléments d'une liste.

    Logique :

        - Liste vide ? Somme = 0

        - Sinon ? Somme = premier élément + somme du reste
    """
    # Cas de base
    if not liste:
        return 0

    # Cas récursif
    premier = liste[0]
    reste = liste[1:]

    print(f"Calculer : {premier} + somme({reste})")

    return premier + somme_recursive(reste)

# Test avec trace
print("\nCalcul détaillé :")
resultat = somme_recursive([10, 20, 30, 40])
print(f"\nRésultat : {resultat}")

Sortie :

Calcul détaillé :
Calculer : 10 + somme([20, 30, 40])
Calculer : 20 + somme([30, 40])
Calculer : 30 + somme([40])
Calculer : 40 + somme([])

Résultat : 100

Compter les éléments

De la même manière, on peut compter les éléments :

def compter_elements(liste):
    """
    Compte le nombre d'éléments dans une liste.

    Logique :

        - Liste vide ? 0 éléments

        - Sinon ? 1 (pour l'élément actuel) + nombre d'éléments dans le reste
    """
    if not liste:
        return 0

    return 1 + compter_elements(liste[1:])

# Test
print(compter_elements([10, 20, 30, 40]))  # 4

Le budget moyen

Maintenant, combinons nos deux fonctions :

def budget_moyen(liste_budgets):
    """Calcule la moyenne des budgets."""
    if not liste_budgets:
        return 0

    total = somme_recursive(liste_budgets)
    nombre = compter_elements(liste_budgets)

    return total / nombre

# Test
budgets = [160000000, 8000000, 6000000, 11400000]
moyenne = budget_moyen(budgets)
print(f"Budget moyen : {moyenne:,.0f}€")  # 46,350,000€

Filtrer avec la récursivité

Maintenant, utilisons tout ce qu'on a appris pour filtrer les films selon leur budget :

def filtrer_petits_budgets(liste_films, budget_limite):
    """
    Garde uniquement les films dont le budget est inférieur à la limite.

    Args:
        liste_films: Liste de [nom, réalisateur, budget]
        budget_limite: Seuil maximum

    Returns:
        Nouvelle liste filtrée
    """
    # Cas de base : liste vide
    if not liste_films:
        return []

    film_actuel = liste_films[0]
    budget_actuel = film_actuel[2]

    # On filtre récursivement le reste
    reste_filtré = filtrer_petits_budgets(liste_films[1:], budget_limite)

    # Si le film actuel a un petit budget, on le garde
    if budget_actuel < budget_limite:
        return [film_actuel] + reste_filtré
    else:
        return reste_filtré

# Application complète
films = [
    ["Inception", "Christopher Nolan", 160000000],
    ["Pulp Fiction", "Quentin Tarantino", 8000000],
    ["Le Parrain", "Francis Ford Coppola", 6000000],
    ["Parasite", "Bong Joon-ho", 11400000]
]

moyenne = budget_moyen([f[2] for f in films])
print(f"Budget moyen : {moyenne:,.0f}\n")

petits_budgets = filtrer_petits_budgets(films, moyenne)

print("Films à petit budget :")
for film in petits_budgets:
    print(f"  • {film[0]} de {film[1]} : {film[2]:,}€")

Résultat :

Budget moyen : 46,350,000€

Films à petit budget :
  • Pulp Fiction de Quentin Tarantino : 8,000,000€
  • Le Parrain de Francis Ford Coppola : 6,000,000€
  • Parasite de Bong Joon-ho : 11,400,000€

Récursivité vs Itération : le grand débat

Version itérative vs récursive

Comparons les deux approches pour la somme :

# Version itérative (avec boucle)
def somme_iterative(liste):
    total = 0
    for nombre in liste:
        total += nombre
    return total

# Version récursive
def somme_recursive(liste):
    if not liste:
        return 0
    return liste[0] + somme_recursive(liste[1:])

Avantages et inconvénients

Récursivité :

  • Code plus élégant et lisible

  • Correspond à la définition mathématique

  • Parfait pour les structures arborescentes

  • Consomme plus de mémoire (pile d'appels)

  • Limite de récursion en Python (~1000 appels)

  • Parfois plus lent

Itération :

  • Plus efficace en mémoire

  • Pas de limite d'itérations

  • Souvent plus rapide

  • Code parfois moins intuitif

  • Difficile pour structures complexes

Quand utiliser la récursivité ?

Utilisez la récursivité pour :

  • Parcourir des arbres (fichiers, DOM, JSON)

  • Algorithmes naturellement récursifs (quicksort, merge sort)

  • Problèmes diviser-pour-régner

  • Backtracking (sudoku, échecs)

Privilégiez l'itération pour :

  • Listes très longues

  • Calculs simples (sommes, moyennes)

  • Performance critique

  • Traitements linéaires

Exemple avancé : aplatir une liste imbriquée

Voici un cas où la récursivité excelle vraiment :

def aplatir(liste):
    """
    Aplatit une liste de n'importe quelle profondeur.

    Exemple : [[1, [2, 3]], [[4], 5]] → [1, 2, 3, 4, 5]
    """
    # Liste vide
    if not liste:
        return []

    premier = liste[0]
    reste = liste[1:]

    # Si le premier élément est lui-même une liste, on l'aplatit
    if isinstance(premier, list):
        return aplatir(premier) + aplatir(reste)
    else:
        # Sinon, on le garde tel quel
        return [premier] + aplatir(reste)

# Tests
print(aplatir([1, 2, 3]))  # [1, 2, 3]
print(aplatir([[1, 2], [3, 4]]))  # [1, 2, 3, 4]
print(aplatir([[1, [2, 3]], [[4], 5]]))  # [1, 2, 3, 4, 5]
print(aplatir([[[[[1]]]], 2]))  # [1, 2]

Traçons l'exécution

Pour [[1, 2], 3] :

aplatir([[1, 2], 3])
├─ premier = [1, 2] (c'est une liste)
├─ on appelle aplatir([1, 2])
│  ├─ premier = 1 (pas une liste)
│  └─ retourne [1] + aplatir([2])
│     └─ retourne [1, 2]
│
├─ on appelle aplatir([3])
│  └─ retourne [3]
│
└─ retourne [1, 2] + [3] = [1, 2, 3]

Optimisation : la récursivité terminale

Un problème avec la récursivité classique est qu'elle conserve tous les contextes en mémoire. La récursivité terminale résout ce problème en utilisant un accumulateur :

# Version classique (non optimisée)
def somme_classique(liste):
    if not liste:
        return 0
    return liste[0] + somme_classique(liste[1:])

# Version avec récursivité terminale
def somme_terminale(liste, accumulateur=0):
    """
    Le dernier appel est directement la fonction récursive,
    sans calcul supplémentaire après.
    """
    if not liste:
        return accumulateur

    # On accumule au fur et à mesure
    return somme_terminale(liste[1:], accumulateur + liste[0])

# Test
print(somme_terminale([1, 2, 3, 4, 5]))  # 15

La différence cruciale : dans la version terminale, il n'y a aucun calcul après l'appel récursif. Le résultat de l'appel récursif EST le résultat de la fonction.

Erreurs courantes à éviter

1. Oublier le cas de base

# ❌ ERREUR : RecursionError
def mauvaise_fonction(n):
    return n + mauvaise_fonction(n - 1)  # Pas de cas de base !

2. Cas de base jamais atteint

# ❌ ERREUR : Le cas de base n'est jamais atteint
def mauvaise_factorielle(n):
    if n == 1:  # Mais que se passe-t-il si n = 0 ?
        return 1
    return n * mauvaise_factorielle(n - 1)

3. Ne pas se rapprocher du cas de base

# ❌ ERREUR : On ne se rapproche pas du cas de base
def mauvais_compte(n):
    if n == 0:
        return
    print(n)
    mauvais_compte(n)  # On devrait faire n-1 !

Exercices pratiques pour progresser

Essayez d'implémenter ces fonctions de manière récursive :

Exercice 1 : Inverser une liste

def inverser(liste):
    """[1, 2, 3] → [3, 2, 1]"""
    # À vous de coder !
    pass

Exercice 2 : Trouver le maximum

def maximum(liste):
    """Trouve le plus grand élément"""
    # À vous de coder !
    pass

Exercice 3 : Palindrome

def est_palindrome(texte):
    """Vérifie si un texte est un palindrome"""
    # À vous de coder !
    pass

Exercice 4 : Suite de Fibonacci

def fibonacci(n):
    """Calcule le n-ième terme de Fibonacci"""
    # À vous de coder !
    pass

Solutions

Cliquez pour voir les solutions
# Solution 1 : Inverser
def inverser(liste):
    if not liste:
        return []
    return inverser(liste[1:]) + [liste[0]]

# Solution 2 : Maximum
def maximum(liste):
    if len(liste) == 1:
        return liste[0]
    max_reste = maximum(liste[1:])
    return liste[0] if liste[0] > max_reste else max_reste

# Solution 3 : Palindrome
def est_palindrome(texte):
    texte = texte.lower().replace(" ", "")
    if len(texte) <= 1:
        return True
    if texte[0] != texte[-1]:
        return False
    return est_palindrome(texte[1:-1])

# Solution 4 : Fibonacci
def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

Conclusion

La récursivité est bien plus qu'une technique de programmation : c'est une façon de penser les problèmes. En décomposant un problème complexe en versions plus simples de lui-même, on arrive souvent à des solutions élégantes et puissantes.

Les points clés à retenir :

  1. Toujours définir un cas de base clair et atteignable

  2. Chaque appel récursif doit se rapprocher du cas de base

  3. Comprendre la pile d'exécution aide à déboguer

  4. Choisir entre récursivité et itération selon le contexte

  5. Utiliser la récursivité terminale pour optimiser

La maîtrise de la récursivité demande de la pratique. Commencez par des exemples simples, tracez l'exécution sur papier, et progressivement vous développerez cette intuition récursive qui transforme des problèmes complexes en solutions élégantes.

Bon code ! 🚀

Commentaires (0)

Laisser un commentaire

Aucun commentaire pour le moment. Soyez le premier à commenter !