Comment utiliser cette page

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.

Utilisation de la démonstration
La démonstration avance uniquement quand vous cliquez sur « Suivant » ou « Précédent » : rien ne bouge tout seul. Un bouton « Lecture automatique » est disponible si vous préférez laisser les étapes défiler seules, à une vitesse que vous choisissez.
📄 Le code de cette page, en fichiers téléchargeables
Le fichier 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.
PARTIE 1 — Version fonctionnelleun dictionnaire, manipulé par trois fonctions

1. Vocabulaire à connaître avant de commencer

Chaque mot ci-dessous est utilisé exactement dans ce sens dans toute la page.

MotDéfinition précise
HistoriqueUn dictionnaire à trois clés qui représente l'historique de navigation d'un onglet : "back", "current" et "forward".
backUne 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).
currentUne chaîne de caractères : l'URL de la page actuellement affichée.
forwardUne 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.
reculerLa fonction qui affiche la page précédente : celle qui est au sommet de back.
avancerLa fonction qui affiche la page suivante : celle qui est au sommet de forward.
nouvelle_urlLa fonction qui enregistre la visite d'une page qu'on n'avait pas déjà dans l'historique.

2. L'exemple utilisé dans cette page

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 ».

Où se trouve le sommet de chaque pile ?
Dans 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().

3. Pourquoi 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 :

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érationCode PythonExtrémité concernéeComplexité
Créer un deque videdeque()——
Ajouter à droited.append(x)droiteO(1)
Retirer à droited.pop()droiteO(1)
Ajouter à gauched.appendleft(x)gaucheO(1)
Retirer à gauched.popleft()gaucheO(1)
Vider entièrementd.clear()—O(1)
Tester si videlen(d) == 0—O(1)

4. Les trois fonctions du mini-projet

4.1 — reculer

Principe, en une phrase
Reculer, c'est faire passer la page courante dans forward (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.
  1. Si back est vide, il n'y a pas de page précédente : on renvoie l'historique tel quel, sans le modifier.
  2. Sinon, on ajoute la page courante en tête de forward (avec appendleft), car on pourra vouloir y revenir avec « avancer ».
  3. On retire le sommet de back (avec pop), et cette page devient la nouvelle page courante.
  4. On renvoie l'historique mis à jour.
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

4.2 — avancer

Principe, en une phrase
Avancer, c'est le symétrique exact de reculer : faire passer la page courante dans back, et faire passer le sommet de forward dans current — sauf si forward est déjà vide.
  1. Si forward est vide, il n'y a pas de page suivante : on renvoie l'historique tel quel, sans le modifier.
  2. Sinon, on ajoute la page courante en queue de back (avec append).
  3. On retire le sommet de forward (avec popleft), et cette page devient la nouvelle page courante.
  4. On renvoie l'historique mis à jour.
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

4.3 — nouvelle_url

Principe, en une phrase
Visiter une nouvelle page ajoute la page courante en queue de back, 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.
  1. On ajoute la page courante en queue de back (avec append).
  2. La nouvelle URL devient la page courante.
  3. On vide complètement forward (avec clear).
  4. On renvoie l'historique mis à jour.
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

5. Démonstration interactive

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

élément qui va être retiré (sommet, sur le point de bouger) élément qui vient d'être ajouté page courante
back (le plus ancien à gauche) — page courante — forward (la plus proche à droite)
back
◀
current
▶
forward
Étape 1 / 1
La ligne de code correspondant à l'étape en cours est surlignée ci-dessous. L'URL choisie pour nouvelle_url est 'url_B2'.
reculer(historique)
reculer(historique)
nouvelle_url(historique, 'url_B2')
reculer(historique)
reculer(historique)
avancer(historique)

6. Autres traces à dérouler (tableaux détaillés)

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.

6.1 — Deuxième séquence de l'énoncé

avancer, avancer, reculer, nouvelle_url, reculer (URL choisie pour nouvelle_url : 'url_G7')

Instructionbackcurrentforward
É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'.)

6.2 — Quand back devient vide : tester reculer

On 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.

Instructionbackcurrentforward
É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é)
Ce que montre ce test
Au 5e appel, 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.

6.3 — Quand forward devient vide : tester avancer

On 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.

Instructionbackcurrentforward
É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)
Ce que montre ce test
Au 3e appel, 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.

7. Tests en Python (avec 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")
📄 Télécharger ce code
src/historique_navigateur_deque.py — contient reculer, avancer, nouvelle_url, et les quatre tests ci-dessus.

8. Complexité des trois fonctions

PARTIE 2 — Version orientée objetla classe Historique

9. La classe 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.

9.1 — Ce qui change par rapport à la version fonctionnelle

Principe, en une phrase
Un objet 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()
Attention à ne pas réaffecter le résultat
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.

9.2 — Code de la classe

  1. Constructeur __init__ : reçoit l'URL de départ, et éventuellement des piles back et forward déjà remplies (vides par défaut).
  2. 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.
  3. avancer : le symétrique exact, entre self.back et self.forward.
  4. nouvelle_url : ajoute self.current en queue de self.back, remplace self.current par la nouvelle URL, puis vide self.forward.
  5. __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)})"
Pourquoi back=None dans le constructeur, plutôt que back=deque() ?
En Python, la valeur par défaut d'un paramètre n'est calculée qu'une seule fois, au moment où __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).

10. Trois onglets, trois objets indépendants

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.

10.1 — Création des trois objets

OngletContextebackcurrentforward
hist_Aun moteur de recherche[url_moteur, url_resultats1]url_article1[]
hist_Bune messagerie[]url_boite_reception[url_message_supprime]
hist_Cun 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'])

10.2 — Jeu d'instructions de test

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 :

  1. hist_A.reculer()
  2. hist_B.avancer()
  3. hist_C.nouvelle_url('url_commentaires')
  4. hist_A.reculer()
  5. hist_B.nouvelle_url('url_nouveau_message')
  6. hist_C.reculer()
  7. hist_A.avancer()

10.3 — Trace : état des trois objets à chaque étape

À 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.

Étapehist_Ahist_Bhist_C
État initialback=[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.)

10.4 — Affichage final, avec __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'])
Pourquoi print(hist_A) affiche-t-il quelque chose de lisible ?
Sans méthode __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.
📄 Télécharger ce code
src/historique_navigateur_poo.py — contient la classe 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.

11. Points clés à retenir

Revoir une fiche en détail ? → Piles · Files · POO · Retour à l'accueil