NSI · Terminale

Langages et programmation : récursivité, modules et mise au point

Un programme peut lire un autre programme comme une donnée. Pour raisonner sur son exécution, on précise le langage, l’état et le résultat attendu. Ce cours relie les limites de la décidabilité, les appels récursifs, les modules, les paradigmes et les tests, avec des traces que tu peux prévoir puis vérifier.

Explications et exemples en accès libre. Ateliers, quiz et cartes avec l’abonnement.

Étude en accès libre
Environ 50 min à 1 h 35
Progression
6 étapes guidées
Vérification
12 questions
Rappel actif
12 cartes
Prévoir mon tempsExplications et exemples en accès libre

Une première étude des explications, schémas, exemples résolus et erreurs expliquées. Les activités, productions, quiz et cartes réservés à l’abonnement ne sont pas comptés ici.

Comprendre2485 mots d’explication et 6 schémas
17 à 28 min
Étudier les exemples et les erreurs18 cas, exemples et activités guidés
36 à 66 min

Étude du cours en accès libre, environ50 min à 1 h 35

Voir le calcul et le temps par étape

Le calcul suit automatiquement les explications et les tâches présentes dans le cours. Ses coefficients sont des repères de planification, pas des temps mesurés auprès d’élèves.

  • Lecture active : 2485 mots, à raison de 160 à 220 mots par minute.
  • Schémas : 6, avec 1 à 2 min pour lire chacun.
  • Exemple guidé : 6 × 2 à 4 min.
  • Suivre le code résolu et ses tests : 6 × 3 à 5 min.
  • Comprendre une erreur expliquée : 6 × 1 à 2 min.
  1. Programme comme donnée : donner un sens aux instructions8 à 16 min
  2. Décidabilité : ce qu’un test d’arrêt peut vraiment conclure8 à 16 min
  3. Récursivité : suivre les appels, puis les retours8 à 16 min
  4. Modules et API : changer l’intérieur sans casser le client8 à 16 min
  5. Paradigmes : trois organisations, un même contrat8 à 16 min
  6. Mise au point : du contre-exemple au test de non-régression9 à 16 min

Les sous-totaux sont arrondis à la minute, puis additionnés. La fourchette totale est élargie aux cinq minutes voisines. Les étapes ci-dessus comprennent leurs explications et exemples ; elles ne s’ajoutent pas une seconde fois au total.

Adapte ce repère à tes acquis et au soin apporté aux exercices. Les pauses, les reprises, la consultation des sources externes et les révisions suivantes s’ajoutent selon tes besoins.

Avec les ateliers, le projet, le quiz et les cartes : environ 2 h 50 à 4 h 55, à répartir sur plusieurs séances.

Objectifs du cours

Ce que tu vas savoir faire

  • Lire un petit programme comme une donnée et suivre ses transformations d’état.
  • Expliquer la portée d’un test d’arrêt et reconstruire la preuve par contradiction.
  • Écrire une fonction récursive et suivre ses cadres jusqu’au retour du résultat.
  • Utiliser, documenter et importer un module derrière une interface stable.
  • Comparer trois styles de programmation par leurs résultats et leurs effets.
  • Construire des tests de frontières, expliquer une erreur et conserver sa correction.
01

Étape du cours · 8 à 16 min

Programme comme donnée : donner un sens aux instructions

Quand tu ouvres un fichier Python dans un éditeur, celui-ci traite des caractères. Quand un interprète exécute ce fichier, il donne un sens à ses constructions. Le même programme est donc une donnée pour un autre programme. Un compilateur traduit une représentation vers une autre ; un interprète applique les règles du langage pendant l’exécution. Cette distinction n’impose pas une frontière absolue : une implantation peut combiner traduction intermédiaire et interprétation.

Pour voir ce mécanisme, utilisons des tuples au lieu d’analyser du texte. L’instruction ('SET', 'x', 3) associe 3 au nom x. ('ADD', 'x', 2) remplace sa valeur par la valeur précédente plus 2. ('PRINT', 'x') ajoute cette valeur à une liste de sorties simulées. SET peut réaffecter un nom ; ADD et PRINT exigent qu’il ait déjà été défini. La liste de tuples décrit le programme, elle n’exécute rien à elle seule.

La syntaxe fixe les formes autorisées ; la sémantique fixe leur effet. Ici, seuls les noms x, y et z, les trois opérations annoncées et des constantes entières de −100 à 100 sont admis. Un booléen n’est pas une constante numérique de ce langage. Le programme contient au plus 100 instructions. Ce choix pédagogique rend les traces courtes et le domaine précis ; il ne décrit pas la grammaire de Python.

Le validateur parcourt tout le programme avant l’interprétation. Il contrôle les formes et les noms définis au fil des instructions. Cette vérification préalable est possible ici parce que l’exécution est linéaire, sans branche ni boucle. Le moteur part ensuite d’un dictionnaire vide. Pour chaque instruction, il conserve un instantané avant et après, avec une copie du dictionnaire et une sortie sous forme de tuple.

Une copie superficielle suffit dans ce modèle car les valeurs sont des entiers immuables. Accepter une liste comme constante permettrait au programme appelant de changer après coup une ancienne trace : c’est pourquoi elle est refusée. Aucun texte n’est évalué comme du code, aucun fichier ni réseau n’est utilisé. Lis d’abord les trois branches du moteur, puis le validateur : les contrôles protègent le contrat sans remplacer l’explication du calcul.

Voir pour comprendre

Le programme, l’état et la sortie ne se confondent pas

Une ligne par étape. Les instantanés avant et après sont distincts.

Le programme, l’état et la sortie ne se confondent pas
InstructionAvantAprèsSortie
Départ{}{}[]
SET x 3{}{'x': 3}[]
ADD x 2{'x': 3}{'x': 5}[]
PRINT x{'x': 5}{'x': 5}[5]

Lis le schéma. Lis de gauche à droite : seule l’instruction du programme donne le changement. PRINT ne change pas la variable.

