Cette page comporte deux parties, qui modélisent toutes les deux le même historique de navigateur. La partie 1 (sections 1 à 8) utilise un dictionnaire manipulé par trois fonctions. La partie 2 (sections 9 et 10) reprend exactement les mêmes idées sous la forme d'une classe Historique, en programmation orientée objet. Chaque partie suit le même plan : vocabulaire ou principe, exemple concret, code, démonstration ou trace détaillée dans un tableau, puis tests.
src/historique_navigateur_deque.py (partie 1) et le fichier src/historique_navigateur_poo.py (partie 2), dans le sous-dossier src à côté de cette page, contiennent tout le code et tous les tests présentés ici. Après avoir téléchargé l'un de ces fichiers, on l'exécute dans un terminal, dans le dossier où il a été enregistré, avec python3 nom_du_fichier.py (Python doit être installé sur l'ordinateur). Chaque fichier affiche « OK » pour chaque test s'il n'y a pas d'erreur.
Chaque mot ci-dessous est utilisé exactement dans ce sens dans toute la page.
| Mot | Définition précise |
|---|---|
| Historique | Un dictionnaire à trois clés qui représente l'historique de navigation d'un onglet : "back", "current" et "forward". |
back | Une pile (un deque) contenant les pages visitées avant la page courante, de la plus ancienne (à gauche) à la plus récente (à droite, juste avant la page courante). |
current | Une chaîne de caractères : l'URL de la page actuellement affichée. |
forward | Une pile (un deque) contenant les pages qu'on a quittées en reculant, de la plus proche de la page courante (à gauche) à la plus lointaine (à droite). |
deque | « Double-ended queue » : une structure de la bibliothèque collections qui permet d'ajouter et de retirer un élément efficacement à ses deux extrémités (contrairement à une liste Python classique). |
append(x) / pop() | Ajouter x à l'extrémité droite d'un deque ; retirer et renvoyer l'élément à l'extrémité droite. |
appendleft(x) / popleft() | Ajouter x à l'extrémité gauche d'un deque ; retirer et renvoyer l'élément à l'extrémité gauche. |
reculer | La fonction qui affiche la page précédente : celle qui est au sommet de back. |
avancer | La fonction qui affiche la page suivante : celle qui est au sommet de forward. |
nouvelle_url | La fonction qui enregistre la visite d'une page qu'on n'avait pas déjà dans l'historique. |
On reprend l'historique donné dans l'énoncé :
from collections import deque
historique = {"back": deque(['url_D3', 'url_C1', 'url_A8', 'url_A5']),
"current": 'url_F',
"forward": deque(['url_Z', 'url_alpha1'])}
Ce dictionnaire se lit ainsi : la page actuellement affichée est url_F. Avant d'arriver sur url_F, l'utilisateur a visité, dans l'ordre, url_D3, puis url_C1, puis url_A8, puis url_A5 — url_A5 est donc la page juste avant url_F, et c'est elle qui sera affichée si on clique une fois sur « précédent ». L'utilisateur avait aussi déjà commencé à avancer vers url_Z puis url_alpha1 avant de revenir en arrière : url_Z est donc la page juste après url_F, et c'est elle qui sera affichée si on clique sur « suivant ».
back, le sommet (la page la plus proche de current) est le dernier élément du deque : celui que renvoie back.pop(). Dans forward, le sommet (la page la plus proche de current) est le premier élément du deque : celui que renvoie forward.popleft().
deque plutôt qu'une liste Python ordinaire ?La fiche Les files a déjà montré que retirer le premier élément d'une liste Python avec l.pop(0) coûte O(n), car Python doit décaler tous les éléments restants d'une case. Le même problème se pose ici pour forward :
forward (avec appendleft).forward (avec popleft).Avec une liste Python classique, ces deux opérations s'écriraient l.insert(0, x) et l.pop(0), toutes deux en O(n). Avec collections.deque, appendleft(x) et popleft() sont toutes les deux en O(1), tout comme append(x) et pop() à l'autre extrémité. Comme back et forward jouent des rôles symétriques — une page qui sort de l'un entre dans l'autre — on utilise deque pour les deux, par cohérence.
| Opération | Code Python | Extrémité concernée | Complexité |
|---|---|---|---|
Créer un deque vide | deque() | — | — |
| Ajouter à droite | d.append(x) | droite | O(1) |
| Retirer à droite | d.pop() | droite | O(1) |
| Ajouter à gauche | d.appendleft(x) | gauche | O(1) |
| Retirer à gauche | d.popleft() | gauche | O(1) |
| Vider entièrement | d.clear() | — | O(1) |
| Tester si vide | len(d) == 0 | — | O(1) |
reculerforward (en tête), et faire passer le sommet de back dans current — sauf si back est déjà vide, auquel cas on ne peut pas reculer davantage et l'historique ne change pas.
back est vide, il n'y a pas de page précédente : on renvoie l'historique tel quel, sans le modifier.forward (avec appendleft), car on pourra vouloir y revenir avec « avancer ».back (avec pop), et cette page devient la nouvelle page courante.def reculer(historique):
"""Retourne l'historique après un clic sur 'précédent'.
Si back est vide, l'historique est renvoyé inchangé."""
if len(historique["back"]) == 0:
return historique
historique["forward"].appendleft(historique["current"])
historique["current"] = historique["back"].pop()
return historique
avancerback, et faire passer le sommet de forward dans current — sauf si forward est déjà vide.
forward est vide, il n'y a pas de page suivante : on renvoie l'historique tel quel, sans le modifier.back (avec append).forward (avec popleft), et cette page devient la nouvelle page courante.def avancer(historique):
"""Retourne l'historique après un clic sur 'suivant'.
Si forward est vide, l'historique est renvoyé inchangé."""
if len(historique["forward"]) == 0:
return historique
historique["back"].append(historique["current"])
historique["current"] = historique["forward"].popleft()
return historique
nouvelle_urlback, fait de cette nouvelle page la page courante, et vide entièrement forward : on ne peut plus « avancer » vers des pages qui n'existent plus dans ce nouveau chemin de navigation.
back (avec append).forward (avec clear).def nouvelle_url(historique, url):
"""Retourne l'historique après la visite d'une nouvelle page url.
forward est entièrement vidé."""
historique["back"].append(historique["current"])
historique["current"] = url
historique["forward"].clear()
return historique
On part de l'historique de l'exemple, et on applique dans l'ordre la première séquence de test de l'énoncé :
reculer, reculer, nouvelle_url, reculer, reculer, avancer
nouvelle_url est 'url_B2'.reculer(historique) reculer(historique) nouvelle_url(historique, 'url_B2') reculer(historique) reculer(historique) avancer(historique)
Ces tableaux se lisent comme la démonstration ci-dessus, mais sans animation : à chaque ligne, on note l'état complet de l'historique juste après l'instruction.
avancer, avancer, reculer, nouvelle_url, reculer (URL choisie pour nouvelle_url : 'url_G7')
| Instruction | back | current | forward |
|---|---|---|---|
| État initial | [D3, C1, A8, A5] | F | [Z, alpha1] |
avancer(historique) | [D3, C1, A8, A5, F] | Z | [alpha1] |
avancer(historique) | [D3, C1, A8, A5, F, Z] | alpha1 | [] |
reculer(historique) | [D3, C1, A8, A5, F] | Z | [alpha1] |
nouvelle_url(historique, 'url_G7') | [D3, C1, A8, A5, F, Z] | G7 | [] |
reculer(historique) | [D3, C1, A8, A5, F] | Z | [G7] |
(Les URL sont notées sans le préfixe url_ pour que le tableau tienne sur la page ; par exemple « F » désigne 'url_F'.)
back devient vide : tester reculerOn repart de l'historique de l'exemple, et on appelle reculer cinq fois de suite. back ne contient que 4 éléments : il devient vide après le 4e appel, et le 5e appel doit vérifier que rien ne change.
| Instruction | back | current | forward |
|---|---|---|---|
| État initial | [D3, C1, A8, A5] | F | [Z, alpha1] |
reculer(historique) (1) | [D3, C1, A8] | A5 | [F, Z, alpha1] |
reculer(historique) (2) | [D3, C1] | A8 | [A5, F, Z, alpha1] |
reculer(historique) (3) | [D3] | C1 | [A8, A5, F, Z, alpha1] |
reculer(historique) (4) | [] | D3 | [C1, A8, A5, F, Z, alpha1] |
reculer(historique) (5) | [] (toujours vide) | D3 (inchangé) | [C1, A8, A5, F, Z, alpha1] (inchangé) |
len(historique["back"]) == 0 est vrai : la fonction renvoie l'historique immédiatement, sans toucher à current ni à forward. Sans ce test, historique["back"].pop() lèverait une erreur (IndexError), car on ne peut pas retirer un élément d'un deque vide.
forward devient vide : tester avancerOn repart de l'historique de l'exemple, et on appelle avancer trois fois de suite. forward ne contient que 2 éléments : il devient vide après le 2e appel, et le 3e appel doit vérifier que rien ne change.
| Instruction | back | current | forward |
|---|---|---|---|
| État initial | [D3, C1, A8, A5] | F | [Z, alpha1] |
avancer(historique) (1) | [D3, C1, A8, A5, F] | Z | [alpha1] |
avancer(historique) (2) | [D3, C1, A8, A5, F, Z] | alpha1 | [] |
avancer(historique) (3) | [D3, C1, A8, A5, F, Z] (inchangé) | alpha1 (inchangé) | [] (toujours vide) |
len(historique["forward"]) == 0 est vrai : la fonction renvoie l'historique immédiatement, sans toucher à current ni à back. C'est exactement le même raisonnement que pour reculer, appliqué à l'autre pile.
assert)Voici comment on peut écrire, en Python, les tests des deux sections précédentes : chaque assert vérifie une valeur du tableau correspondant.
def etat_initial():
"""Reconstruit l'historique de l'exemple, à chaque appel."""
return {"back": deque(['url_D3', 'url_C1', 'url_A8', 'url_A5']),
"current": 'url_F',
"forward": deque(['url_Z', 'url_alpha1'])}
def test_sequence_1():
"""reculer, reculer, nouvelle_url, reculer, reculer, avancer"""
h = etat_initial()
reculer(h)
reculer(h)
nouvelle_url(h, 'url_B2')
reculer(h)
reculer(h)
avancer(h)
assert list(h["back"]) == ['url_D3', 'url_C1']
assert h["current"] == 'url_A8'
assert list(h["forward"]) == ['url_B2']
print("Séquence 1 : OK")
def test_sequence_2():
"""avancer, avancer, reculer, nouvelle_url, reculer"""
h = etat_initial()
avancer(h)
avancer(h)
reculer(h)
nouvelle_url(h, 'url_G7')
reculer(h)
assert list(h["back"]) == ['url_D3', 'url_C1', 'url_A8', 'url_A5', 'url_F']
assert h["current"] == 'url_Z'
assert list(h["forward"]) == ['url_G7']
print("Séquence 2 : OK")
def test_reculer_quand_back_devient_vide():
h = etat_initial()
for _ in range(4):
reculer(h)
assert len(h["back"]) == 0
assert h["current"] == 'url_D3'
# Un 5e appel ne doit rien changer
reculer(h)
assert len(h["back"]) == 0
assert h["current"] == 'url_D3'
print("reculer avec back vide : OK")
def test_avancer_quand_forward_devient_vide():
h = etat_initial()
for _ in range(2):
avancer(h)
assert len(h["forward"]) == 0
assert h["current"] == 'url_alpha1'
# Un 3e appel ne doit rien changer
avancer(h)
assert len(h["forward"]) == 0
assert h["current"] == 'url_alpha1'
print("avancer avec forward vide : OK")
reculer, avancer, nouvelle_url, et les quatre tests ci-dessus.
reculer fait un test de longueur, un appendleft et un pop : trois opérations en O(1), donc reculer est en O(1).avancer fait un test de longueur, un append et un popleft : trois opérations en O(1), donc avancer est aussi en O(1).nouvelle_url fait un append et un clear : append est en O(1), et clear est également en O(1) sur un deque (contrairement à une liste, où vider en boucle coûterait O(n)).deque : appendleft et popleft deviendraient insert(0, x) et pop(0), tous les deux en O(n).Historique (programmation orientée objet)On reprend exactement les mêmes idées que dans la partie 1, mais sous forme d'une classe : back, current et forward deviennent des attributs d'un objet, et reculer, avancer, nouvelle_url deviennent des méthodes de cette classe. Le vocabulaire de la programmation orientée objet (classe, attribut, méthode, constructeur, self, __repr__) est détaillé dans la fiche Interfaces, implémentation bas niveau et POO.
Historique regroupe back, current et forward dans ses propres attributs (self.back, self.current, self.forward) ; ses méthodes reculer, avancer et nouvelle_url modifient ces attributs directement, sans avoir besoin de recevoir l'historique en argument ni de le renvoyer.
| Partie 1 (fonctions sur un dictionnaire) | Partie 2 (méthodes sur un objet) |
|---|---|
historique = {"back": ..., "current": ..., "forward": ...} | hist = Historique(url_actuelle, back=..., forward=...) |
historique["back"] | hist.back |
historique["current"] | hist.current |
historique = reculer(historique) | hist.reculer() |
reculer, avancer et nouvelle_url sont maintenant des méthodes qui modifient self directement et ne renvoient rien (elles renvoient None). Il ne faut donc jamais écrire hist = hist.reculer() : cela remplacerait hist par None. Il suffit d'appeler hist.reculer(), sans réaffectation.
__init__ : reçoit l'URL de départ, et éventuellement des piles back et forward déjà remplies (vides par défaut).reculer : si self.back est vide, ne fait rien ; sinon, ajoute self.current en tête de self.forward, puis remplace self.current par le sommet de self.back.avancer : le symétrique exact, entre self.back et self.forward.nouvelle_url : ajoute self.current en queue de self.back, remplace self.current par la nouvelle URL, puis vide self.forward.__repr__ : renvoie une chaîne de caractères lisible, affichée par print(hist).class Historique:
"""Historique de navigation d'un onglet."""
def __init__(self, url_actuelle, back=None, forward=None):
self.back = deque(back) if back is not None else deque()
self.current = url_actuelle
self.forward = deque(forward) if forward is not None else deque()
def reculer(self):
"""Modifie self : passe à la page précédente (sommet de back).
Ne fait rien si back est vide."""
if len(self.back) == 0:
return
self.forward.appendleft(self.current)
self.current = self.back.pop()
def avancer(self):
"""Modifie self : passe à la page suivante (sommet de forward).
Ne fait rien si forward est vide."""
if len(self.forward) == 0:
return
self.back.append(self.current)
self.current = self.forward.popleft()
def nouvelle_url(self, url):
"""Modifie self : visite une nouvelle page url, et vide forward."""
self.back.append(self.current)
self.current = url
self.forward.clear()
def __repr__(self):
return f"Historique(back={list(self.back)}, current={self.current!r}, forward={list(self.forward)})"
back=None dans le constructeur, plutôt que back=deque() ?__init__ est défini — pas à chaque appel. Si on écrivait def __init__(self, url_actuelle, back=deque(), forward=deque()):, ce même deque() serait réutilisé par tous les objets créés sans argument back ou forward explicite : modifier l'historique d'un onglet modifierait alors, par erreur, tous les autres. En utilisant None comme valeur par défaut, puis en créant le deque à l'intérieur du constructeur, chaque objet reçoit sa propre pile.
Remarque sur la complexité : reculer, avancer et nouvelle_url utilisent exactement les mêmes opérations sur deque que dans la partie 1 (section 8) ; elles sont donc, elles aussi, toutes les trois en O(1).
Chaque onglet ouvert dans le navigateur a son propre historique de navigation, totalement indépendant des autres. On crée donc un objet Historique différent pour chaque onglet : hist_A, hist_B et hist_C.
| Onglet | Contexte | back | current | forward |
|---|---|---|---|---|
hist_A | un moteur de recherche | [url_moteur, url_resultats1] | url_article1 | [] |
hist_B | une messagerie | [] | url_boite_reception | [url_message_supprime] |
hist_C | un site de vidéos | [url_accueil_C] | url_video1 | [url_video2, url_video3] |
hist_A = Historique('url_article1', back=['url_moteur', 'url_resultats1'], forward=[])
hist_B = Historique('url_boite_reception', back=[], forward=['url_message_supprime'])
hist_C = Historique('url_video1', back=['url_accueil_C'], forward=['url_video2', 'url_video3'])
On applique, dans cet ordre, sept instructions réparties sur les trois onglets, pour vérifier qu'agir sur l'un ne modifie jamais les deux autres :
hist_A.reculer()hist_B.avancer()hist_C.nouvelle_url('url_commentaires')hist_A.reculer()hist_B.nouvelle_url('url_nouveau_message')hist_C.reculer()hist_A.avancer()À chaque ligne, seul l'onglet concerné par l'instruction change d'état ; les deux autres restent identiques à la ligne précédente — c'est exactement ce qu'on attend de trois objets indépendants.
| Étape | hist_A | hist_B | hist_C |
|---|---|---|---|
| État initial | back=[moteur, resultats1], cur=article1, fwd=[] | back=[], cur=boite_reception, fwd=[message_supprime] | back=[accueil_C], cur=video1, fwd=[video2, video3] |
hist_A.reculer() | back=[moteur], cur=resultats1, fwd=[article1] | inchangé | inchangé |
hist_B.avancer() | inchangé | back=[boite_reception], cur=message_supprime, fwd=[] | inchangé |
hist_C.nouvelle_url('url_commentaires') | inchangé | inchangé | back=[accueil_C, video1], cur=commentaires, fwd=[] |
hist_A.reculer() | back=[], cur=moteur, fwd=[resultats1, article1] | inchangé | inchangé |
hist_B.nouvelle_url('url_nouveau_message') | inchangé | back=[boite_reception, message_supprime], cur=nouveau_message, fwd=[] | inchangé |
hist_C.reculer() | inchangé | inchangé | back=[accueil_C], cur=video1, fwd=[commentaires] |
hist_A.avancer() | back=[moteur], cur=resultats1, fwd=[article1] | inchangé | inchangé |
(Comme dans les tableaux de la partie 1, les URL sont abrégées : « moteur » désigne 'url_moteur', « commentaires » désigne 'url_commentaires', etc.)
__repr__>>> print(hist_A) Historique(back=['url_moteur'], current='url_resultats1', forward=['url_article1']) >>> print(hist_B) Historique(back=['url_boite_reception', 'url_message_supprime'], current='url_nouveau_message', forward=[]) >>> print(hist_C) Historique(back=['url_accueil_C'], current='url_video1', forward=['url_commentaires'])
print(hist_A) affiche-t-il quelque chose de lisible ?__repr__, print(hist_A) afficherait quelque chose comme <__main__.Historique object at 0x...>, une adresse mémoire sans intérêt. En définissant __repr__, on choisit soi-même ce que Python affiche pour un objet Historique : ici, ses trois attributs, dans un format facile à relire.
Historique, la création de hist_A, hist_B, hist_C, le jeu d'instructions ci-dessus, et un test d'indépendance des objets.
back et forward, encadrant une page courante, current — sous forme d'un dictionnaire (partie 1) ou des attributs d'un objet (partie 2).collections.deque plutôt qu'une liste Python car reculer et avancer ont besoin d'ajouter et de retirer des éléments à l'extrémité gauche d'une des deux piles ; avec deque, ces opérations sont en O(1) grâce à appendleft et popleft.forward : on ne peut plus « avancer » vers un chemin de navigation qui n'existe plus.len(...) == 0) : sans ce test, pop() ou popleft() sur un deque vide lèverait une erreur.self directement et ne renvoient rien : on écrit hist.reculer(), jamais hist = hist.reculer().Historique est indépendant des autres, à condition d'initialiser back et forward avec None comme valeur par défaut dans le constructeur, plutôt qu'avec un deque() partagé entre toutes les instances.