Structures de données et interfaces

Définir une interface, distinguer contrat et implémentation, puis comparer deux réalisations d’une file en Python.

Environ 40 minutes

  • Spécifier les opérations d’une structure
  • Distinguer interface et implémentation
  • Comparer deux implémentations sur un même scénario
Sommaire
  1. 1. Décrire le service avant sa représentation
  2. 2. Première implémentation : une liste Python
  3. 3. Seconde implémentation : deux piles
  4. 4. Au bac : vérifier une abstraction

Deux programmes peuvent-ils employer la même structure sans la représenter de la même manière ?

Un service reçoit des demandes et doit les traiter dans leur ordre d’arrivée. On peut mémoriser les demandes dans un tableau ou dans deux piles. Ce qui importe au programme utilisateur est d’obtenir la bonne demande au bon moment. Ce chapitre construit cette séparation entre service attendu et représentation choisie. Prérequis : fonctions Python, listes, boucles et tests simples. Nous utilisons une file comme exemple ; l’étude générale des piles et des files viendra ensuite.

1. Décrire le service avant sa représentation

Une structure de données organise des valeurs et propose des opérations. Une interface décrit les opérations accessibles, leurs paramètres, leurs résultats et leurs conditions d’utilisation. Elle ne décrit pas nécessairement les instructions qui les réalisent. Une implémentation choisit une représentation et des algorithmes pour respecter ce contrat. L’abstraction consiste à raisonner sur les propriétés utiles sans dépendre de tous les détails internes. Ici, le mot interface ne désigne pas un écran : il désigne la frontière entre le programme utilisateur et la structure qu’il utilise.

Pour notre file, creer() produit une file vide ; ajouter(f, x) place x à la fin ; est_vide(f) renvoie un booléen ; retirer(f) enlève et renvoie la valeur la plus ancienne. La précondition de retirer est que la file ne soit pas vide. La postcondition précise le résultat et le nouvel état : si l’état abstrait est [a, b, c], retirer renvoie a et laisse [b, c]. Ajouter modifie la structure et ne renvoie ici aucune valeur utile. Cette précision évite de confondre une modification avec le résultat d’une fonction.

Le premier élément à retirer est 4 ; l’indice 0 désigne ici le début de la file.

Exemple résolu

Spécifie le comportement d’une file après ajouter 4, ajouter 9, retirer, ajouter 2.

  1. Étape 1 : Pars de l’état vide. Les deux ajouts donnent l’ordre abstrait [4, 9].
  2. Étape 2 : Le retrait doit renvoyer 4 et laisser [9], puisque la première valeur ajoutée sort en premier.
  3. Étape 3 : Le dernier ajout donne [9, 2]. Le prochain retrait doit donc renvoyer 9.

Résultat : La première valeur produite est 4 et l’état final contient 9 puis 2. Cette description est indépendante de la représentation interne.

Piège à éviter : Donner seulement le contenu final oublie la valeur renvoyée par retirer, qui fait aussi partie du contrat.

À toi d'essayer

Une fonction ajoute une valeur puis renvoie la liste entière. Respecte-t-elle forcément une interface qui annonce un ajout sans résultat utile ?

Voir la réponse

Elle peut modifier correctement l’état, mais son résultat supplémentaire ne doit pas devenir une dépendance du programme utilisateur. Pour une substitution sûre, on s’en tient au contrat annoncé et on choisit une convention de retour cohérente entre les implémentations.

2. Première implémentation : une liste Python

Une réalisation directe place les valeurs dans une liste Python, de la plus ancienne à la plus récente. L’invariant de représentation est la propriété qui doit rester vraie après chaque opération : l’ordre de la liste correspond à l’ordre d’arrivée des valeurs encore présentes. Ajouter en fin conserve cet ordre. Retirer au début enlève bien la plus ancienne. Le code ci-dessous est volontairement simple. Les retraits dans les blocs Python indiquent l’appartenance aux fonctions et aux boucles ; recopie quatre espaces par niveau.

Exemple résolu

Implémente les quatre opérations avec une liste Python et vérifie le scénario précédent.

  1. Étape 1 : Définis les opérations.def creer():
        return []

    def ajouter(f, x):
        f.append(x)

    def retirer(f):
        return f.pop(0)

    def est_vide(f):
        return len(f) == 0
  2. Étape 2 : Crée f, ajoute 4 puis 9 et vérifie que retirer(f) vaut 4. Ajoute 2 : f contient maintenant [9, 2].
  3. Étape 3 : Teste aussi une création indépendante : deux appels de creer doivent produire deux listes distinctes.

Résultat : Le programme respecte le contrat tant que retirer est appelé sur une file non vide. La représentation de l’ordre est directement visible dans la liste.

Piège à éviter : Remplacer pop(0) par pop() change le résultat : la dernière valeur sortirait avant la première.

La correction fonctionnelle ne dit pas tout sur le coût. Retirer au début d’une liste Python oblige à décaler les références des éléments suivants. Pour n valeurs, le coût dépend donc de n. Un ajout en fin est généralement peu coûteux, avec des réallocations occasionnelles. On distingue ici l’ordre de grandeur du coût et une durée mesurée sur une machine. Quelques essais chronométrés ne prouvent pas une loi universelle ; ils peuvent seulement illustrer un modèle que l’on explique. Le choix de l’implémentation dépend du nombre d’opérations et des contraintes du programme.

À toi d'essayer

Un utilisateur lit directement f[0] pour consulter la première demande. Pourquoi cela fragilise-t-il son programme ?

Correction détaillée pas à pasDans ce chapitre : 2 corrections détaillées.

3. Seconde implémentation : deux piles