Une trace explique la transformation ; ce n’est ni le programme lui-même, ni seulement son dernier résultat.

Schéma original Maxdecours ; données et exemples du cours. · Source du repère · 06/09/2026

Exemples résolus et erreurs expliquées

De trois tuples à la sortie 5

  1. État initial : aucun nom défini et aucune sortie. Le validateur accepte les trois formes et l’ordre des définitions.
  2. SET x 3 produit {'x': 3}. Son instantané avant est {}, son instantané après contient 3.
  3. ADD x 2 lit 3 et produit {'x': 5}. La trace précédente reste à 3.
  4. PRINT x conserve {'x': 5} et ajoute 5 aux sorties. Le moteur rend état, sorties et trois étapes.

Conclusion. La sortie [5] et l’état {'x': 5} jouent deux rôles différents ; le programme d’entrée reste inchangé.

Laboratoire de code

Languepython

ButPrévoir les instantanés, puis exécuter un programme de tuples validé avant son interprétation.

Code solution
def verifier_programme(programme):
    """Liste de 0 à 100 tuples SET/ADD/PRINT ; noms x, y, z ; constantes -100 à 100."""
    if type(programme) is not list or len(programme) > 100:
        raise ValueError('liste de 100 instructions au plus attendue')
    connus, instructions = set(), []
    for i, instruction in enumerate(programme):
        if type(instruction) is not tuple or len(instruction) not in (2, 3):
            raise ValueError(f'instruction {i} : forme invalide')
        op, nom = instruction[:2]
        if type(op) is not str or op not in ('SET', 'ADD', 'PRINT'):
            raise ValueError(f'instruction {i} : opération inconnue')
        if type(nom) is not str or nom not in ('x', 'y', 'z'):
            raise ValueError(f'instruction {i} : nom invalide')
        if op == 'PRINT':
            if len(instruction) != 2:
                raise ValueError(f'instruction {i} : PRINT attend un nom')
        else:
            if len(instruction) != 3 or type(instruction[2]) is not int or not -100 <= instruction[2] <= 100:
                raise ValueError(f'instruction {i} : constante entière de -100 à 100 attendue')
        if op != 'SET' and nom not in connus:
            raise ValueError(f'instruction {i} : variable non définie')
        if op == 'SET':
            connus.add(nom)
        instructions.append(instruction)
    return tuple(instructions)

def interpreter(programme):
    instructions = verifier_programme(programme)
    etat, sorties, trace = {}, [], []
    for i, instruction in enumerate(instructions):
        avant = dict(etat)
        op, nom = instruction[:2]
        if op == 'SET':
            etat[nom] = instruction[2]
        elif op == 'ADD':
            etat[nom] += instruction[2]
        else:  # PRINT, déjà vérifié.
            sorties.append(etat[nom])
        trace.append((i, instruction, avant, dict(etat), tuple(sorties)))
    return etat, sorties, trace

Tests

P = [('SET', 'x', 3), ('ADD', 'x', 2), ('PRINT', 'x')]
etat, sorties, trace = interpreter(P)
assert etat == {'x': 5} and sorties == [5]
assert trace[0] == (0, ('SET', 'x', 3), {}, {'x': 3}, ())
assert trace[2] == (2, ('PRINT', 'x'), {'x': 5}, {'x': 5}, (5,))
assert interpreter([]) == ({}, [], [])
assert P == [('SET', 'x', 3), ('ADD', 'x', 2), ('PRINT', 'x')]
for mauvais in [[()], [('SET', 'x', True)], [('ADD', 'x', 1)], [('SET', 'x', [])]]:
    try:
        interpreter(mauvais)
    except ValueError:
        pass
    else:
        raise AssertionError('programme invalide accepté')

Trace

  • L’indice 0 correspond à SET : l’état vide devient x = 3.
  • À l’indice 1, ADD lit 3 puis remplace cette valeur par 5.
  • À l’indice 2, PRINT enrichit la sortie sans changer x ; les copies préservent les états antérieurs.

Clinique de bogue

Indice observéL’opération inconnue MUL est traitée comme une addition : appliquer('MUL', 3, 2) rend 5.

CauseLa branche else attribue un sens à tout ce qui n’est pas SET. Une grammaire fermée exige de nommer chaque opération admise et de refuser le reste avant le calcul ; sinon une faute de frappe devient silencieusement une autre instruction.

02

Étape du cours · 8 à 16 min

Décidabilité : ce qu’un test d’arrêt peut vraiment conclure

Un problème de décision demande une réponse oui ou non pour chaque instance : « cet entier est-il premier ? » ou « ce programme termine-t-il sur cette entrée ? ». Il est décidable s’il existe un algorithme qui répond correctement et termine sur toute instance du domaine. Être décidable ne signifie pas être rapide. La complexité étudie les ressources nécessaires ; la décidabilité demande d’abord si un tel algorithme existe.

Dans le modèle idéal du calcul, sans limite fixée de mémoire ou de temps, les langages généralistes usuels peuvent exprimer les mêmes fonctions calculables. Changer de langage ne résout donc pas le problème général de l’arrêt. Cette équivalence ne concerne pas tous les langages restreints : notre langage SET/ADD/PRINT est sans boucle, et tous ses programmes valides terminent. Les bornes d’un ordinateur réel ne sont pas la définition mathématique de la calculabilité.

Pour comprendre l’impossibilité générale, supposons un programme H toujours correct, qui termine et répond si le programme de texte p s’arrête sur l’entrée x. Construisons alors conceptuellement D(p) : si H(p, p) prédit l’arrêt, D boucle sans fin ; sinon D s’arrête. Le texte de D est une donnée, notée d. Examinons D(d). Si H(d, d) répond oui, D(d) boucle : réponse fausse. S’il répond non, D(d) s’arrête : réponse fausse encore. Les deux cas contredisent l’hypothèse de H.

