T1.3 Dictionnaires⚓︎
1. Retour sur les dictionnaires⚓︎
Au préalable, on peut relire le chapitre de Première.
En Python, un dictionnaire (<class 'dict'>
) est une structure de données native qui permet de représenter le type abstrait de données appelé tableau associatif ou p-uplet nommé qui correspond à un tableau dont les valeurs sont indexées par des clés, et dont l'interface contient généralement les opérations:
- ajout : association d'une nouvelle valeur à une nouvelle clé ;
- modification : association d'une nouvelle valeur à une ancienne clé ;
- suppression : suppression d'une clé (et donc de la valeur);
- recherche : détermination de la valeur associée à une clé, si elle existe.
Puisqu'un tableau associatif est un ensemble de couples clé:valeur, on pourrait penser l'implémenter simplement avec une simple liste de couples. Mais ainsi la recherche d'une clé (pour modification, suppression ou recherche) ne correspondrait plus à l'indice du couple dans la liste. Il faudrait donc pour chaque clé faire une recherche dans la liste.
list
vs. dict
: comparaison des temps d'exécution d'une recherche
On se propose de mesurer le temps d'exécution d'une recherche d'un élément dans une liste, et dans un dictionnaire. Pour cela on se place dans «le pire des cas», c'est-à-dire où la valeur recherchée n'appartient pas à la structure étudiée (liste ou dictionnaire).
On crée donc deux fonctions:
timeit_lst
: construit une liste d'entiers de tailleN
donnée en paramètre;timeit_dct
: construit un dictionnaire de tailleN
donnée en paramètre dont les clés sont des entiers, puis qui y recherche ensuite lestr
"a"
... un certain nombre de fois (nbloop
) en mesurant le temps d'exécution à l'aide de la fonctiontime
du moduletime
pour obtenir une moyenne.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
- Exécuter chacune de ces deux fonctions pour les valeurs de
N
: 10, 100, 1000, 10000. Noter un ordre de grandeur des temps moyens obtenus. - Comment augmente le temps moyen d'exécution de recherche dans une liste lorsque
N
est multiplié par 10? par 100? par 1000? En déduire la complexité de la recherche dans une liste. - Mêmes questions que précédemment pour la recherche dans un dictionnaire.
Construire un graphique
Pour illustrer (ou observer) ces complexités, on peut tracer un graphique représentant l'évolution du temps d'exécution en fonction de N
.
Pour cela, on utilise le module matplotlib
en reliant des points dont les abscisses sont dans une première liste (tailles
par ex.) et les ordonnées dans une autre (temps
par ex.):
1 2 3 4 5 6 |
|
Commençons par créer une liste contenant les valeurs de N demandées, puis les temps obtenus pour chaque fonction:
1 2 3 |
|
1 2 3 4 5 |
|
On constate les temps d'exécution suivants (en ordre de grandeur, c'est-à-dire la puissance de 10 la plus proche)
N | 10 | 100 | 1000 | 10000 |
---|---|---|---|---|
Temps d'exécution pour la recherche dans une liste | \(10^{-7}\) | \(10^{-6}\) | \(10^{-5}\) | \(10^{-4}\) |
Temps d'exécution pour la recherche dans un dictionnaire | \(10^{-7}\) | \(10^{-7}\) | \(10^{-7}\) | \(10^{-7}\) |
Autrement dit, pour la recherche dans une liste, lorsqu'on multiplie N par 10, le temps d'exécution est lui aussi multiplié par 10, ce qui exprime la proportionnalité du temps d'exécution par rapport à N : la complexité est linéaire, en \(O(n)\).
En revanche, pour la recherche dans un dictionnaire, le temps d'exécution ne varie pas: la complexité est en temps constant, en \(O(1)\).
Cela s'illustre assez bien avec un graphique:
1 2 3 4 5 6 |
|
2. Table de hachage⚓︎
Puisqu'un tableau classique ne convient pas, il existe principalement deux méthodes pour implémenter correctement un tableau associatif:
- un arbre binaire de recherche;
- une table de hachage.
Python, avec le type dict
, utilise une table de hachage.
Principe d'une table de hachage
Une table de hachage est un tableau couplé à une fonction de hachage.
Le dictionnaire d = {"pommes":3, "poires":0, "bananes":5}
sera implémenté dans un tableau comme celui-ci:
- À chaque nouvelle clé, la fonction de hachage calcule un nombre entier (le «haché», ou «condensé»): par exemple
"pommes"
sera haché en7e1fd3345
; - un calcul sur cet entier donne un indice correspondant à la clé dans le tableau, généralement en prenant le reste de la
division euclidienne de la valeur de hachage par la taille du tableau:
7e1fd3345
devient l'indice4
; - On obtient ainsi la position du couple (clé, valeur) dans le tableau, pour l’ajout ou pour la recherche.
Le calcul de l’indice à partir de la clé s’effectue donc en temps constant. Et Python sait où chercher une clé: soit il la trouve à l'indice calculé, soit il n'y a rien et la clé n'appartient pas au dictionnaire.
Collisions
En théorie, toutes les clés peuvent être associées au même indice et terminer dans la même alvéole (case du tableau, bucket en anglais). La recherche d’une clé s’effectue alors en \(O(n)\). C’est le pire des cas.
En pratique, le semblant d’aléatoire de la fonction de hachage permet généralement une répartition équilibrée dans le tableau avec un petit nombre de couples dans chaque alvéole. Si le nombre de collisions (couples dans chaque alvéole) est majoré par une constante (au plus quatre couples dans chaque alvéole par exemple), l’ajout ou la recherche d’une clé sont en temps constant.
En résumé, avec une table de hachage, l’ajout ou la recherche d’une valeur sont généralement en temps constant, en \(O(1)\), mais peuvent être en \(O(n)\) dans le pire des cas. Cela dépend des collisions.
Clé non mutables
On ne peut pas utiliser un objet mutable comme clé: un dictionnaire a besoin d'avoir des clés dont les hachés sont définitifs.
Or si on prend une liste par exemple comme clé, on pourrait la modifier en dehors et donc ne plus la trouver dans le dictionnaire puisque son haché (et donc son indice dans le tableau) aurait changé!
On dit également que le type list
n'est pas hachable.
>>> lst = ["non", "hachable"]
>>> dico = {lst : "non non non"}
Traceback (most recent call last):
File "<console>", line 1, in <module>
TypeError: unhashable type: 'list'
3. Exercices⚓︎
Exercice 1
L’ARN contient le codage des protéines, composées de chaines d’acides aminés.
Le dictionnaire ci-dessous donne les correspondances entre les codons, des séquences d’ARN constitués de trois nucléotides, et les acides aminés.
La séquence AUG, par exemple, correspond à la méthionine, notée M.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Écrire une fonction traduction
qui traduit une chaine d’ARN en protéine. On suppose que la longueur
de la chaine d’ARN est un multiple de trois. Ainsi, traduction('UUCAGUGGG')
renverra 'FSG'
.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Exercice 2
Une ville souhaite gérer son parc de vélos en location partagée. L’ensemble de la flotte de vélos est stocké dans une table de données représentée en langage Python par un dictionnaire contenant des associations de type id_velo : dict_velo
où id_velo
est un nombre entier compris entre 1 et 199 qui correspond à l'identifiant unique du vélo et dict_velo
est un dictionnaire dont les clés sont : "type"
, "etat"
, "station"
.
Les valeurs associées aux clés "type"
, "etat"
, "station"
de dict_velo
sont de type chaînes de caractères ou nombre entier :
"type"
: chaîne de caractères qui peut prendre la valeur"electrique"
ou"classique"
;"état"
: nombre entier qui peut prendre la valeur 1 si le vélo est disponible, 0 si le vélo est en déplacement, -1 si le vélo est en panne;"station"
: chaînes de caractères qui identifie la station où est garé le vélo.
Dans le cas où le vélo est en déplacement ou en panne, "station"
correspond à celle
où il a été dernièrement stationné.
Voici un extrait de la table de données :
flotte = {
12 : {"type" : "electrique", "etat" : 1, "station" : "Prefecture"},
80 : {"type" : "classique", "etat" : 0, "station" : "Saint-Leu"},
45 : {"type" : "classique", "etat" : 1, "station" : "Baraban"},
41 : {"type" : "classique", "etat" : -1, "station" : "Citadelle"},
26 : {"type" : "classique", "etat" : 1, "station" : "Coliseum"},
28 : {"type" : "electrique", "etat" : 0, "station" : "Coliseum"},
74 : {"type" : "electrique", "etat" : 1, "station" : "Jacobins"},
13 : {"type" : "classique", "etat" : 0, "station" : "Citadelle"},
83 : {"type" : "classique", "etat" : -1, "station" : "Saint-Leu"},
22 : {"type" : "electrique", "etat" : -1, "station" : "Joffre"}
}
flotte
étant une variable globale du programme.
Toutes les questions de cet exercice se réfèrent à l'extrait de la table flotte fourni ci-dessus.
-
a. Que renvoie l'instruction
flotte[26]
?b. Que renvoie l'instruction
flotte[80]["etat"]
?c. Que renvoie l'instruction
flotte[99]["etat"]
? -
Voici le script d'une fonction :
def proposition(choix): for v in flotte: if flotte[v]["type"] == choix and flotte[v]["etat"] == 1: return flotte[v]["station"]
a. Quelles sont les valeurs possibles de la variable
choix
?b. Expliquer ce que renvoie la fonction lorsque l'on choisit comme paramètre l'une des valeurs possibles de la variable
choix
. -
a. Écrire un script en langage Python qui affiche les identifiants
(id_velo)
de tous les vélos disponibles à la station"Citadelle"
.b. Écrire un script en langage Python qui permet d'afficher l'identifiant
(id_velo)
et la station de tous les vélos électriques qui ne sont pas en panne. -
On dispose d'une table de données des positions GPS de toutes les stations, dont un extrait est donné ci-dessous. Cette table est stockée sous forme d’un dictionnaire. Chaque élément du dictionnaire est du type:
'nom de la station' : (latitude, longitude)
:stations = { 'Prefecture' : (49.8905, 2.2967) , 'Saint-Leu' : (49.8982, 2.3017), 'Coliseum' : (49.8942, 2.2874), 'Jacobins' : (49.8912, 2.3016) }
On admet que l'on dispose d'une fonction
distance
permettant de renvoyer la distance en mètres entre deux positions données par leurs coordonnées GPS (latitude et longitude).Cette fonction prend en paramètre deux tuples représentant les coordonnées des deux positions GPS et renvoie un nombre entier représentant cette distance en mètres.
Par exemple,
distance((49.8905, 2.2967), (49.8912, 2.3016))
renvoie9591
.Écrire une fonction qui prend en paramètre les coordonnées GPS de l'utilisateur sous forme d’un tuple et qui renvoie, pour chaque station située à moins de 800 mètres de l'utilisateur :
- le nom de la station ;
- la distance entre l'utilisateur et la station ;
- les identifiants des vélos disponibles dans cette station.
Une station où aucun vélo n’est disponible ne doit pas être affichée.
-
a. L'instruction
flotte[26]
renvoie la valeur associée à la clé26
, c'est-à-dire le dictionnaire{'type': 'classique', 'etat': 1, 'station': 'Coliseum'}
.b. L'instruction
flotte[80]['etat']
renvoie0
.c. L'instruction
flotte[99]['etat']
renvoie une erreur (KeyError
), puisqu'il n'y a pas de clé égale à99
dans le dictionnaireflotte
. -
a. Les valeurs possibles de la variable
choix
sontclassique
ouelectrique
.b. La fonction renvoie le nom des stations où des vélos du type
choix
sont disponibles. -
a.
1 2 3
for v in flotte: if flotte[v]['station'] == 'Citadelle' and flotte[v]['etat'] == 1: print(v)
b.
1 2 3
for v in flotte: if flotte[v]['type'] == 'electrique' and flotte[v]['etat'] != -1: print(v, flotte[v]['station'])
-
La liste en compréhension ligne 6 n'est pas obligatoire mais elle est bien pratique...
1 2 3 4 5 6 7 8 9
def info_velo(position:tuple) -> list: requete = [] for station, coordonnees_GPS in stations.items(): d = distance(position, coordonnees_GPS) if d < 800: velos_disponibles = [v for v in flotte if flotte[v]['station'] == station and flotte[v]['etat'] == 1] if velos_diponibles != []: requete.append((station, d, velos_disponibles)) return requete
Exercice 3
La cryptographie est un ensemble de techniques permettant de chiffrer un message.
Une technique de cryptographie consiste à mélanger les lettres d'un alphabet et à réécrire le message avec ces permutations. En Python, on peut créer un dictionnaire dans lequel les clés sont les lettres de l'alphabet et les valeurs sont celles de l'alphabet mélangé.
Exemple
Par exemple, si l'alphabet contient les 4 lettres A, B, C et D, et si le dictionnaire de l'alphabet mélangé est
alpha = {"A": "B", "B": "D", "C": "A", "D": "C"}
"BAC"
sera chiffrée "DBA"
.
Un tel dictionnaire sera appelé dictionnaire de chiffrement.
-
On souhaite chiffrer un message écrit avec l'alphabet A, B, C, D, E, F, G à l'aide du dictionnaire
alpha ={"A": "B", "B": "D", "C": "A", "D": "C", "E": "F", "F": "G", "G": "E"}
a. Quelle est la valeur associée à la clé
"D"
? En Python, comment l'obtenir ?b. Chiffrer la chaine de caractères
"BAGAGE"
avec le dictionnairealpha
. -
On considère qu'un mot est une chaine de caractères (un objet de type
str
) écrite uniquement avec les 26 lettres de l'alphabet en majuscule. Par exemple,"ARBRE"
est un mot et"L'ARBRE !"
n'est pas un mot à cause des caractères :"'"
," "
(espace) et"!"
.Écrire une fonction
chiffre
qui prend en paramètresmot
un mot etalpha
un dictionnaire de chiffrement, telle quechiffre(mot, alpha)
renvoiemot
sous forme de chaine chiffrée avec le dictionnaire de chiffrementalpha
. -
On souhaite déchiffrer un mot chiffré avec cette méthode.
a. Si un mot est chiffré avec le dictionnaire de chiffrement
alpha = {"A": "B", "B": "D", "C": "A", "D": "C", "E": "F", "F": "G", "G": "E"}
, donner un dictionnaire permettant de le déchiffrer.b. Écrire une fonction en Python appelée
dico_dechiffrement
qui prend en paramètredico
un dictionnaire de chiffrement et qui renvoie un dictionnaire permettant le déchiffrement. On pourra s'inspirer du code incomplet ci-dessous ou proposer une autre solution :def dico_dechiffrement(dico): nouveau = {} for lettre in dico: code = dico[...] nouveau[...] = ... return nouveau
c. Écrire une fonction telle que
dechiffre(mot_chiffre, dico)
renvoie le mot décodé, quandmot_chiffre
est chiffré par le dictionnaire de chiffrementdico
. On utilisera les fonctions écrites dans les questions précédentes. -
On souhaite à présent créer un dictionnaire de chiffrement. Écrire une fonction
dico_chiffrement
qui prend en paramètrealphabet
un tableau de lettres et qui renvoie un dictionnaire de chiffrement dont les clés sont les lettres du tableaualphabet
et les valeurs sont les lettres du tableau alphabet mélangées.On pourra utiliser la fonction
shuffle
du modulerandom
qui mélange en place un tableau. Par exemple, on a :>>> tableau = ["A", "B", "C", "D"] >>> shuffle(tableau) >>> tableau ["B", "A", "D", "C"]
-
a. La valeur associée à la clé
'D'
est'C'
, on l'obtient en Python avecalpha['D']
.b. On obtient
'DBEBEF'
. -
Il suffit d'utiliser une variable accumulatrice et de chiffrer en parcourant le paramètre
mot
:1 2 3 4 5
def chiffre(mot:str, alpha:str) -> str: resultat = '' for c in mot: resultat = resultat + alpha[c] return resultat
-
a. Il suffit d'inverser clés et valeurs, donc on peut déchiffrer avec le dictionnaire:
{'A': 'C', 'B': 'A', 'C': 'D', 'D': 'B', 'E': 'G', 'F': 'E', 'G': 'F'}
.b. On obtient:
1 2 3 4 5 6
def dico_dechiffrement(dico:dict) -> dict: nouveau = {} for lettre in dico: code = dico[lettre] nouveau[code] = lettre return nouveau
c. À l'aide des deux dernières fonctions, cela va très vite:
1 2 3
def dechiffre(mot_chiffre:str , dico:dict) -> str: dico_inverse = dico_dechiffrement(dico) return chiffre(mot_chiffre, dico_inverse)
-
La difficulté tient dans le fait qu'il faut faire une copie de la liste contenant l'alphabet avant de mélanger.
1 2 3 4 5 6 7 8 9
import random def dico_chiffrement(alphabet:list) -> dict: alpha = alphabet.copy() random.shuffle(alpha) dico = {} for i in range(len(alphabet)): dico[alphabet[i]] = alpha[i] return dico
Exercice 4
Afin d'organiser les répertoires et les fichiers sur un disque dur, une structure arborescente est utilisée. Les fichiers sont dans des répertoires qui sont eux-mêmes dans d'autres répertoires, etc.
Dans une arborescence, chaque répertoire peut contenir des fichiers et des répertoires, qui sont identifiés par leur nom. Le contenu d'un répertoire est modélisé par la structure de données dictionnaire. Les clés de ce dictionnaire sont des chaines de caractères donnant le nom des fichiers et des répertoires contenus.
Un répertoire
Un répertoire est représenté par un dictionnaire dont chaque clé est :
- soit un nom de sous répertoire : la valeur associée est alors un dictionnaire représentant le contenu de ce sous répertoire.
- soit un nom de fichier : la valeur associée est alors un entier, représentant la taille du fichier en ko.
Un fichier est donc une clé d'un répertoire(dictionnaire) dont la valeur associée est un entier.
Exemple illustré
Le répertoire appelé Téléchargements
contient deux fichiers rapport.pdf
et jingle.mp3
ainsi qu'un répertoire Images
contenant simplement le fichier logo.png
.
Il est représenté ci-dessous.
Ce répertoire Téléchargements
est modélisé en Python par le dictionnaire suivant :
{"Images": {"logo.png": 36}, "rapport.pdf": 450, "jingle.mp3": 4800}
Les valeurs numériques sont exprimées en ko (kilo-octets).
"logo.png": 36
signifie que le fichier logo.png
occupe un espace mémoire de 36 ko sur le disque dur.
On rappelle, ci-dessous, quelques commandes sur l'utilisation d'un dictionnaire :
dico = dict()
crée un dictionnaire vide appelédico
,dico[cle] = contenu
met la valeurcontenu
pour la clécle
dans le dictionnairedico
,dico[cle]
renvoie la valeur associée à la clécle
dans le dictionnairedico
,cle in dico
renvoie un booléen indiquant si la clécle
est présente dans le dictionnairedico
.for cle in dico:
permet d'itérer sur les clés d'un dictionnaire.len(dico)
renvoie le nombre de clés d'un dictionnaire.
L'adresse d'un fichier ou d'un répertoire correspond au nom de tous les répertoires à parcourir depuis la racine afin d'accéder au fichier ou au répertoire. Cette adresse est modélisée en Python par la liste des noms de répertoire à parcourir pour y accéder.
Par exemple, l'adresse du répertoire : /home/pierre/Documents/
est modélisée par la liste ["home", "pierre", "Documents"]
.
-
Dessiner l'arbre donné par le dictionnaire
docs
suivant, qui correspond au répertoire"Documents"
.docs = { "Administratif":{ "certificat_JDC.pdf": 1500, "attestation_recensement.pdf": 850 }, "Cours": { "NSI": { "TP.html": 60, "dm.odt": 345 }, "Philo": { "Tractatus_logico-philosophicus.epub": 2600 } }, "liste_de_courses.txt": 24 }
-
On donne la fonction
parcourt
suivante qui prend en paramètres un répertoire racine et une liste représentant une adresse, et qui renvoie le contenu du répertoire cible correspondant à l'adresse.Exemple : Si la variable
docs
contient le dictionnaire de l'exemple de la question 1 alorsparcourt(docs, ["Cours", "Philo"])
renvoie le dictionnaire{"Tractatus_logico-philosophicus.epub": 2600}
.a. Recopier et compléter la ligne 4:
1 2 3 4 5
def parcourt(racine, adr): repertoire = racine for nom_repertoire in adr: repertoire = ... return repertoire
b. Soit la fonction suivante :
def affiche(racine, adr, nom_fichier): repertoire = parcourt(racine, adr) print(repertoire[nom_fichier])
Qu'affiche l'instruction
affiche(docs, ["Cours", "NSI"], "TP.html")
sachant que la variabledocs
contient le dictionnaire de la question 1 ? -
a. La fonction
ajoute_fichier
suivante, de paramètresracine
,adr
,nom_fichier
ettaille
, ajoute au dictionnaireracine
, à l'adresseadr
, la clénom_fichier
associé à la valeurtaille
.Une ligne de la fonction donnée ci-dessous contient une erreur. Laquelle ? Proposer une correction.
1 2 3
def ajoute_fichier(racine, adr, nom_fichier, taille): repertoire = parcourt(racine, adr) taille = repertoire[nom_fichier]
b. Écrire une fonction
ajoute_repertoire
de paramètresracine
,adr
etnom_repertoire
qui crée un dictionnaire représentant un répertoire vide appelénom_repertoire
dans le dictionnaireracine
à l'adresseadr
. -
a.
isinstance
pour vérifier le type d'une variableisinstance(variable, A)
renvoieTrue
sivariable
est de typeA
etFalse
sinon.A
peut être le typeint
,dict
ou tout autre type Python.Écrire une fonction
est_fichier
de paramètresrepertoire
etcle
, oùcle
est une clé du dictionnairerepertoire
, qui détermine sicle
correspond à un fichier ou non. Sicle
n'est pas une clé derepertoire
, la fonction doit provoquer une erreur.On pourra compléter le code suivant ou en proposer un autre.
def est_fichier(repertoire, cle): return ...
b. Écrire une fonction
taille
de paramètreracine
qui prend en paramètre un dictionnaireracine
modélisant un répertoire et qui renvoie le total d'espace mémoire occupé par les fichiers contenus dans ce répertoire et ceux qu'il contient, de manière récursive.
-
Voici l'arbre obtenu:
-
a. On «plonge» dans chacun des dictionnaires successifs:
b. La fonction affiche la valeur associé à la clé1 2 3 4 5
def parcourt(racine, adr): repertoire = racine for nom_repertoire in adr: repertoire = repertoire[nom_repertoire] return repertoire
nom_fichier
du dictionnaire obtenu par parcours, c'est-à-dire60
. -
a. L'affectation ligne 3 est inversée, il faut écrire
repertoire[nom_fichier] = taille
b. On s'inspire de la fonction précédente:
1 2 3
def ajoute_repertoire(racine, adr, nom_repertoire): repertoire = parcourt(racine, adr) repertoire[nom_repertoire] = dict()
-
a. On vérifie simplement si la valeur associée à
cle
est de typeint
.1 2
def est_fichier(repertoire, cle): return isinstance(repertoire[cle], int)
b. L'écriture est délicate ici car le cas de base est à l'intérieur de la boucle qui parcourt les clés du dictionnaire: si la clé est un fichier, on renvoie sa taille, sinon on appelle récursivement la fonction sur la clé qui est alors un (sous-)répertoire.
1 2 3 4 5 6 7 8
def taille(racine): s = 0 for cle in racine: if est_fichier(racine, cle): s += racine[cle] else: s += taille(racine[cle]) return s