Adloun

Prouver et analyser : la boîte à outils formalisée

Cours complet · informatique (tronc commun des prépas scientifiques), chapitre 10 · prépas scientifiques, tronc commun

Travailler ce chapitre sur Adloun Exercices corrigés de ce chapitre

<i class="fa-solid fa-compass mr-2" style="color:#9A563B"></i>10.1 Introduction et motivation

Le premier semestre a pratiqué la discipline de programmation en artisan : spécifier, tester, exhiber un variant, énoncer un invariant. Ce chapitre la formalise en théorie outillée — non par goût de l'abstraction, mais parce que les programmes du second semestre (graphes, plus courts chemins) sont trop subtils pour l'intuition seule. On y précise d'abord ce qu'est un programme Python — la différence entre expression et instruction, et la notion d'effet de bord, source des bogues les plus déroutants du langage. On y définit ensuite avec exactitude ce que « cet algorithme est juste » veut dire : correction partielle (s'il s'arrête, le résultat est bon) et correction totale (il s'arrête, et le résultat est bon) — deux propriétés réellement distinctes, dont la séparation éclaire toute la méthodologie variant/invariant. On y systématise enfin les deux outils d'évaluation : le jeu de tests construit par partitionnement du domaine d'entrée, et la complexité — désormais en temps et en espace.

Le mot d'ordre du programme : on ne prouve pas systématiquement tous les algorithmes, mais on dégage l'idée qu'un algorithme doit se prouver et que sa programmation doit se tester. Les deux, toujours : la preuve garantit l'algorithme, le test contrôle le programme — et chacun attrape ce que l'autre laisse passer.

10.2 Expressions, instructions, effets de bord

10.2.1 Deux natures de code

Définition 10.1Expression, instruction

Une expression désigne un calcul qui produit une valeur : 2 + 3, t[i] &gt; m, len(t) - 1, f(x). Une instruction désigne une action qui modifie l'état du programme ou son déroulement : l'affectation x = 2 + 3, le return, le if, la boucle. Une expression peut figurer partout où une valeur est attendue ; une instruction, jamais.

iRemarque

Qu'en Python l'affectation soit une instruction — et non une expression qui vaudrait quelque chose — est un choix des concepteurs du langage. Conséquence heureuse : le grand classique du C, écrire if (x = 3) (affectation) en croyant tester x == 3 (comparaison), est en Python une erreur de syntaxe — le bogue est promu faute de frappe détectée. Conséquence à connaître : on ne peut pas écrire y = (x = 3) + 1 ; une chaîne d'affectations a = b = 0 est une forme spéciale, pas une expression.

10.2.2 Les effets de bord

Définition 10.2Effet de bord

Un fragment de code a un effet de bord s'il modifie un état observable au-delà de la valeur qu'il renvoie : modifier une liste ou un dictionnaire passé en argument, modifier une variable globale, écrire à l'écran ou dans un fichier. Une fonction sans effet de bord — qui ne fait que calculer sa valeur de retour — est dite pure.

Exemple 10.3Pure ou à effet de bord : le contrat doit le dire

def trie_copie(t: list) -> list:      # PURE : t est intact,
    return sorted(t)                  # la valeur de retour porte tout

def trie_en_place(t: list) -> None:   # EFFET DE BORD assumé :
    t.sort()                          # t est modifiée, rien n'est renvoyé

Les deux conventions sont légitimes ; le danger est l'hybride non documenté — une fonction qui modifie son argument et renvoie quelque chose, ou pire, qui le modifie « par accident ». Le contrat (chapitre 1) doit toujours répondre : cette fonction modifie-t-elle ses arguments ?

AttentionL'effet de bord accidentel : le partage

L'affectation Python ne copie jamais les listes : elle partage (chapitre 8). L'effet de bord accidentel naît de ce partage :


def ajoute_zero(t: list) -> list:
    t.append(0)            # modifie la liste de L'APPELANT !
    return t

notes = [12, 15]
copie = notes              # alias, pas copie
ajoute_zero(notes)
# notes == copie == [12, 15, 0] : deux noms, une liste, un effet partout

Règles pratiques : pour ne pas modifier, travailler sur list(t) ; pour modifier, l'écrire dans la docstring et renvoyer None (la convention de t.sort() : l'absence de valeur de retour signale l'effet de bord).