Cette preuve n’affirme pas que toute question particulière d’arrêt est insoluble. Un variant peut prouver la terminaison d’une fonction précise ; une répétition du même état complet dans un système déterministe prouve un cycle. Répéter seulement un compteur partiel ou une ligne du programme ne suffit pas : le reste de l’état peut avoir changé. Le laboratoire décrit une machine où le nom de l’état est volontairement toute l’information déterminant la transition suivante.

Chaque état non terminal de cette machine finie possède exactement un successeur connu ; HALT est terminal. L’observateur avance dans la limite d’un budget de transitions. Il distingue arrêt atteint, cycle prouvé et inconnu à la limite. Les états obtenus exactement au dernier pas sont examinés avant la conclusion de budget. Avec N états non terminaux, N transitions suffisent ici pour atteindre HALT ou répéter un état ; cette propriété de ce modèle fermé ne fournit aucun décideur pour tous les programmes.

Voir pour comprendre

Le décideur supposé se contredit dans les deux cas

d est le texte du programme D. Par définition, D fait l’opposé de la prédiction H(d, d).

Le décideur supposé se contredit dans les deux cas
Réponse supposée de H(d, d)D(d) par constructionContradiction
« S’arrête »Boucle sans finH annonçait un arrêt.
« Ne s’arrête pas »S’arrêteH annonçait le non-arrêt.

Lis le schéma. Examine les deux réponses possibles, sans essayer d’implanter H. Chacune devient fausse pour le programme diagonal D.

Il n’existe pas de H qui termine et réponde correctement pour tout programme et toute entrée.

Schéma original Maxdecours ; données et exemples du cours. · Source du repère · 06/09/2026

Exemples résolus et erreurs expliquées

La même chaîne, deux budgets

  1. Machine : A → B → C → HALT. Avec un budget de 1, on effectue A → B ; B n’est ni terminal ni déjà rencontré.
  2. Le budget est épuisé : la bonne réponse est inconnu_limite, pas « boucle infinie ».
  3. Avec un budget de 3, la troisième transition atteint HALT. L’arrêt est reconnu même si tout le budget a été utilisé.
  4. Pour A → B → A, deux transitions ramènent à l’état complet A. La trace et le cycle A, B, A constituent une preuve dans ce modèle déterministe.

Conclusion. Une inconnue dépend du budget d’observation ; elle ne décrit pas le comportement futur du programme général.

Laboratoire de code

Languepython

ButComparer un arrêt, un cycle et un budget insuffisant sur une machine finie entièrement décrite.

Code solution
def verifier_machine(machine, initial, limite):
    if type(machine) is not dict or len(machine) > 100:
        raise ValueError('au plus 100 états attendus')
    def nom_valide(nom):
        return type(nom) is str and 1 <= len(nom) <= 12
    if any(not nom_valide(nom) or nom == 'HALT' for nom in machine):
        raise ValueError('noms non vides, HALT réservé')
    for destination in machine.values():
        if not nom_valide(destination) or (destination != 'HALT' and destination not in machine):
            raise ValueError('transition vers un état inconnu')
    if not nom_valide(initial) or (initial != 'HALT' and initial not in machine):
        raise ValueError('état initial inconnu')
    if type(limite) is not int or not 0 <= limite <= 1000:
        raise ValueError('budget entier de 0 à 1000 attendu')

def observer(machine, initial, limite):
    verifier_machine(machine, initial, limite)
    etat, pas = initial, 0
    vus, chemin, trace = {}, [], []
    while True:
        if etat == 'HALT':
            return 'termine', trace, []
        if etat in vus:
            return 'cycle_prouve', trace, chemin[vus[etat]:] + [etat]
        if pas == limite:
            return 'inconnu_limite', trace, []
        vus[etat] = len(chemin)
        chemin.append(etat)
        suivant = machine[etat]
        trace.append((pas, etat, suivant))
        etat = suivant
        pas += 1

Tests

M = {'A': 'B', 'B': 'C', 'C': 'HALT'}
assert observer(M, 'A', 1) == ('inconnu_limite', [(0, 'A', 'B')], [])
assert observer(M, 'A', 3)[0] == 'termine'
assert observer({'A': 'B', 'B': 'A'}, 'A', 2)[2] == ['A', 'B', 'A']
assert observer({'A': 'A'}, 'A', 1)[0] == 'cycle_prouve'
assert observer({}, 'HALT', 0) == ('termine', [], [])
assert observer(M, 'A', 0) == ('inconnu_limite', [], [])
assert M == {'A': 'B', 'B': 'C', 'C': 'HALT'}
for mauvais in [{'A': 'B'}, {'A': 'HALT', 'X': 'absent'}]:
    try:
        observer(mauvais, 'A', 5)
    except ValueError:
        pass
    else:
        raise AssertionError('transition incomplète acceptée')

Trace

  • Au pas 0, A quitte l’ensemble des états encore inconnus ; la transition A → B est enregistrée.
  • Retrouver un état déjà vu fournit le suffixe du chemin qui se répète, fermé par ce même état.
  • Le budget compte les transitions effectuées, pas les contrôles d’arrêt et de cycle.

Clinique de bogue

Indice observéLa chaîne A → B → C → HALT est déclarée non terminante lorsqu’on cesse de l’observer après un seul pas.

CauseLa limite porte sur l’observation, pas sur le programme observé. Remplacer une absence de preuve par une réponse négative transforme un outil borné en faux oracle. Des indicateurs incompatibles ou non booléens doivent aussi être refusés.

03

Étape du cours · 8 à 16 min

Récursivité : suivre les appels, puis les retours

Une fonction récursive s’appelle elle-même, directement ou par l’intermédiaire d’autres fonctions. Pour en écrire une, cherche un cas que tu sais résoudre immédiatement, puis une réduction vers ce cas. La factorielle fournit un exemple simple : 0! vaut 1 ; pour n positif, n! vaut n × (n − 1)!. Le cas de base donne une valeur sans nouvel appel ; le cas récursif utilise le résultat d’un problème plus petit.