Une pile autorise l’ajout et le retrait à la même extrémité. Avec deux piles, on peut pourtant obtenir l’ordre d’une file. Les nouvelles valeurs entrent dans une pile entree. Une pile sortie fournit les valeurs à retirer. Quand sortie est vide, on y transfère toutes les valeurs de entree en les retirant une à une. Le transfert inverse leur ordre : la plus ancienne devient alors accessible au sommet de sortie. Tant que sortie contient des valeurs, on n’y transfère rien. Ce dernier point est essentiel pour conserver la priorité des anciennes demandes.

Le transfert a inversé l’ordre de stockage : 4 devient accessible avant 9 et 2.

Exemple résolu

Écris une seconde version avec les mêmes noms d’opérations.

  1. Étape 1 : Utilise un dictionnaire portant les deux piles.def creer():
        return {'entree': [], 'sortie': []}

    def ajouter(f, x):
        f['entree'].append(x)

    def retirer(f):
        if not f['sortie']:
            while f['entree']:
                f['sortie'].append(f['entree'].pop())
        return f['sortie'].pop()

    def est_vide(f):
        return not f['entree'] and not f['sortie']
  2. Étape 2 : Après les ajouts de 4 et 9, entree contient [4, 9] et sortie est vide. Le premier retrait transfère 9 puis 4 avant de retirer 4.
  3. Étape 3 : Après l’ajout de 2, entree contient [2] et sortie contient [9]. Le retrait suivant doit utiliser sortie sans transférer 2.

Résultat : Les valeurs renvoyées sont les mêmes qu’avec la liste : 4, puis 9, puis 2 si l’on vide la file. La représentation interne est différente, l’ordre abstrait est conservé.

Piège à éviter : Transférer entree à chaque retrait placerait parfois une demande récente devant une demande ancienne.

L’état abstrait correspond aux valeurs de sortie lues depuis son sommet, suivies des valeurs de entree lues dans leur ordre d’ajout. Cette relation permet de vérifier la correction au-delà d’un exemple. Ajouter prolonge la seconde partie. Retirer enlève la tête de la première partie ; si elle est vide, le transfert recrée cette partie en conservant l’ordre abstrait. Chaque valeur est ajoutée à entree, transférée au plus une fois, puis retirée de sortie. Une série d’opérations évite ainsi les décalages répétés du tableau, même si un retrait particulier peut déclencher un transfert long.

À toi d'essayer

Les deux piles sont non vides. La pile de sortie contient encore la plus ancienne demande. Un programme transfère pourtant les nouvelles demandes avant de retirer. Construis un contre-exemple.

Correction détaillée pas à pasDans ce chapitre : 2 corrections détaillées.

4. Au bac : vérifier une abstraction

Au bac : comparer deux implémentations

  1. Écris d’abord l’interface : paramètres, résultats, modification de l’état et préconditions.
  2. Trace le même scénario dans les deux représentations et compare les valeurs observables, pas l’égalité des objets internes.
  3. Teste les frontières : structure vide, un seul élément, ajouts et retraits alternés, créations indépendantes.
  4. Explique un invariant qui relie représentation et état abstrait. Un test réussi ne remplace pas cette justification générale.
  5. Compare ensuite les coûts en fonction des opérations et de la taille des données, sans confondre correction et efficacité.

Pour tester une interface, le programme de test doit passer par ses opérations. S’il lit les clés entree ou sortie, il vérifie des détails de la seconde implémentation plutôt que la compatibilité du service. On peut garder en parallèle une liste de référence, calculer l’ordre attendu et comparer chaque retrait. Pour une file vide, on vérifie est_vide avant retirer : l’absence de résultat sur ce cas vient ici de la précondition. Un sujet peut imposer une exception ou une autre convention ; c’est alors cette convention qu’il faut respecter.

Vocabulaire

Interface :
Contrat des opérations utilisables et de leurs effets observables.
Implémentation :
Réalisation concrète d’un contrat avec une représentation et des algorithmes.
Précondition :
Condition qui doit être vraie avant une opération.
Invariant :
Propriété conservée par les opérations autorisées.

Deux réalisations, un service

RéalisationRetrait
ListeSuppression au début et décalages.
Deux pilesSommet de sortie ; transfert seulement quand cette pile est vide.

S’entraîner : ouvre l’espace Exercices du site.

À retenir

  • Une interface spécifie les opérations sans imposer tous leurs détails internes.
  • Deux implémentations peuvent avoir des représentations différentes et les mêmes effets observables.
  • La correction d’une opération dépend aussi de ses préconditions.
  • Un invariant relie les états internes aux propriétés de la structure abstraite.

Fiche de révision

Pour une file, note séparément ce qui est renvoyé et ce qui reste mémorisé.

Le tableau et les deux piles se comparent à travers le même programme utilisateur.

Une optimisation doit conserver le service attendu : un retrait plus rapide mais dans le mauvais ordre est incorrect.

Un contre-exemple avec opérations alternées détecte des erreurs qu’un simple remplissage puis vidage peut manquer.

Questions fréquentes

Une interface impose-t-elle une classe ?

Non. Des fonctions, une classe ou une bibliothèque peuvent exposer un contrat.

Les deux représentations doivent-elles être égales ?

Non. Les résultats et effets annoncés doivent être compatibles ; leur organisation interne peut différer.

Et maintenant ?

Tu as lu le cours. L’étape suivante tient en une action.

Relis le coursReprends l’essentiel avant de passer à la suite.

Prolonge l’expérience !

Continue à explorer ce chapitre à travers de nouvelles activités, des défis et des expériences interactives. Avec Premium, l’apprentissage continue !

Ici, tu débloques aussi 2 corrections détaillées.

Parent ? Suivez les progrès de votre enfant.

Tous les cours de NSI