10.3 Annoter et vérifier : assertions

Définition 10.4Annotations d'un bloc

Un bloc d'instructions s'annote, en commentaires, par trois énoncés logiques :

  • sa précondition : ce qui est supposé vrai avant le bloc ;
  • sa postcondition : ce qui est garanti vrai après ;
  • pour une boucle, sa propriété invariante : vraie à chaque passage au même point.

def division(a: int, b: int) -> tuple:
    # Précondition : a >= 0 et b > 0
    q, r = 0, a
    while r >= b:
        # Invariant : a == b * q + r  et  r >= 0
        q, r = q + 1, r - b
    # Postcondition : a == b * q + r  et  0 <= r < b
    return (q, r)

Le programme du semestre est explicite : ces annotations se font sans formalisme imposé — un français précis suffit — mais elles se font.

Définition 10.5Assertion

L'instruction assert condition vérifie la condition à l'exécution : si elle est fausse, le programme s'arrête immédiatement (AssertionError). Usage encouragé : transformer les annotations critiques en vérifications vivantes, en particulier valider les entrées :


def division(a: int, b: int) -> tuple:
    assert a >= 0 and b > 0, "précondition violée"
    ...

(La forme exigible de l'annexe du programme est le assert nu, sans message ; le message est un confort de rédaction.) Une précondition assertée ne peut plus être violée silencieusement : l'erreur éclate chez le fautif (l'appelant), au bon endroit, au lieu de produire un résultat faux trois fonctions plus loin. (Le rattrapage des erreurs — les exceptions — n'est pas au programme : une assertion levée arrête le programme, et c'est exactement ce qu'on lui demande.)

Méthode : Où placer des assertions ?

Trois emplacements rentables : à l'entrée d'une fonction (préconditions — surtout celles qu'un appelant peut violer par mégarde : tableau trié, valeur positive) ; à la sortie d'un calcul délicat (postcondition vérifiable à bas coût : le résultat appartient bien à l'intervalle promis) ; dans les jeux de tests (chapitre 1 — c'est leur maison naturelle). On évite en revanche d'asserter ce qui coûterait plus cher que le calcul lui-même — vérifier qu'un tableau est trié est , acceptable ; re-trier pour comparer ne l'est pas.

10.4 Terminaison et correction

10.4.1 Les trois propriétés

Définition 10.6Correction partielle, terminaison, correction totale

Soit un algorithme muni d'une spécification (précondition , postcondition ).

  • Il est partiellement correct si : pour toute entrée vérifiant , si l'exécution se termine, alors le résultat vérifie . (Rien n'est affirmé quand elle ne termine pas.)
  • Il termine si : pour toute entrée vérifiant , l'exécution s'arrête en un nombre fini d'étapes.
  • Il est totalement correct s'il est partiellement correct et termine : pour toute entrée légale, il s'arrête et rend un résultat conforme.
Exemple 10.7Partiellement correct sans terminer

Les deux propriétés sont réellement indépendantes. La fonction suivante cherche un entier dont le carré vaut :


def racine_si_carre(n: int) -> int:
    # Précondition : n >= 0. Postcondition : le résultat r vérifie r * r == n.
    r = 0
    while r * r != n:
        # Invariant : aucun entier de [0, r-1] n'a pour carré n... et r*r != n testé
        r = r + 1
    return r

Elle est partiellement correcte : si elle s'arrête, c'est que la condition du while est devenue fausse, donc — la postcondition est garantie par la structure même de la boucle. Mais sur , elle ne termine jamais ( n'est pas un carré : croît sans fin). Partiellement correcte, non totalement correcte : la moitié « invariant » du travail était faite, la moitié « variant » manquait — et aucun variant n'existe, puisque la boucle ne termine pas. La version totale corrige la spécification ou l'algorithme : s'arrêter dès que et renvoyer aussi un verdict.

ImportantLa division du travail

La séparation n'est pas un raffinement de vocabulaire : c'est une division du travail de preuve.

PropriétéOutilQuestion à se poser
Terminaisonvariantquelle quantité entière décroît strictement ?
Correction partielleinvariantqu'est-ce qui reste vrai à chaque tour ?
Correction totaleles deux—

L'invariant ne dit rien de l'arrêt ; le variant ne dit rien du résultat. Les deux preuves sont indépendantes, se mènent séparément, et la correction totale est leur conjonction.

10.4.2 Le schéma de preuve complet

Méthode : Prouver totalement une boucle

Pour une boucle while C d'invariant et de variant :

  • Initialisation : est vrai avant le premier tour (conséquence de la précondition) ;
  • Conservation : si et sont vrais au début d'un tour, est vrai à la fin ;
  • Conclusion : à la sortie, est vrai — et doit impliquer la postcondition ;
  • Variant : est entier, tant que la boucle tourne, et chaque tour le fait strictement décroître.

Les points 1–3 donnent la correction partielle, le point 4 la terminaison. Le point 3 est celui qu'on oublie : un invariant vrai mais trop faible pour impliquer la postcondition ne prouve rien — l'invariant se choisit en partant de la postcondition, pas du code.

Exemple 10.8Preuve totale de la division euclidienne

Reprenons division avec et . (1) Avant la boucle, , : et (précondition). ✓ (2) Si , et (condition), alors après le tour : et . ✓ (3) À la sortie : , et (négation de la condition) — c'est exactement la postcondition, qui caractérise le quotient et le reste. ✓ (4) : entier, positif pendant la boucle (), et décroît de à chaque tour. ✓ L'algorithme est totalement correct. (Remarquer le rôle de la précondition : sans elle, le point 4 s'effondre — donne une boucle infinie, et la preuve le montre : prouver, c'est aussi découvrir les préconditions qui manquent.)

10.5 Les jeux de tests, méthodiquement

Définition 10.9Partitionnement du domaine d'entrée

Tester toutes les entrées est impossible ; l'idée du partitionnement est de découper le domaine d'entrée en classes à l'intérieur desquelles le programme suit le même chemin — puis de tester au moins une entrée par classe, plus les limites entre classes. Pour division(a, b) :

ClasseReprésentantSortie attendue
(zéro tour de boucle)
divise (reste nul)
cas général
limites : ; ; , , , ,

Six tests couvrent ce que mille entrées aléatoires « moyennes » ne couvriraient pas : chaque frontière du comportement.

Méthode : Construire le partitionnement

Les classes se lisent à deux endroits : dans la spécification (présent / absent ; vide / non vide ; positif / négatif / nul) et dans la structure du code (chaque branche de if, zéro tour / un tour / plusieurs tours de chaque boucle). Les limites sont les valeurs où l'on change de classe : , , la longueur du tableau, l'égalité parfaite — c'est là que vivent les erreurs de bornes, moitié des bogues du semestre 1. Un jeu de tests se rend, comme une démonstration : avec ses entrées et ses sorties attendues, calculées à la main.

10.6 La complexité, en temps et en espace

Définition 10.10Complexité en espace

La complexité en espace d'un algorithme est l'ordre de grandeur de la mémoire supplémentaire qu'il alloue (au-delà de l'entrée elle-même), dans le cas le pire, en fonction de la taille de l'entrée.

Exemple 10.11Le même problème, deux profils mémoire

Renverser une liste (chapitre 1) :


def renverse_copie(t: list) -> list:        # ESPACE O(n) : une liste neuve
    return [t[len(t) - 1 - i] for i in range(len(t))]

def renverse_en_place(t: list) -> None:     # ESPACE O(1) : deux indices
    g, d = 0, len(t) - 1
    while g < d:
        # Invariant : t[0..g-1] et t[d+1..n-1] sont échangés ; variant : d - g
        t[g], t[d] = t[d], t[g]
        g, d = g + 1, d - 1

Même temps , mais contre en espace — et des contrats opposés (pure contre effet de bord). Le vocabulaire du chapitre 9 se relit ainsi : un tri en place est un tri en espace (sélection, insertion), le tri fusion est en espace ; et la récursivité a un coût d'espace caché, la pile — pour la dichotomie, pour somme_jusqua (chapitre 6).

iRemarque

Temps et espace s'échangent souvent : le dictionnaire des « déjà-vus » (chapitre 2) achète du temps () contre de l'espace (). Annoncer les deux coûts fait partie de l'analyse — le programme le demande désormais explicitement.

<i class="fa-solid fa-dumbbell mr-2" style="color:#2E7559"></i>10.7 Exercices résolus

Niveau (Application directe du cours)

Exercice 1 : Expression ou instruction ?

Classer : x + 1 ; x = x + 1 ; t.append(3) ; len(t) == 0 ; print(&quot;ok&quot;) ; return x — et dire lesquels ont un effet de bord.

Démonstration (Solution)

x + 1 : expression, pure. x = x + 1 : instruction (l'affectation), effet sur l'état local. t.append(3) : techniquement une expression (un appel, qui vaut None) utilisée comme instruction — effet de bord sur la liste . len(t) == 0 : expression, pure. print(&quot;ok&quot;) : appel valant None, effet de bord (écriture à l'écran). return x : instruction (contrôle du flot). (La frontière subtile : un appel de fonction est toujours une expression, mais sa valeur peut être inintéressante — None — et son intérêt tout entier dans l'effet de bord. D'où le bogue classique t = t.append(3), qui affecte None à t : la liste est perdue.)

Exercice 2 : Annoter un bloc complet

Annoter (précondition, invariant, postcondition) puis prouver totalement la fonction :


def somme_impairs(n: int) -> int:
    s, k = 0, 0
    while k < n:
        s = s + 2 * k + 1
        k = k + 1
    return s

Que calcule-t-elle ?

Démonstration (Solution)

Précondition : (entier). Invariant : (la somme des premiers impairs vaut ). Initialisation : . ✓ Conservation : si , alors , et devient : l'invariant suit. ✓ Sortie : donne (car croît de depuis : il atteint exactement) et . Postcondition : la fonction renvoie . Variant : , entier, positif pendant la boucle, décroît de par tour. ✓ La fonction calcule le carré par additions — totalement correcte. (L'invariant ne se devine pas en relisant le code ligne à ligne : on le trouve en exécutant à la main — — puis en conjecturant ; la preuve transforme ensuite la conjecture en certitude. Démarche expérimentale, conclusion mathématique.)

Exercice 3 : Le partitionnement de `dichotomie`

Construire le partitionnement du domaine d'entrée de la recherche dichotomique (chapitre 5) : classes, limites, et le jeu de tests qui en découle — puis comparer au « jeu de tests canonique » donné au chapitre 5.

Démonstration (Solution)

Classes issues de la spécification : élément présent / absent. Classes issues de la structure : présent trouvé au premier milieu / après descente à gauche / après descente à droite ; absent plus petit que tout / plus grand que tout / entre deux éléments. Classes de taille : tableau vide (zéro tour), singleton (un tour), taille générale. Limites : présent en première position, en dernière position, au milieu exact ; doublons (toutes occurrences identiques).

Le jeu de tests : un représentant par case — c'est, à l'ordre près, exactement le « jeu canonique » du chapitre 5 : présent(milieu, première, dernière), absent(avant, après, entre), vide, singleton (présent, absent), tous égaux. (Ce qui était au chapitre 5 une liste apprise devient une méthode : le canonique n'était pas un catalogue arbitraire, c'était le partitionnement — désormais, on sait le reconstruire pour n'importe quelle fonction.)

Niveau (Application avec raisonnement intermédiaire)

Exercice 4 : L'effet de bord qui traverse trois fonctions

Prédire l'affichage, diagnostiquer, proposer deux corrections de philosophies opposées :


def normalise(notes: list) -> list:
    m = max(notes)
    for i in range(len(notes)):
        notes[i] = notes[i] / m
    return notes

brutes = [10, 15, 20]
normalisees = normalise(brutes)
print(brutes)
Démonstration (Solution)

Affichage : [0.5, 0.75, 1.0] — les notes brutes ont disparu. normalise écrit dans la liste reçue (effet de bord) tout en renvoyant cette même liste : l'hybride non documenté du cours. L'appelant, voyant un return, croit légitimement la source préservée.

Correction pure : construire une liste neuve — return [x / m for x in notes] ; brutes reste intacte, le return dit vrai. Correction « effet assumé » : garder la mutation, supprimer le return (renvoyer None) et documenter « modifie notes en place » — la convention de list.sort. (Le choix entre les deux est un choix de conception à expliciter — troisième compétence du programme ; le crime n'est ni la pureté ni la mutation, c'est l'ambiguïté.)

Exercice 5 : Correct mais sans variant simple — et l'inverse

(a) Montrer que la boucle suivante est partiellement correcte pour la postcondition « renvoie un indice de maximum de », puis montrer qu'elle peut ne pas terminer :


import random
def indice_max_hasard(t: list) -> int:
    # Précondition : t non vide
    while True:
        i = random.randrange(len(t))
        if all(t[i] >= x for x in t):
            return i

(b) Donner à l'inverse une boucle qui termine toujours mais n'est pas partiellement correcte pour sa spécification.

Démonstration (Solution)

(a) Correction partielle : l'unique sortie est le return i, gardé par le test all(t[i] &gt;= x ...) — si la fonction s'arrête, l'indice renvoyé majore tous les éléments : postcondition garantie par construction. Non-terminaison possible : rien n'oblige le tirage à atteindre un indice de maximum — la suite de tirages (improbable mais possible) boucle sans fin sur un tableau dont le maximum est ailleurs. Aucun variant n'existe : aucune quantité ne décroît à coup sûr. (On dit que l'algorithme termine presque sûrement — notion du cours de probabilités, hors de notre définition de terminaison, qui exige l'arrêt pour toute exécution.)

(b) L'exemple minimal : def maximum(t): return t[0] avec la postcondition « renvoie le maximum ». Termine toujours (aucune boucle), faux dès que le maximum n'est pas en tête : terminaison sans correction partielle. (Les deux moitiés de la correction totale sont bien indépendantes — chacune peut exister sans l'autre, et chacune a son outil.)

Exercice 6 : Tester les limites — l'inventaire des classes de `mediane`

La fonction mediane(t) (chapitre 4) trie une copie et lit le ou les éléments centraux. Construire son partitionnement complet (avec sorties attendues), en identifiant la classe que les tests « naturels » oublient toujours.

Démonstration (Solution)

Classes structurelles : effectif impair (un élément central) / pair (moyenne de deux). Limites de taille : () ; ( moyenne des deux). Classes de valeurs : éléments tous égaux ( cette valeur) ; doublons autour du centre () ; valeurs non triées en entrée (vérifier que la fonction trie bien : ) ; et la classe oubliée : la médiane d'un effectif pair peut ne pas appartenir au tableau — , de type flottant alors que les entrées sont entières. Jeu de tests :


assert mediane([5]) == 5
assert mediane([1, 2]) == 1.5            # la classe oubliée : résultat HORS tableau
assert mediane([3, 1, 2]) == 2           # entrée non triée
assert mediane([1, 2, 2, 3]) == 2.0
assert mediane([4, 4, 4, 4]) == 4.0
assert mediane([2, 1]) == 1.5            # pair, non trié

(Le test attrape deux familles de bogues réels : la division entière // utilisée par erreur — qui rendrait — et la confusion « la médiane est un élément du tableau ». Les classes oubliées sont presque toujours celles où le type ou la provenance du résultat change.)

Exercice 7 : L'espace caché de la récursivité

Donner le coût en temps et en espace de chacune des fonctions : somme(t) itérative ; tri_fusion(t) ; dichotomie_rec ; sous_listes(t) — en comptant pour les récursives la pile d'appels et les objets construits.

Démonstration (Solution)
FonctionTempsEspaceD'où vient l'espace
`somme` (boucle)deux variables
`tri_fusion`listes de fusion ( de pile)
`dichotomie_rec`la pile (aucun objet construit)
`sous_listes`le résultat : listes de taille moyenne

Trois sources d'espace à inventorier systématiquement : les structures auxiliaires (les listes du tri fusion), la pile de récursion (un contexte par appel en attente — chapitre 6), et le résultat lui-même quand il est volumineux (les sous-listes : aucune implémentation ne fera mieux, l'espace est dans la spécification). (Pour le tri fusion, une subtilité honnête : les tranches t[:m] créent elles aussi des copies — l'implémentation du chapitre 9 est d'espace au total grâce à la libération au fil des fusions, mais si l'on retenait tout ; les analyses d'espace exigent de savoir quand la mémoire se libère, ce qui les rend plus délicates que celles de temps.)

Niveau (Raisonnement subtil ou plusieurs étapes)

Exercice 8 : Le drapeau tricolore, preuve totale intégrale

Trier en place, en un seul parcours, un tableau ne contenant que des valeurs , , (chapitre 9, banque) :


def drapeau(t: list) -> None:
    g, i, d = 0, 0, len(t) - 1
    while i <= d:
        if t[i] == 0:
            t[g], t[i] = t[i], t[g]
            g += 1; i += 1
        elif t[i] == 2:
            t[i], t[d] = t[d], t[i]
            d -= 1
        else:
            i += 1

Énoncer l'invariant à quatre zones, le variant, et rédiger la preuve totale — en expliquant pourquoi le cas t[i] == 2 n'incrémente pas i.

Démonstration (Solution)

Invariant — le tableau est partagé en quatre zones :

Initialisation : , — les trois zones connues sont vides, tout est inconnu. ✓

Conservation, cas par cas. Si : l'échange avec ramène ce en bout de zone-0 ; la case échangée était soit un (zone-1 non vide : son premier élément), soit la case elle-même () — dans les deux cas, après g += 1; i += 1, les zones 0 et 1 sont correctes. Si : l'échange l'envoie en bout de zone-2 et recule ; mais la valeur reçue de est inconnue — c'est pourquoi i ne bouge pas : la case doit être réexaminée au tour suivant (l'incrémenter laisserait passer un ou un non classé : le bogue classique de cet algorithme, que l'invariant interdit noir sur blanc). Si : il prolonge la zone-1, avance. ✓

Sortie : — la zone inconnue est vide ; les quatre zones se réduisent à : trié, et seules des transpositions ont eu lieu : permutation. ✓

Variant : (la taille de la zone inconnue) — positif pendant la boucle, et chaque branche le fait décroître strictement ( avance ou recule). ✓ Correction totale, temps , espace . (L'invariant à zones est le schéma des algorithmes de partition — on l'a entrevu avec le drapeau à deux couleurs du chapitre 1 et la partition du tri rapide ; sa force : chaque ligne de code se justifie par « quelle zone cette case rejoint-elle ? », et l'anomalie du cas 2 — ne pas avancer — cesse d'être une astuce pour devenir une nécessité démontrée.)

Exercice 9 : Un invariant trop faible

Un étudiant veut prouver que maximum (chapitre 2) renvoie bien le maximum, avec l'invariant : « est un élément de ». Vérifier que est bien un invariant (initialisé, conservé), puis montrer qu'il ne permet pas de conclure — et rappeler l'invariant qui convient. En tirer la méthode générale.

Démonstration (Solution)

est un invariant irréprochable : initialement (élément du préfixe), et chaque tour laisse inchangé ou l'affecte à — toujours un élément du préfixe étendu. ✓ Mais à la sortie, donne seulement : « appartient à » — ce que vérifie aussi return t[0] ! La postcondition « pour tout de » n'est pas impliquée : l'invariant est vrai mais trop faible. Celui qui convient (chapitre 1, exercice 6) : « est le maximum de » — appartenance et domination.

Méthode générale : l'invariant se construit à rebours depuis la postcondition — on écrit ce qu'on veut à la sortie (), on remplace la borne finale par la variable de boucle (), et l'on vérifie ensuite initialisation et conservation. Partir du code donne des invariants vrais ; partir de la postcondition donne des invariants utiles. (C'est le point 3 de la méthode du cours, vu en creux : la conclusion est une obligation de preuve à part entière, et c'est elle qui calibre la force de l'invariant.)

Exercice 10 : Peut-on tout prouver ? Peut-on tout tester ?

Discuter, exemples à l'appui, les limites des deux outils : (a) pourquoi aucun programme général ne peut décider la terminaison de tous les programmes (argument informel de l'arrêt) ; (b) pourquoi aucun jeu de tests fini ne peut établir la correction d'un programme sur un domaine infini ; (c) conclure sur la complémentarité preuve/test prônée par le programme.

Démonstration (Solution)

(a) Supposons disposer d'une fonction termine(f, x) qui répondrait, pour tout programme f et toute entrée x, si f(x) s'arrête. On construit alors le programme paradoxal :


def paradoxe(f):
    if termine(f, f):           # si f(f) s'arrête...
        while True:              # ... boucler sans fin
            pass
    return 0                     # sinon, s'arrêter

Que fait paradoxe(paradoxe) ? S'il s'arrête, c'est que termine a répondu « ne s'arrête pas » — contradiction ; s'il boucle, c'est que termine a répondu « s'arrête » — contradiction encore. Donc termine ne peut pas exister : c'est l'indécidabilité de l'arrêt (Turing, 1936). La terminaison se prouve au cas par cas, par l'intelligence d'un variant — jamais par un vérificateur universel ; et l'on a rencontré au chapitre 1 une boucle de quatre lignes (Syracuse) dont la terminaison résiste depuis un siècle.

(b) Le domaine de division(a, b) est infini ; un jeu de tests en visite un nombre fini. Pour tout jeu de tests fini, il existe un programme faux qui le passe : celui qui code en dur les bonnes réponses des tests et répond partout ailleurs. Le test établit la présence de conformité sur les cas essayés, jamais la correction universelle — chapitre 1, désormais argumenté.

(c) La preuve garantit l'algorithme sur tout le domaine, mais porte sur le texte idéal qu'on a raisonné — pas sur le fichier réellement exécuté, ses fautes de frappe, ses bornes décalées, sa bibliothèque surprenante (round(2.5) !). Le test exécute le vrai programme sur de vraies entrées, mais en nombre fini. L'un couvre l'infini des entrées, l'autre la réalité de la machine : aucun ne contient l'autre, et le programme officiel demande les deux — un algorithme doit se prouver, sa programmation doit se tester. (Les outils professionnels — preuve assistée, génération de tests — industrialisent chacun des deux pôles sans jamais abolir l'autre ; le grand théorème de (a) garantit même qu'aucune automatisation complète n'existera jamais : il restera toujours du métier.)

Synthèse du chapitre (à retenir)
  • Expression (vaut quelque chose) / instruction (agit) ; l'affectation est une instruction par choix de conception de Python (le if (x = 3) du C y est une erreur de syntaxe). Effet de bord : modifier un argument, une globale, l'écran ; fonction pure sinon ; le contrat dit toujours si les arguments sont modifiés (convention : mutation renvoyer None) ; piège du partage (t = t.append(x) perd la liste).
  • Annotations : précondition / postcondition / invariant, en commentaires, sans formalisme imposé ; assertions pour rendre les préconditions vivantes (validation des entrées ; arrêt du programme ; pas d'exceptions au programme) — asserter ce qui est vérifiable à bas coût.
  • Correction partielle (si arrêt, alors résultat conforme — outil : invariant) ; terminaison (arrêt sur toute entrée légale — outil : variant) ; correction totale les deux, preuves indépendantes. Schéma complet : initialisation, conservation, conclusion ( — l'étape qu'on oublie), variant. Un invariant vrai peut être trop faible : le construire à rebours depuis la postcondition.
  • Jeux de tests : partitionnement du domaine (classes lues dans la spécification et dans les branches du code) limites entre classes (, , , égalité, zéro tour de boucle) ; entrées et sorties attendues calculées à la main ; aucun jeu fini ne prouve (le programme qui code les réponses en dur).
  • Complexité en espace : mémoire supplémentaire au pire — structures auxiliaires pile de récursion résultat ; en place ; temps et espace s'échangent (le dictionnaire du chapitre 2).
  • Limites de principe : l'arrêt est indécidable (l'argument du paradoxe auto-appliqué) ; preuve et test sont complémentaires — l'algorithme se prouve, le programme se teste.

10.8 Exercices d'entraînement

Cette banque d'exercices, classée par thème, couvre l'intégralité du chapitre. La numérotation prolonge celle des dix exercices résolus. Légende : application directe, raisonnement intermédiaire, approfondissement ; le symbole signale un classique incontournable.

A. Expressions, instructions, effets de bord

B. Preuves de correction totale

C. Jeux de tests et partitionnement

D. Complexité en espace et synthèses

Continuer sur Adloun : animation, QCM, fiches, exercices