Pour calculer 3!, le premier cadre possède son n égal à 3 et attend le résultat de factorielle(2). Le suivant garde n égal à 2 et attend factorielle(1). L’appel avec 0 rend 1. Les cadres reprennent alors dans l’ordre inverse : 1 × 1, puis 2 × 1, puis 3 × 2. Le paramètre n d’un cadre n’est pas remplacé par celui du suivant. Les variables locales et le point de reprise appartiennent à chaque appel.

Un variant permet de justifier que les appels se rapprochent du cas de base. Ici, n est un entier naturel et diminue de 1 à chaque appel récursif : il ne peut pas diminuer ainsi indéfiniment sans atteindre 0. Cela justifie la terminaison mathématique sur le domaine annoncé, pas à lui seul la valeur du résultat. Il faut encore vérifier la base 0! = 1 et la relation qui recompose les sous-résultats.

Le laboratoire borne n entre 0 et 100 pour conserver une trace manipulable. Ce n’est pas une borne de la définition de la factorielle. Chaque appel ajoute une entrée, puis un retour ; pour n = 3, la fonction récursive calculer a quatre cadres au maximum et huit événements, sans compter l’enveloppe qui prépare la trace. À coût unitaire par opération arithmétique, le calcul comporte n multiplications et n + 1 appels ; les grands entiers rendent le coût réel des multiplications variable.

Une profondeur excessive peut provoquer RecursionError en Python même si la fonction mathématique termine. Inversement, remplacer n − 1 par n + 1 éloigne du cas de base : le calcul récursif abstrait ne le rejoint pas, et Python finit par signaler une erreur de profondeur. Une telle erreur n’est donc pas l’observation d’une exécution infinie. Dans le transfert sur listes imbriquées, on vérifie aussi l’absence de cycle et une profondeur bornée avant de commencer la somme.

Voir pour comprendre

Des appels en attente aux résultats rendus

Factorielle de 3 : f désigne la fonction récursive calculer. Chaque cadre conserve son paramètre ; f(0) = 1.

1. Les appels

  1. f(3) Garde n = 3 ; attend f(2).
  2. f(2) Garde n = 2 ; attend f(1).
  3. f(1) Garde n = 1 ; attend f(0).
  4. f(0) Cas de base : rend 1.

2. Les retours

  1. f(0) = 1 La base fournit 1.
  2. f(1) = 1 1 × 1 = 1 ; rend le résultat.
  3. f(2) = 2 2 × 1 = 2 ; rend le résultat.
  4. f(3) = 6 3 × 2 = 6 ; rend le résultat.

4 cadres récursifs au maximum · 3 multiplications · résultat : 6.

Lis le schéma. Suis les appels du premier au dernier, puis les retours du dernier cadre au premier. Dans la colonne de retour, le paramètre remonte de 0 à 3.

Quatre cadres récursifs au maximum, trois multiplications : f(3) reçoit 2 puis rend 6. L’enveloppe de préparation de la trace n’est pas représentée.

Schéma original Maxdecours, appels et retours calculés depuis n = 3. · Source du repère · 06/09/2026

Exemples résolus et erreurs expliquées

Le cadre de n = 3 attend, il ne disparaît pas

  1. On note f la fonction récursive calculer du laboratoire. À l’entrée de f(3), son cadre retient « multiplier par 3 après le retour de f(2) ».
  2. f(2), f(1), puis f(0) créent chacun un contexte local ; quatre cadres sont présents au point le plus profond.
  3. f(0) rend 1. f(1) reprend avec son n = 1 et rend 1 × 1 = 1 ; f(2) rend 2 × 1 = 2.
  4. Le premier cadre reprend avec son n = 3, multiplie le 2 reçu et rend 6.

Conclusion. La descente construit les attentes ; la remontée calcule les valeurs. Le dernier appel entré est le premier à rendre son résultat.

Laboratoire de code

Languepython

ButPrévoir les huit événements de factorielle_trace(3), puis contrôler les paramètres et valeurs de chaque cadre.

Code solution
def verifier_n(n):
    if type(n) is not int or not 0 <= n <= 100:
        raise ValueError('entier de 0 à 100 attendu')

def factorielle_trace(n):
    """Renvoie (n!, trace) ; trace indépendante à chaque appel."""
    verifier_n(n)
    trace = []
    def calculer(k):
        trace.append(('appel', k, None))
        if k == 0:
            resultat = 1
        else:
            resultat = k * calculer(k - 1)
        trace.append(('retour', k, resultat))
        return resultat
    return calculer(n), trace

def factorielle(n):
    return factorielle_trace(n)[0]

Tests

assert factorielle(0) == 1
assert factorielle(1) == 1
assert factorielle(5) == 120
resultat, trace = factorielle_trace(3)
assert resultat == 6
assert trace[:4] == [('appel', 3, None), ('appel', 2, None), ('appel', 1, None), ('appel', 0, None)]
assert trace[4:] == [('retour', 0, 1), ('retour', 1, 1), ('retour', 2, 2), ('retour', 3, 6)]
assert len(trace) == 8 and factorielle_trace(0)[1] is not trace
for n in (-1, 101, True, 3.0):
    try:
        factorielle(n)
    except ValueError:
        pass
    else:
        raise AssertionError('domaine non respecté')

Trace

  • Les quatre entrées portent successivement les paramètres 3, 2, 1 et 0.
  • Le cas de base retourne 1 ; il ne lance pas d’appel avec −1.
  • Les retours portent les valeurs 1, 1, 2 et 6, dans l’ordre des cadres 0, 1, 2 et 3.

Clinique de bogue

Indice observéPour n = 3, les appels portent sur 4, 5, 6… : Python signale finalement une erreur de profondeur au lieu de rendre 6.

CauseLe paramètre augmente au lieu de diminuer vers le cas de base. Changer seulement la valeur de retour de la base ne corrigerait pas cette progression. Il faut rétablir n − 1 et annoncer le domaine entier naturel retenu par l’exercice.

