Licence CC BY-SA

Les dictionnaires

Les listes permettent de stocker un ensemble d’éléments en associant chaque élément à un index numérique.

Mais cela n’est pas adapté à toutes les données, toutes ne représentent pas une séquence de valeurs.

Comment par exemple représenter un répertoire téléphonique ? Avec une liste de listes où chaque sous-liste serait composée de deux éléments : un nom et un numéro ?

Par exemple, on pourrait avoir :

phonebook = [['Alice', '0633432380'], ['Bob', '0663621029'], ['Alex', '0714381809']]

Ça fonctionnerait mais c’est loin d’être idéal, il faudrait se souvenir d’utiliser [0] pour le nom et [1] pour le numéro, et ça demanderait de parcourir toute la liste chaque fois que l’on voudrait chercher un numéro.

Heureusement pour nous, les dictionnaires sont une structure de données bien plus adaptée à ce genre de problématique.

Des tables d'association

Comme les listes, les dictionnaires sont des conteneurs. C’est-à-dire qu’ils contiennent d’autres valeurs auxquelles on peut accéder avec l’opérateur d’indexation [].

Mais plutôt qu’associer une valeur à un index, ils vont l’associer à une clé quelconque, par exemple une chaîne de caractères. Il s’agit donc d’un ensemble de couples clé-valeur.

Un dictionnaire se définit avec des accolades, entre lesquelles les couples sont séparés par des virgules. Un couple clé-valeur est de la forme clé: valeur.

Voilà à quoi pourrait ressembler le répertoire téléphonique donné en introduction :

phonebook = {'Alice': '0633432380', 'Bob': '0663621029', 'Alex': '0714381809'}

C’est déjà plus clair à écrire, mais là où ça devient intéressant c’est pour l’accès aux éléments. On retrouve en effet l’opérateur [], mais on va pouvoir lui préciser une clé de notre dictionnaire plutôt qu’un index.

>>> phonebook['Alex']
'0714381809'

Il suffit de connaître le nom pour accéder au numéro, pas besoin de parcourir tout le répertoire. On comprend donc l’analogie avec le dictionnaire, qui permet d’associer des définitions à des mots, et de retrouver la définition à partir du mot.

Opérations sur les dictionnaires

Les dictionnaires sont des objets modifiables, on retrouve donc l’opérateur d’indexation en lecture et en écriture.

>>> phonebook['Alice']
'0633432380'
>>> phonebook['Bob'] = '0712800331'
>>> del phonebook['Alex']
>>> phonebook
{'Alice': '0633432380', 'Bob': '0712800331'}

Par contre pas de slicing ici, cela n’a pas de sens sur des clés de dictionnaire.

Une clé non trouvée dans le dictionnaire provoque une erreur.

>>> phonebook['Mehdi']
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'Mehdi'

Les dictionnaires sont sensibles à la casse, c’est-à-dire que les lettres majuscules sont traitées différemment des minuscules. 'Alice' et 'alice' sont alors deux clés distinctes.

>>> phonebook['alice'] = '0729570663'
>>> phonebook
{'Alice': '0633432380', 'Bob': '0712800331', 'alice': '0729570663'}

On retrouve aussi l’opérateur d’appartenance (in), qui fonctionne sur les clés et non sur les valeurs.

>>> 'Bob' in phonebook
True
>>> '0633432380' in phonebook
False

Et on peut connaître la taille d’un dictionnaire en appelant la fonction len.

>>> len(phonebook)
3

Comme tout objet, il est possible de tester l’égalité entre deux dictionnaires avec l’opérateur ==, et la différence avec !=. Deux dictionnaires sont considérés comme égaux s’ils contiennent les mêmes éléments, avec les mêmes valeurs pour les mêmes clés.

>>> {'a': 1} == {'a': 1}
True
>>> {'a': 0} == {'b': 0}
False
>>> {'a': 0} != {'a': 0, 'b': 1}
True

