Exercice 1 : Une structure de données est un contrat
Avant toute implémentation, une structure se définit par son INTERFACE : la liste des opérations autorisées et leur coût. Deux implémentations différentes du même contrat sont interchangeables pour l'utilisateur.
class Pile:
def __init__(self):
self.contenu = []
def est_vide(self):
return len(self.contenu) == 0
def empiler(self, x):
self.contenu.append(x)
def depiler(self):
return self.contenu.pop()
def sommet(self):
return self.contenu[-1]- a) Donnez les quatre opérations de l'interface d'une pile et dites, pour chacune, ce qu'elle attend et ce qu'elle renvoie. Que signifie l'abréviation LIFO ?
- b) On empile 3, puis 7, puis 2, puis on dépile deux fois, puis on empile 5. Donnez le contenu de la pile, du fond vers le sommet, et les valeurs renvoyées par les dépilements.
- c) La méthode depiler ne vérifie pas que la pile est non vide. Décrivez ce qui se produit sur une pile vide et proposez deux conceptions différentes pour traiter ce cas.
- d) On remplace l'implémentation par une liste chaînée, sans changer les noms des méthodes. Le programme qui utilise la pile doit-il être modifié ? Formulez le principe en jeu.
- e) Un élève écrit self.contenu.pop(0) au lieu de pop(). L'objet obtenu respecte-t-il encore le contrat d'une pile ? Quel est le second problème, plus insidieux ?
Voir la correction
Réponses
- a) Pile : créer, tester, empiler, dépiler ; LIFO
- b) Renvoie 2 puis 7, reste 3, 5
- c) Pile vide : exception explicite ou contrat
- d) Encapsulation : programmer contre l'interface
- e) pop(0) : une file, et un coût linéaire
a) Les quatre opérations. Créer une pile vide, qui n'attend rien et renvoie la structure. Tester si elle est vide, qui n'attend rien et renvoie un booléen. Empiler, qui attend une valeur et ne renvoie rien mais modifie la pile. Dépiler, qui n'attend rien, retire l'élément du sommet et le renvoie. On y ajoute souvent une cinquième opération, lire le sommet sans le retirer. LIFO signifie « dernier entré, premier sorti » : l'élément rendu par un dépilement est toujours le plus récemment empilé.
b) Après avoir empilé 3, 7 puis 2, la pile contient 3, 7, 2 du fond vers le sommet. Le premier dépilement renvoie 2, le second renvoie 7 : la pile ne contient plus que 3. On empile ensuite 5, donc la pile contient 3, 5 du fond vers le sommet. Les valeurs renvoyées sont donc 2 puis 7, dans cet ordre, ce qui est bien l'inverse de leur ordre d'arrivée.
c) Sur une pile vide, l'appel à pop sur une liste vide lève une exception IndexError, dont le message parle d'une liste alors que l'utilisateur croyait manipuler une pile : la fuite d'implémentation est déjà un défaut en soi. Deux conceptions possibles. La première est DÉFENSIVE : depiler teste est_vide et lève une exception explicite, du genre « dépilement d'une pile vide », ce qui interrompt le programme au bon endroit avec le bon message. La seconde est CONTRACTUELLE : on documente que dépiler une pile vide est interdit, et c'est à l'appelant de tester avant. La première est plus sûre, la seconde est plus rapide ; le choix se justifie, il ne se devine pas.
d) Non, le programme utilisateur n'a pas à être modifié, à condition qu'il n'ait jamais accédé à l'attribut contenu. C'est le principe d'ENCAPSULATION, ou de séparation entre interface et implémentation : l'utilisateur programme contre le contrat, jamais contre la réalisation. Ce principe est ce qui permet d'optimiser une structure sans réécrire les programmes qui s'en servent, et c'est aussi pourquoi accéder directement à contenu depuis l'extérieur est une faute, même quand le langage l'autorise.
e) Avec pop(0), on retire l'élément d'INDICE 0, c'est-à-dire le plus anciennement empilé : l'objet obtenu se comporte comme une file, pas comme une pile. Il ne respecte donc plus le contrat. Le second problème, plus insidieux, est le COÛT : retirer le premier élément d'une liste Python oblige à décaler tous les autres, donc l'opération est linéaire en la taille au lieu d'être constante. Une boucle qui vide la structure passe ainsi d'un coût linéaire à un coût quadratique, sans qu'aucun test fonctionnel ne le signale, puisque le résultat, lui, serait correct si l'on voulait une file.