04

Étape du cours · 8 à 16 min

Modules et API : changer l’intérieur sans casser le client

Un module regroupe une responsabilité derrière une interface. En Python, un fichier mesures.py peut définir des classes et fonctions réutilisables ; un autre fichier les importe au lieu de les recopier. Une API, ou interface de programmation, décrit les opérations offertes au client. Elle peut être locale, comme celle d’une bibliothèque : une API n’est pas nécessairement un service Web et ne demande pas toujours une clé ou un compte.

Lis un contrat dans cet ordre : nom de l’opération, paramètres, valeur renvoyée et unité, erreurs, effets éventuels. Notre client appelle temperature_celsius() sans argument et attend un flottant entre −50 et 60. Les données sont fictives. La version V1 du fournisseur renvoie un entier en dixièmes de degré ; V2 renvoie un dictionnaire en millièmes accompagné d’un statut. Les deux formats ne doivent pas remonter jusqu’au code d’affichage.

L’adaptateur concentre les différences. Il appelle lire() en V1, mesure() en V2, vérifie la réponse puis divise par 10 ou 1 000. Un statut autre que « ok », une clé manquante, un booléen ou une valeur hors domaine produit ValueError. Il ne remplace pas une mesure invalide par zéro. Une exception levée par le fournisseur lui-même n’est pas silencieusement absorbée ; dans cet exercice, les fournisseurs sont des objets locaux aux méthodes déterministes.

Pour 215 dixièmes et 21 500 millièmes, le client obtient le même flottant 21,5. Mais V2 peut aussi porter 21 537 millièmes, une précision que V1 ne représente pas exactement. La compatibilité se vérifie sur les observations communes, pas en prétendant que tous les formats ont la même précision. L’affichage à une décimale arrondit le texte présenté ; il ne doit pas remplacer la valeur utilisée par les calculs. Un flottant reste une représentation approchée.

Copie les définitions du laboratoire, sans les tests, dans mesures.py. Dans le même dossier, crée client.py avec le code de l’exemple résolu, puis lance python3 client.py. Les docstrings sont accessibles par help(mesures.AdaptateurMesure) après import mesures. Importer un module exécute ses instructions de niveau supérieur : garde les démonstrations sous la garde __name__ == '__main__' si tu en ajoutes. Le préfixe _ signale un détail interne par convention, il ne rend pas un attribut inaccessible.

Voir pour comprendre

Une interface stable entre le client et les formats

Même contrat client, deux sources fictives ; aucune requête réseau.

  1. Le client demande des °Ctemperature_celsius() ; il ignore les unités brutes.
    appelle
  2. L’adaptateur choisit la méthodeV1 : lire() ; V2 : mesure(). Il contrôle format, statut et valeur.
    traduit
  3. Deux unités d’entréeV1 : 215 ÷ 10 ; V2 : 21 500 ÷ 1 000.
    renvoie au client
  4. Même valeur : 21,5 °CLe code d’affichage reçoit une valeur Celsius ; son arrondi reste une décision séparée.

Lis le schéma. Suis la demande, le contrôle puis la conversion. La réponse revient au client avec une unité commune.

L’interface isole le changement de format ; elle ne crée pas de précision supplémentaire.

Schéma original Maxdecours ; données et exemples du cours. · Source du repère · 06/09/2026

Exemples résolus et erreurs expliquées

Un vrai client de module, deux fournisseurs

  1. Enregistre les définitions du laboratoire dans mesures.py. Aucun affichage n’est lancé lors de son import.
  2. Dans client.py, importe explicitement les quatre noms dont le client a besoin.
  3. Construis un adaptateur V1 et un V2 : leurs lectures valent 21,5 °C pour les valeurs fictives fournies.
  4. Exécute client.py. Les deux lignes sont identiques, alors que noms de méthodes et unités brutes diffèrent.

client.py, dans le même dossier que mesures.py

from mesures import ApiV1, ApiV2, AdaptateurMesure, afficher_mesure

for fournisseur, version in ((ApiV1(), 1), (ApiV2(), 2)):
    source = AdaptateurMesure(fournisseur, version)
    print(afficher_mesure(source))

Conclusion. Le client peut rester identique lorsque le fournisseur change, tant que l’adaptateur conserve le contrat.

Laboratoire de code

Languepython

ButCréer un module mesures.py documenté, l’importer depuis client.py et comparer les réponses de deux versions fictives.

Code solution
class ApiV1:
    """Source fictive : lire() renvoie des dixièmes de degré."""
    def __init__(self, dixiemes=215):
        self._dixiemes = dixiemes
    def lire(self):
        return self._dixiemes

class ApiV2:
    """Source fictive : mesure() renvoie milli_degres et statut."""
    def __init__(self, milli_degres=21500, statut='ok'):
        self._milli_degres, self._statut = milli_degres, statut
    def mesure(self):
        return {'milli_degres': self._milli_degres, 'statut': self._statut}

class AdaptateurMesure:
    """API client : temperature_celsius() -> float de -50 à 60.
    Réponse/version hors contrat : ValueError ; aucune donnée source modifiée.
    Les objets fournisseurs sont ceux du laboratoire, sans accès réseau.
    """
    def __init__(self, api, version):
        if type(version) is not int or version not in (1, 2):
            raise ValueError('version 1 ou 2 attendue')
        methode = 'lire' if version == 1 else 'mesure'
        if not callable(getattr(api, methode, None)):
            raise ValueError('méthode fournisseur absente')
        self._api, self._version = api, version
    def temperature_celsius(self):
        if self._version == 1:
            valeur, diviseur = self._api.lire(), 10
        else:
            reponse = self._api.mesure()
            if type(reponse) is not dict or set(reponse) != {'milli_degres', 'statut'}:
                raise ValueError('format V2 invalide')
            if type(reponse['statut']) is not str or reponse['statut'] != 'ok':
                raise ValueError('mesure indisponible')
            valeur, diviseur = reponse['milli_degres'], 1000
        if type(valeur) is not int or not -50 * diviseur <= valeur <= 60 * diviseur:
            raise ValueError('mesure entière hors domaine')
        return valeur / diviseur