Cela est vrai quel que soit l’ordre des éléments dans le dictionnaire.

>>> {'a': 0, 'b': 1} == {'b': 1, 'a': 0}
True
>>> {'a': 0, 'b': 1} != {'b': 1, 'a': 0, 'c': 2}
True
Méthodes principales

Une première méthode intéressante est la méthode get. Elle agit comme l’opérateur [] mais sans produire d’erreur si la clé n’est pas trouvée.

>>> phonebook.get('Mehdi')
>>> print(phonebook.get('Mehdi'))
None

Comme on le voit, la valeur renvoyée si la clé n’est pas trouvée est None. Il est possible de renvoyer une autre valeur en la précisant comme second argument à get.

>>> phonebook.get('Mehdi', 'xxx')
'xxx'

Ensuite on va surtout trouver des méthodes pour modifier le dictionnaire, comme on en trouvait sur les listes.

La méthode pop est d’ailleurs équivalente à celle des listes, elle supprime une clé du dictionnaire et renvoie la valeur associée.

>>> phonebook.pop('Alice')
'0633432380'
>>> phonebook.pop('alice')
'0729570663'

L’appel produit une erreur si la clé n’est pas trouvée, mais il est là encore possible de donner une valeur par défaut en deuxième argument.

>>> phonebook.pop('Mehdi')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'Mehdi'
>>> phonebook.pop('Mehdi', 'xxx')
'xxx'

La méthode update permet d’étendre le dictionnaire avec les données d’un autre dictionnaire.

>>> phonebook.update({'Julie': '0619096810', 'Mehdi': '0762253973'})
>>> phonebook
{'Bob': '0712800331', 'Julie': '0619096810', 'Mehdi': '0762253973'}

Si une clé existe déjà dans le dictionnaire actuel, sa valeur est remplacée par la nouvelle qui est reçue.

>>> phonebook.update({'Julie': '0734593960'})
>>> phonebook
{'Bob': '0712800331', 'Julie': '0734593960', 'Mehdi': '0762253973'}

La méthode clear sert à vider complètement un dictionnaire.

>>> phonebook.clear()
>>> phonebook
{}

{} représente donc un dictionnaire vide.

Enfin, setdefault est un peu le pendant de get mais en écriture : elle va insérer une valeur dans le dictionnaire seulement si la clé n’est pas déjà présente.
La méthode renvoie la valeur associée à cette clé dans le dictionnaire, donc soit celle qui vient d’être ajoutée soit celle qui était déjà présente.

>>> phonebook.setdefault('Julie', '0619096810')
'0619096810'
>>> phonebook.setdefault('Julie', '0734593960')
'0619096810'
>>> phonebook
{'Julie': '0619096810'}
Conversions

On peut convertir une liste de couples clé/valeur en un dictionnaire, en appelant dict comme une fonction.

Par exemple avec notre répertoire téléphonique défini en introduction :

>>> phonebook = [['Alice', '0633432380'], ['Bob', '0663621029'], ['Alex', '0714381809']]
>>> dict(phonebook)
{'Alice': '0633432380', 'Bob': '0663621029', 'Alex': '0714381809'}

L’appel à dict sur un dictionnaire existant permet aussi d’en créer une copie.

>>> phonebook = {'Mehdi': '0762253973'}
>>> mybook = dict(phonebook)
>>> mybook['Julie'] = '0734593960'
>>> mybook
{'Mehdi': '0762253973', 'Julie': '0734593960'}
>>> phonebook
{'Mehdi': '0762253973'}

Enfin, une autre utilité de l’appel à dict est de pouvoir construire un dictionnaire à partir d’arguments nommés. Les noms des arguments deviennent ainsi les clés du dictionnaire.

>>> dict(Bob='0712800331', Julie='0734593960', Mehdi='0762253973')
{'Bob': '0712800331', 'Julie': '0734593960', 'Mehdi': '0762253973'}

Données composites

