Retour à la page d'accueilNSI · Terminale (maths complémentaires) · Changer de matière
Commencer gratuitement
Terminale (maths complémentaires) · NSI
Structures de données, bases de données et algorithmique · Algorithmique avancée et programmation

Programmation, tests et calculabilité

Tu lances ton application de réservation de billets TER et elle « plante » au moment de payer. Comment savoir si le programme est correct avant de le livrer ? Et existe-t-il un outil capable de vérifier automatiquement que n'importe quel programme s'arrête un jour ? Dans cette page, tu apprends à structurer un programme (objets, modules), à le tester, à écrire des fonctions récursives, puis tu découvres une limite surprenante de l'informatique. Nous répondrons à la question de l'outil universel à la fin.

Objets, classes et modularité

Un programme de grande taille se découpe en petites briques que l'on peut comprendre, tester et réutiliser séparément.

class Compte:

    def __init__(self, titulaire, solde):

        self.titulaire = titulaire

        self.solde = solde

    def deposer(self, montant):

        self.solde = self.solde + montant

c = Compte("Léa", 20)

c.deposer(15)

print(c.solde)   # 35

Ici Compte est la classe, titulaire et solde sont des attributs, __init__ (le constructeur) et deposer sont des méthodes. On accède à un attribut avec objet.attribut et on appelle une méthode avec objet.methode(...).

Exemple : une pile (LIFO) a pour interface empiler, depiler, est_vide. On peut l'implémenter avec une liste Python, ou avec une liste chaînée. Celui qui utilise la pile n'a pas besoin de savoir laquelle.

class Pile:

    def __init__(self):

        self.contenu = []

    def empiler(self, x):

        self.contenu.append(x)

    def depiler(self):

        return self.contenu.pop()

    def est_vide(self):

        return len(self.contenu) == 0

Modules et API

Un module est un fichier Python qui regroupe des fonctions et des classes. On l'utilise avec import.

from math import sqrt

print(sqrt(49))   # affiche sept, sous forme de flottant

L'API (interface de programmation) d'une bibliothèque est la liste de ce qu'elle propose : noms, paramètres, rôle, décrits dans sa documentation. Pour sqrt, l'API dit : « prend un nombre positif, renvoie sa racine carrée ». On n'a pas besoin de connaître le code interne. Remarque : Python affiche les nombres décimaux avec un point (7.0), alors qu'en français on écrit 7,0.

Entraîne-toi★☆☆Pas commencé
On définit une classe : a) Identifier la classe, les attributs et les méthodes. b) Écrire les instructions qui créent un objet c pour « Camille » avec 100 €, puis…

On définit une classe :

class Compte:

    def __init__(self, titulaire, solde):

        self.titulaire = titulaire

        self.solde = solde

    def deposer(self, montant):

        self.solde = self.solde + montant
  1. a) Identifier la classe, les attributs et les méthodes.
  1. b) Écrire les instructions qui créent un objet c pour « Camille » avec 100 €, puis déposent 50 €.
  1. c) Donner la valeur de c.solde ensuite.
Résoudre
Entraîne-toi★☆☆Pas commencé
a) Expliquer ce qu'est un module Python et donner l'instruction pour importer la fonction sqrt du module math. b) Expliquer la notion d'interface (API) d'une…
  1. a) Expliquer ce qu'est un module Python et donner l'instruction pour importer la fonction sqrt du module math.
  1. b) Expliquer la notion d'interface (API) d'une bibliothèque en prenant cet exemple.
  1. c) Citer deux avantages de la modularité pour un grand programme.
Résoudre
Entraîne-toi★★☆Pas commencé
On implémente une pile avec une classe Pile contenant la liste contenu. a) Écrire les méthodes empiler, depiler et est_vide. b) Écrire une docstring et une précondition…

On implémente une pile avec une classe Pile contenant la liste contenu.

  1. a) Écrire les méthodes empiler, depiler et est_vide.
  1. b) Écrire une docstring et une précondition (assert) pour depiler.
  1. c) Expliquer la différence entre l'interface d'une pile et son implémentation, en proposant une autre implémentation possible.
Résoudre
Entraîne-toi★★★Pas commencé
On souhaite écrire un module geometrie.py qui gère des points du plan et des segments. a) Proposer une classe Point (attributs x, y) avec une méthode distance(autre),…