def afficher_mesure(source):
    """Affichage à une décimale ; ne change pas la précision de la source."""
    return f'{source.temperature_celsius():.1f} °C'

Tests

a1, a2 = AdaptateurMesure(ApiV1(), 1), AdaptateurMesure(ApiV2(), 2)
assert a1.temperature_celsius() == a2.temperature_celsius() == 21.5
assert afficher_mesure(a1) == afficher_mesure(a2) == '21.5 °C'
assert type(a1.temperature_celsius()) is float
assert AdaptateurMesure(ApiV2(-50000), 2).temperature_celsius() == -50.0
assert AdaptateurMesure(ApiV1(600), 1).temperature_celsius() == 60.0
for action in (lambda: AdaptateurMesure(ApiV1(), True),
               lambda: AdaptateurMesure(ApiV2(True), 2).temperature_celsius(),
               lambda: AdaptateurMesure(ApiV2(21500, 'absent'), 2).temperature_celsius()):
    try:
        action()
    except ValueError:
        pass
    else:
        raise AssertionError('contrat non respecté')

Trace

  • V1 : lire() rend 215, le contrôle entier passe puis 215 / 10 donne 21,5.
  • V2 : le format et le statut sont contrôlés avant de diviser 21 500 par 1 000.
  • Le client reçoit l’unité Celsius dans les deux cas ; aucune valeur fictive n’est remplacée par une valeur de secours.

Clinique de bogue

Indice observéLe client échoue dès que le fournisseur change d’attribut interne, même si son interface publique reste disponible.

CauseLe client dépend d’un nom non contractuel et encode directement une unité. Corriger seulement le nom de l’attribut déplace le couplage. Il faut appeler l’interface documentée, vérifier sa réponse et concentrer l’adaptation à un seul endroit.

05

Étape du cours · 8 à 16 min

Paradigmes : trois organisations, un même contrat

Un paradigme est une manière d’organiser un programme. Le style impératif décrit des changements d’état successifs, avec affectations, conditions et boucles. Le style fonctionnel privilégie la composition de fonctions et la construction de nouvelles valeurs. Le style objet réunit un état et des méthodes au sein d’objets. Python permet de combiner ces styles ; leur nom ne garantit ni la correction, ni l’absence d’effets, ni une meilleure performance.

Comparons un même calcul : additionner les carrés des valeurs paires de [1, 2, 3, 4]. Le filtre retient 2 et 4, le carré produit 4 et 16, l’addition donne 20. L’impératif fait évoluer un accumulateur local de 0 à 4 puis à 20. La version fonctionnelle compose filter, map et sum : est_pair et carre sont des fonctions transmises comme données, non des appels écrits avec leurs parenthèses.

Une fonction pure dépend de ses arguments et ne provoque pas d’effet observable extérieur. Modifier un accumulateur local n’est donc pas modifier la liste reçue. À l’inverse, une fonction écrite avec une seule expression peut appeler une fonction qui affiche ou mute un objet : sa forme ne la rend pas pure. Un effet peut être nécessaire, par exemple pour afficher un résultat ; il doit être assumé et séparé du calcul lorsqu’on veut le tester.

La version objet conserve une copie sous forme de tuple d’entiers, puis sa méthode calculer() lit cet état. Modifier la liste d’origine ne modifie donc pas l’objet construit. Cette indépendance dépend ici de l’immuabilité des éléments : transformer en tuple une liste de listes ne suffirait pas à copier les listes internes. Le préfixe _valeurs signale un attribut interne, sans constituer une protection d’accès du langage.

Les trois versions partagent le domaine, le résultat et les tests : liste ou tuple d’au plus 1 000 entiers de −100 à 100, booléens refusés, source inchangée. Elles parcourent les valeurs ; les formes fonctionnelles ne suppriment pas le travail de filtrage ou de calcul. Choisis selon le besoin : boucle claire pour une progression d’état, composition pour une chaîne de transformations, objet pour associer un état stable à plusieurs opérations. Compare aussi les cas vide, négatif, répété et les appels successifs.

Voir pour comprendre

Le même calcul, trois façons d’organiser l’état

Entrée : [1, 2, 3, 4]. Somme des carrés pairs : 20.

Le même calcul, trois façons d’organiser l’état
StyleOrganisationSource reçue
Impératiftotal : 0 → 4 → 20Lue, non modifiée
Fonctionnelfiltrer 2 et 4 → carrés 4 et 16 → somme 20Lue, non modifiée
Objettuple mémorisé → méthode calculer() → 20Copiée à la construction

Lis le schéma. Compare le même filtre et le même résultat. La différence est dans la circulation de l’état, pas dans une garantie attachée au style.

Un accumulateur local peut préserver l’entrée ; un objet n’est indépendant que si sa copie convient aux valeurs stockées.

Schéma original Maxdecours ; données et exemples du cours. · Source du repère · 06/09/2026

Exemples résolus et erreurs expliquées

Pourquoi les trois versions rendent 20

  1. Le contrat élimine 1 et 3 parce qu’ils sont impairs. Les valeurs d’entrée restent [1, 2, 3, 4].
  2. La boucle ajoute successivement 2² puis 4² dans son accumulateur local.
  3. La composition applique est_pair, puis carre, puis sum ; elle obtient les mêmes 4 et 16.
  4. L’objet a mémorisé le tuple (1, 2, 3, 4) ; sa méthode rend 20, même si la liste source change ensuite.

Conclusion. Même résultat ne veut pas dire même organisation ; teste séparément le résultat, l’indépendance et les effets.

Laboratoire de code

Languepython

ButComparer impératif, fonctionnel et objet avec le même corpus, puis modifier la liste d’origine pour vérifier la copie de l’objet.