Un autre cas d’utilisation des dictionnaires est celui d’agréger dans un même objet plusieurs valeurs liées les unes aux autres, plutôt que dans des variables différentes. Pensez par exemple à la représentation des monstres dans notre TP : on a un nom d’un côté, un nombre de PV de l’autre et aussi une liste d’attaques.

À la place, on pourrait utiliser un dictionnaire par monstre, en y faisant figurer toutes ses données.

{
    'nom': 'Pythachu',
    'PV': 50,
    'attaques': ['tonnerre', 'charge'],
}

En effet, tous les types de données sont acceptés en tant que valeurs, et toutes les valeurs n’ont pas besoin d’être du même type.

Mais on peut faire encore mieux. En usant de listes et de dictionnaires, on construit facilement des structures arborescentes pour représenter toutes nos données.

monstres = {
    'Pythachu': {
        'type': 'foudre',
        'description': 'Petit rat surchargé.',
        'attaques': ['tonnerre', 'charge'],
    },
    'Pythard': {
        'type': 'aquatique',
        'description': "Tétard qui a cru qu'il était tôt.",
        'attaques': ['jet-de-flotte', 'charge'],
    },
    'Ponytha': {
        'type': 'flamme',
        'description': 'Cheval enflammé.',
        'attaques': ['brûlure', 'charge'],
    },
}

attaques = {
    'charge': {'degats': 20},
    'tonnerre': {'degats': 50},
    'jet-de-flotte': {'degats': 40},
    'brûlure': {'degats': 40},
}

joueurs = [
    {
        'nom': 'Joueur 1',
        'monstre': 'Pythachu',
        'PV': 100,
    },
    {
        'nom': 'Joueur 2',
        'monstre': 'Ponytha',
        'PV': 120,
    },
]

Ainsi, on représente dans des variables différentes la structure de nos données. Pour avoir d’un côté la définition des monstres et des attaques, et de l’autre les monstres en jeu.

>>> print(joueurs[0]['nom'], ':', joueurs[0]['monstre'])
Joueur 1 : Pythachu
>>> print('Attaques :', monstres[joueurs[0]['monstre']]['attaques'])
Attaques : ['tonnerre', 'charge']
>>> print('Dégâts de tonnerre :', attaques['tonnerre']['degats'])
Dégâts de tonnerre : 50

Clés de dictionnaires

Jusqu’ici, je n’ai présenté que des chaînes de caractères comme clés de dictionnaire, par souci de simplicité.

Mais ce ne sont pas les seuls types de clés possibles, les booléens ou les nombres sont aussi des clés valides.

choices = {
    True: 'OK',
    False: 'KO',
}

diviseurs = {
    4: [1, 2],
    6: [1, 2, 3],
    8: [1, 2, 4],
    9: [1, 3],
    10: [1, 2, 5],
}

Qui s’utilisent de la même manière lors de l’indexation.

>>> choices[True]
'OK'
>>> choices[False] = 'erreur'
>>> diviseurs[8]
[1, 2, 4]
>>> diviseurs.get(5, [1])
[1]

Dans le cas de la construction d’un dictionnaire à l’aide d’un appel du type dict(a=0, b=1), les clés seront forcément des chaînes de caractères et doivent correspondre à des noms valides.
Il n’est alors pas possible d’écrire quelque chose comme dict(9=[1, 3]) puisque 9 n’est pas un nom d’argument valide.


En revanche, tout type de données n’est pas accepté comme clé de dictionnaire. Vous avez dû vous en rendre compte si vous avez essayé d’y placer une liste ou un dictionnaire.

>>> {[]: 0}
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'

Nous reviendrons plus tard sur cette erreur et ce qu’elle signifie, mais retenez pour le moment que dans les types que nous connaissons seuls les non-modifiables peuvent être utilisés en tant que clés.

Les valeurs modifiables telles que les listes ou les dictionnaires ne peuvent pas être utilisées en tant que clés, car comment retrouverait-on la valeur associée si la clé est mise à jour ?