On souhaite écrire un module geometrie.py qui gère des points du plan et des segments.

  1. a) Proposer une classe Point (attributs x, y) avec une méthode distance(autre), puis une classe Segment construite à partir de deux points avec une méthode longueur.
  1. b) Écrire une batterie de tests avec assert : le segment de (0 ; 0) à (3 ; 4) mesure 5 ; un segment réduit à un point mesure 0 ; la distance est symétrique.
  1. c) Expliquer pourquoi on teste aussi les cas limites et discuter l'usage de la comparaison de flottants.
  1. d) Rédiger la documentation de l'API publique du module à l'intention d'un autre développeur.
Résoudre

Mise au point : spécification, assertions et tests

Un programme correct est un programme qui fait ce qu'on a spécifié. Pour s'en assurer, on documente, on protège et on teste.

  • La docstring (texte entre """ sous la ligne def) décrit ce que fait la fonction, ses paramètres et ce qu'elle renvoie. Elle sert de documentation (help(maximum)).

  • La précondition est une condition que les arguments doivent respecter. On la vérifie avec assert.

  • Un jeu de tests est un ensemble de cas dont on connaît le résultat attendu.

def maximum(liste):

    """Renvoie le plus grand élément de liste (non vide)."""

    assert len(liste) > 0, "la liste doit être non vide"

    m = liste[0]

    for x in liste:

        if x > m:

            m = x

    return m

assert maximum([3, 8, 5]) == 8       # cas général

assert maximum([4]) == 4             # un seul élément

assert maximum([-7, -2, -9]) == -2   # valeurs négatives

Si une assertion est fausse, Python s'arrête avec une AssertionError : le bogue est signalé tout de suite.

Cas limites et flottants

Un bon jeu de tests contient des cas limites : liste vide, un seul élément, valeurs négatives, zéro, très grandes valeurs. Les bogues se cachent souvent là.

Entraîne-toi★☆☆Pas commencé
On considère la fonction : a) Écrire trois tests sous forme d'assert : un cas général, un cas à un seul élément et un cas avec des valeurs négatives. b) Écrire une…

On considère la fonction :

def maximum(liste):

    """Renvoie le plus grand élément de liste (non vide)."""

    m = liste[0]

    for x in liste:

        if x > m:

            m = x

    return m
  1. a) Écrire trois tests sous forme d'assert : un cas général, un cas à un seul élément et un cas avec des valeurs négatives.
  1. b) Écrire une précondition avec assert sur la liste.
  1. c) Expliquer le rôle de la docstring.
Résoudre
Entraîne-toi★★☆Pas commencé
La fonction suivante doit renvoyer la moyenne d'une liste de nombres : a) Trouver le bogue et donner un jeu de test qui le met en évidence. b) Corriger la fonction. c)…

La fonction suivante doit renvoyer la moyenne d'une liste de nombres :

def moyenne(liste):

    s = 0

    for i in range(1, len(liste)):

        s = s + liste[i]

    return s / len(liste)
  1. a) Trouver le bogue et donner un jeu de test qui le met en évidence.
  1. b) Corriger la fonction.
  1. c) Indiquer le comportement pour une liste vide et proposer une protection.
Résoudre

Récursivité

Une fonction est récursive quand elle s'appelle elle-même. Elle comporte toujours :

  • un cas de base qui s'arrête sans appel récursif ;

  • un appel récursif sur un problème plus petit, qui se rapproche du cas de base.

def somme_chiffres(n):

    if n < 10:

        return n

    return n % 10 + somme_chiffres(n // 10)

La version itérative (paradigme impératif) utilise une boucle while et une variable accumulatrice :

def somme_chiffres_iter(n):

    s = 0

    while n > 0:

        s = s + n % 10

        n = n // 10

    return s

La version récursive est plus proche de la définition mathématique ; l'itérative évite d'empiler les appels.

Terminaison, correction et coût

  • Terminaison : on exhibe une quantité entière positive qui diminue strictement à chaque appel (ici, le nombre de chiffres de n, ou n lui-même).

  • Correction : on la prouve par récurrence (forte si l'appel porte sur une valeur plus petite quelconque).

Exemple : l'exponentiation rapide.

def puissance(x, n):

    if n == 0:

        return 1

    if n % 2 == 0:

        y = puissance(x, n // 2)

        return y * y

    return x * puissance(x, n - 1)

Si $n$ est pair, on divise $n$ par 2 : $x^n = (x^{n/2})^2$. Si $n$ est impair, on se ramène à un exposant pair : $x^n = x \cdot x^{n-1}$. Après un appel impair vient forcément un appel pair, donc l'exposant est au moins divisé par 2 tous les deux appels. Le nombre d'appels est en $O(\log_2 n)$, contre $n$ multiplications pour la méthode naïve.

Entraîne-toi★★☆Pas commencé
a) Écrire une fonction récursive somme_chiffres(n) qui renvoie la somme des chiffres d'un entier $n \ge 0$. b) Dérouler somme_chiffres(4 276). c) Écrire une version…
  1. a) Écrire une fonction récursive somme_chiffres(n) qui renvoie la somme des chiffres d'un entier $n \ge 0$.
  1. b) Dérouler somme_chiffres(4 276).
  1. c) Écrire une version itérative et comparer les deux approches (paradigmes récursif et impératif).
Résoudre
Entraîne-toi★★★Pas commencé
On considère la fonction récursive : a) Dérouler puissance(2, 13) et compter les multiplications. b) Montrer par récurrence forte la correction : puissance(x, n) renvoie…

On considère la fonction récursive :

def puissance(x, n):

    if n == 0:

        return 1

    if n % 2 == 0:

        y = puissance(x, n // 2)

        return y * y

    return x * puissance(x, n - 1)
  1. a) Dérouler puissance(2, 13) et compter les multiplications.
  1. b) Montrer par récurrence forte la correction : puissance(x, n) renvoie $x^n$.
  1. c) Justifier la terminaison à l'aide d'une quantité strictement décroissante.
  1. d) Montrer que le nombre d'appels est en $O(\log_2 n)$ et comparer avec la méthode naïve.
Résoudre

Calculabilité : le problème de l'arrêt

Un programme est aussi une donnée : c'est un texte, que l'on peut stocker, copier, transmettre à une autre fonction. C'est ce que fait un compilateur, un interpréteur ou un antivirus.

On dit qu'un problème est décidable s'il existe un algorithme qui le résout pour toutes les entrées en un temps fini. Il est indécidable sinon. La calculabilité ne dépend pas du langage choisi : ce qu'on ne peut pas faire en Python, on ne peut pas le faire en un autre langage.

Conséquence : aucun outil ne peut vérifier automatiquement, pour tous les programmes, qu'ils s'arrêtent. Les outils de vérification existent, mais ils sont limités à certaines familles de programmes ou peuvent répondre « je ne sais pas ».

Revenons à la question du début : l'outil universel qui détecte les programmes qui bouclent n'existe donc pas. Cela ne dispense pas de tester, au contraire : c'est pourquoi on combine tests, assertions et preuves au cas par cas.

Entraîne-toi★★★Pas commencé
On veut démontrer que le problème de l'arrêt est indécidable. Supposons qu'il existe une fonction arret(prog, donnee) qui renvoie True si le programme prog appliqué à…

On veut démontrer que le problème de l'arrêt est indécidable. Supposons qu'il existe une fonction arret(prog, donnee) qui renvoie True si le programme prog appliqué à donnee s'arrête, False sinon.

  1. a) Expliquer pourquoi un programme peut être traité comme une donnée.
  1. b) On définit paradoxe(p) : si arret(p, p) renvoie True, la fonction boucle indéfiniment ; sinon elle renvoie 0. Que se passe-t-il pour paradoxe(paradoxe) ?
  1. c) En déduire la conclusion et expliquer ce que cela implique pour un outil de vérification automatique de programmes.
Résoudre

L'essentiel

  • Une classe définit des attributs et des méthodes ; un objet en est une instance. L'interface dit ce qu'on peut faire, l'implémentation dit comment.

  • Un module regroupe du code réutilisable ; son API et sa documentation suffisent pour l'utiliser.

  • On met au point un programme avec une docstring, des préconditions (assert) et un jeu de tests incluant les cas limites. On ne compare pas deux flottants avec ==.

  • Une fonction récursive a un cas de base et un appel sur un problème plus petit ; on justifie sa terminaison par une quantité qui décroît strictement.

  • Le problème de l'arrêt est indécidable : aucun algorithme ne peut décider, pour tout programme, s'il s'arrête.

Voir tous les exercices de cette partie