Code solution
def verifier_valeurs(valeurs):
    if type(valeurs) not in (list, tuple) or len(valeurs) > 1000:
        raise ValueError('liste ou tuple de 1000 valeurs au plus')
    if any(type(v) is not int or not -100 <= v <= 100 for v in valeurs):
        raise ValueError('entiers stricts de -100 à 100 attendus')
    return tuple(valeurs)

def somme_carres_pairs_imperatif(valeurs):
    valeurs = verifier_valeurs(valeurs)
    total = 0
    for valeur in valeurs:
        if valeur % 2 == 0:
            total += valeur * valeur
    return total

def est_pair(x):
    return x % 2 == 0

def carre(x):
    return x * x

def somme_carres_pairs_fonctionnel(valeurs):
    valeurs = verifier_valeurs(valeurs)
    return sum(map(carre, filter(est_pair, valeurs)))

class SommeCarresPairs:
    def __init__(self, valeurs):
        self._valeurs = verifier_valeurs(valeurs)
    def calculer(self):
        return sum(v * v for v in self._valeurs if v % 2 == 0)

Tests

D = [1, 2, 3, 4]
objet = SommeCarresPairs(D)
assert somme_carres_pairs_imperatif(D) == somme_carres_pairs_fonctionnel(D) == objet.calculer() == 20
assert D == [1, 2, 3, 4]
D.append(6)
assert objet.calculer() == 20
assert somme_carres_pairs_imperatif(D) == 56
for valeurs, attendu in [([], 0), ([-2, -4], 20), ([2, 2], 8), ([1, 3], 0)]:
    assert somme_carres_pairs_imperatif(valeurs) == attendu
    assert somme_carres_pairs_fonctionnel(valeurs) == attendu
    assert SommeCarresPairs(valeurs).calculer() == attendu
try:
    SommeCarresPairs([True, 2])
except ValueError:
    pass
else:
    raise AssertionError('booléen admis comme entier')

Trace

  • La boucle modifie seulement total : 0, 4, 20.
  • La composition transmet les fonctions est_pair et carre, et consomme les valeurs retenues.
  • L’objet garde un tuple d’entiers, indépendant des ajouts ultérieurs à la liste d’origine.

Clinique de bogue

Indice observéAprès carres_pairs([2]), carres_pairs([4]) rend [4, 16] au lieu de [16] ; le premier résultat est aussi modifié.

CauseLa valeur par défaut est évaluée lors de la définition de la fonction, pas à chaque appel. Tous les appels sans second argument partagent donc la même liste. Le contrat demande au contraire une liste nouvelle, sans état caché entre les appels.

06

Étape du cours · 9 à 16 min

Mise au point : du contre-exemple au test de non-régression

Commence par le contrat, pas par une modification du code. Un test relie une entrée, une attente et une observation. L’attente, appelée oracle, vient de la spécification ou d’un calcul indépendant. Pour l’intervalle fermé [0 ; 10], les bornes 0 et 10 appartiennent à l’intervalle ; −1 et 11 n’y appartiennent pas. Le test du seul milieu 5 ne distingue pas plusieurs conditions fautives d’une condition correcte.

Choisis des familles d’entrées, puis leurs frontières : valeur en dessous, égale, à l’intérieur, au-dessus, intervalle réduit à un point, bornes inversées et mauvais types. True est un booléen même si isinstance(True, int) vaut vrai en Python ; un contrat d’entiers stricts peut le refuser. Tester toutes les valeurs d’un petit domaine prouve le résultat sur ce domaine seulement. Une série de succès ne démontre pas à elle seule la correction pour toutes les entrées possibles.

Un symptôme indique où chercher, pas encore la cause. Pour x ≥ a ou x ≤ b, prends x = −1, a = 0 et b = 10. La première condition est fausse mais la seconde est vraie : « ou » accepte la valeur extérieure. L’appartenance exige que les deux conditions soient vraies. La bonne correction est « et » ou l’inégalité chaînée a ≤ x ≤ b ; le contre-exemple devient un test permanent.

Deux autres erreurs se lisent avec un cas minimal. Le dernier indice d’une liste non vide est len(t) − 1 : t[len(t)] déborde. Une fonction qui renvoie « positif » pour n > 0 et « négatif » pour n < 0, sans autre branche, renvoie None pour zéro. Nommer une variable secondes alors qu’elle contient des minutes peut cacher une conversion erronée. Relie chaque défaut à une entrée discriminante, au résultat attendu et à la première étape qui diverge.

Les flottants représentent de nombreux décimaux de façon approchée : 0.1 + 0.2 n’est généralement pas exactement 0.3 en Python. Une comparaison par proximité demande une règle liée au besoin. math.isclose exige |a − b| ≤ max(abs_tol, rel_tol × max(|a|, |b|)) : la tolérance relative est proportionnelle à la plus grande valeur absolue ; près de zéro, une tolérance absolue positive peut être nécessaire. Notre fonction limite les valeurs à ±10¹², les tolérances à des nombres finis et refuse booléens, NaN et infinis.

Une tolérance ne corrige pas une erreur d’unité ou un algorithme faux. La proximité n’est pas forcément transitive : avec une tolérance absolue de 1, 0 est proche de 0,75, qui est proche de 1,5, mais 0 n’est pas proche de 1,5. Documente donc son usage au lieu de remplacer toutes les égalités. Les assertions conviennent aux tests ; les contrôles du contrat utilisent ici des exceptions explicites, car Python peut supprimer les assert en mode optimisé.

Voir pour comprendre

Un diagnostic qui reste vérifiable après correction

Cas minimal : −1 dans l’intervalle fermé [0 ; 10].

  1. Prévoir : faux−1 est sous le minimum ; le contrat donne l’oracle.
    compare à
  2. Observer : vraiAvec « ou », la seconde condition −1 ≤ 10 suffit.
    localise
  3. Expliquer la divergenceL’union remplace l’intersection des deux contraintes.
    corrige
  4. Rétablir les deux bornes0 ≤ x ≤ 10 ; garder les contrôles de types et d’ordre.
    rejoue
  5. Conserver les tests−1, 0, 10, 11 et [0 ; 0] distinguent extérieurs et bornes.

Lis le schéma. Chaque étape conserve son rôle : une observation n’est pas encore une cause, et une correction n’est pas encore une preuve générale.

Le contre-exemple devient un test de non-régression, pas une erreur à oublier.

Schéma original Maxdecours ; données et exemples du cours. · Source du repère · 06/09/2026

Exemples résolus et erreurs expliquées

Le test qui sépare « ou » de « et »

  1. Contrat : dans_intervalle(−1, 0, 10) doit rendre faux, car −1 est extérieur à l’intervalle fermé.
  2. Observation de la version fautive : −1 ≥ 0 est faux, mais −1 ≤ 10 est vrai ; faux ou vrai donne vrai.
  3. La première divergence est dans la combinaison logique, pas dans les bornes ni dans le type.
  4. Corrige la condition, puis garde −1 et 11 parmi les tests ; vérifie aussi 0, 10 et l’intervalle [0 ; 0].

Conclusion. Un test utile discrimine la cause supposée. Le milieu de l’intervalle aurait laissé passer cette erreur.

Laboratoire de code

Languepython

ButConstruire les cas de frontières et comparer des nombres approchés avec des tolérances explicites.

Code solution
def dans_intervalle(valeur, minimum, maximum):
    if any(type(v) is not int for v in (valeur, minimum, maximum)):
        raise ValueError('entiers stricts attendus')
    if minimum > maximum:
        raise ValueError('bornes inversées')
    return minimum <= valeur <= maximum

def cas_frontieres(minimum, maximum):
    dans_intervalle(minimum, minimum, maximum)
    return sorted({minimum - 1, minimum, minimum + 1, maximum - 1, maximum, maximum + 1})

from math import isfinite, isclose

def nombre_fini_borne(valeur, borne):
    return type(valeur) in (int, float) and abs(valeur) <= borne and isfinite(valeur)

def proches(a, b, rel_tol=1e-9, abs_tol=0.0):
    if not all(nombre_fini_borne(v, 1e12) for v in (a, b)):
        raise ValueError('nombres finis entre -1e12 et 1e12 attendus')
    if not nombre_fini_borne(rel_tol, 1) or not 0 <= rel_tol < 1:
        raise ValueError('tolérance relative dans [0, 1[')
    if not nombre_fini_borne(abs_tol, 1) or not 0 <= abs_tol <= 1:
        raise ValueError('tolérance absolue dans [0, 1]')
    return isclose(a, b, rel_tol=rel_tol, abs_tol=abs_tol)

Tests

assert cas_frontieres(0, 10) == [-1, 0, 1, 9, 10, 11]
assert [dans_intervalle(x, 0, 10) for x in cas_frontieres(0, 10)] == [False, True, True, True, True, False]
assert cas_frontieres(0, 0) == [-1, 0, 1]
assert dans_intervalle(0, 0, 0)
assert proches(0.1 + 0.2, 0.3)
assert not proches(1e-12, 0) and proches(1e-12, 0, abs_tol=1e-9)
assert proches(0, 0.75, rel_tol=0, abs_tol=1)
assert proches(0.75, 1.5, rel_tol=0, abs_tol=1)
assert not proches(0, 1.5, rel_tol=0, abs_tol=1)
for action in (lambda: cas_frontieres(10, 0), lambda: dans_intervalle(True, 0, 10),
               lambda: proches(1, 2, abs_tol=True), lambda: proches(float('nan'), 0)):
    try:
        action()
    except ValueError:
        pass
    else:
        raise AssertionError('contrat non respecté')

Trace

  • −1 et 11 doivent être refusés comme valeurs de l’intervalle ; 0 et 10 sont inclus.
  • Lorsque les bornes sont confondues, les doublons des cas de test sont retirés.
  • Près de zéro, la tolérance absolue fournit une échelle que la seule tolérance relative ne donne pas.

Clinique de bogue

Indice observéAvec a ≤ b, tous les entiers sont acceptés, y compris les voisins extérieurs −1 et 11 pour [0 ; 10].

CauseL’appartenance est l’intersection de deux conditions : être au moins égal au minimum et au plus égal au maximum. Leur union par or couvre tous les entiers lorsque les bornes sont ordonnées ; modifier une seule borne ne répare pas cette logique.

Poursuivre avec l’abonnement

Passe des traces à tes propres programmes

Lis les explications, les schémas et les exemples résolus pour suivre les calculs et leurs limites.

Entraîne-toi avec les ateliers, les diagnostics, le projet, les douze questions et les cartes de révision.

12 questions · 12 cartes. Ta reprise et tes révisions sont enregistrées dans ce navigateur. Elles ne se synchronisent pas entre appareils.

Accéder à l’entraînement

Vérifier et prolonger

Sources du cours

Édition Maxdecours · Vérifié le .

Programme de spécialité NSI Terminale, BO spécial du 25 juillet 2019

  1. Programme de l'enseignement de spécialité NSI de terminaleMinistère de l'Éducation nationale · consulté le 2026-09-06
  2. Calculabilité et décidabilitéÉduscol · consulté le 2026-09-06
  3. RécursivitéÉduscol · consulté le 2026-09-06
  4. Modularité et APIÉduscol · consulté le 2026-09-06
  5. Le paradigme fonctionnelÉduscol · consulté le 2026-09-06
  6. Écriture de testsÉduscol · consulté le 2026-09-06
  7. Mise au point des programmes, gestion des bugsÉduscol · consulté le 2026-09-06
  8. Modules Python : fichiers, importation et espace de nomsPython Software Foundation · consulté le 2026-09-06
  9. Fonctions Python : cadres locaux et paramètres par défautPython Software Foundation · consulté le 2026-09-06
  10. math : nombres finis et comparaison approchéePython Software Foundation · consulté le 2026-09-06