Aller au contenu

1NSI : cours Dictionnaires Python⚓︎

Introduction⚓︎

Dictionnaires / Tableaux Associatifs

Un Dictionnaire en Python, ou Tableau Associatif dans d'autres langages, est un conteneur qui fait correspondre à des éléments hashables, appelés les clés (du dictionnaire), des objets de type quelconque, appelés les valeurs (du dictionnaire).

nomDictionnaire {
  # type hashable : type quelconque
  cle1 : valeur1,
  cle2 : valeur2,
  cle3 : valeur3  # virgule, ou pas (à la dernière ligne)
  # etc...
}

Les dictionnaires sont donc indexés par les clés hashables (et non pas par des entiers).

Rappel

  • Types Hashables : int, float, str, tuple, (frozenset), ...
  • Types NON Hashables : set, list, dict, ...

C'est une structure de données primitive de Python, notée dict, qui est un type construit donc un conteneur/collection de paires clés : valeurs 🇫🇷 ou keys : values pairs 🇬🇧

Un dict en Python, modélise la notion de fonction définie sur un dictionnaire fini en Mathématiques: ce sont des mappings

Les types des différentes paires clés:valeurs peuvent :

  • être variables, pour certains langages, dits à typage dynamique (Python, ..),
  • ou pas, pour certains autres langages, dits à typage statique (langage C, Java, etc..)

Définir un Dictionnaire avec des {}⚓︎

On définit un dictionnaire de type dict, avec des accolades {}

# Les clés peuvent être des nombres entiers (car ils sont hashables)
>>> dico = {
  # clés : valeurs
  13 : "marseille",
  69 : "lyon",
  29 : "brest"
  }

# et même : un mélange de plusieurs types de nombres (entiers et flottants)
>>> dico = {
  # clés : valeurs
  13 : "marseille",
  69 : "lyon",
  29 : "brest",
  3.14 : "pi"
  }

# voire : un mélange de nombres (entiers et flottants), et d'autres types hashables comme 'str'
>>> dico = {
  # clés : valeurs
  13 : "marseille",
  69 : "lyon",
  3.14 : "pi",
  "marseille" : 13,
  "lyon" : 69
  }

# voire : on peut même utiliser des tuples en tant que clés (car ils sont hashables)
>>> dico = {
  # clés : valeurs
  13 : "marseille",
  69 : "lyon",
  3.14 : "pi",
  "marseille" : 13,
  "lyon" : 69,
  (1,1) : "paul",
  ("marc","durand") : 27
  }

Quels types de paires 'clés-valeurs' peut contenir un dictionnaire ?

  • les clés d'un dictionnaire DOIVENT être hashables : types int, float, str, tuple En particulier les clés d'un dict ne peuvent PAS être (ni contenir) de type list, set, dict (car non hashables)
  • les valeurs d'un dictionnaire sont de type quelconque

DICTIONNAIRE VIDE la notation {} désigne le dictionnaire vide (et non pas un ensemble vide... attention)

Définir un dictionnaire avec dict()⚓︎

On peut utiliser le constructeur de type dict() pour définir un dictionnaire :

# à partir d'une suite de paramètres "clé=valeur" :
>>> d1 = dict(one=1,two=2,three=3)
# à partir d'un liste de tuples (clés,valeurs) :
>>> d2 = dict([('one',1),('two',2),('three',3)])
# à partir d'un tuple de tuples (clés,valeurs) :
>>> d3 = dict((('two',2),('one',1),('three',3)))
# à partir d'un ensemble/set de tuples (clés,valeurs) :
>>> d4 = dict({('three',3),('one',1),('two',2)})
# à partir d'un dictionnaire de clés:valeurs :
>>> d5 = dict({'three':3,'one':1,'two':2})
# un mélange des techniques précédentes
>>> d6 = dict({'one':1,'two':2}, three=3)

>>> d1
{'one': 1, 'two': 2, 'three': 3}

>>> d1==d2==d3==d4==d5==d6
True

Remarque : la notation dict() permet de définir le dictionnaire vide

Plus généralement, on peut générer un dictionnaire grâce à : * dict(iterable,clé1=valeur1, etc..)iterable est un itérable de tuples (clés, valeurs) (chaînes, listes, tuples, sets). Les paramètres clé1 = valeur1 sont facultatifs, et doivent être placés à la fin. * dict(mapping,clé1=valeur1,etc..)mapping est un mapping, comprendre une succession d'associations clés:valeurs (dictionnaires). Les paramètres clé1 = valeur1 sont facultatifs, et doivent être placés à la fin.

Fonctions utiles sur les dictionnaires⚓︎

Longueur d'un dictionnaire⚓︎

>>> d = {'one': 1, 'two': 2, 'three': 3}
>>> print(d)
{'one': 1, 'two': 2, 'three': 3}
>>> len(d)  # renvoie le nombre de paires clés:valeurs du dictionnaire
3

len renvoie la longueur du dictionnaire d

dictionnaire Vide⚓︎

# {} ou dict() désignent le dictionnaire VIDE
>>> d = {}
>>> d = dict()
>>> len(d)
0   # la longueur du dictionnaire vide vaut 0

DICTIONNAIRE VIDE \(\,\) {} ou dict() représentent le dictionnaire vide, (et NON PAS l'ensemble vide)

Appartenance à un dictionnaire avec in, ou pas, avec not in⚓︎

>>> d = {'one': 1, 'two': 2, 'three': 3}
>>> d
{'one': 1, 'two': 2, 'three': 3}
>>> 'one' in d  # ATTENTION : ne recherche QUE parmi les clés (sous-entendu)
>>> 'one' in d.keys() # équivalent à la ligne précédente
True
# ou bien NON appartenance avec les mots-clés `not in`
>>> 'one' not in d.keys()
False
>>> 1 in d  # ne recherche QUE parmi les clés du dictionnaire (sous-entendu)
False
>>> 1 in d.values()  # rechercher parmi les valeurs du dictionnaire
True

# les dictionnaires ne peuvent PAS contenir de dictionnaires en tant que clés...
>>> d = { {1:'a', 2:'b'} :3, 'c':4, 'd':5}
TypeError: unhashable type: 'dict'

# Par contre, rien n'empêche d'avoir un dictionnaire en tant que valeur d'un dictionnaire:
>>> d={3: {1: 'a', 2: 'b'}, 'c':4, 'd':5}
>>> d
{3: {1:'a', 2:'b'}, 'c':4, 'd':5}

Indices / Index⚓︎

Les clés hashables sont les indices des dictionnaires.

>>> d = {'a': 1, 2: 'b', 3: [4,5], (1,2) : 'laurent', 5: {1: 'a', 2: 'b'}}
>>> d['a']  # renvoie la valeur dont la clé est 'a', ici:
1
>>> d[(1,2)]  # renvoie la valeur dont la clé est le tuple (1,2), ici:
'laurent'

Pas de slicing évidemment.

Les dictionnaires NE sont PAS des Séquences⚓︎

Les dictionnaires disposent bien d'une fonction len(), mais PAS d'indices entiers en général: en particulier ils ne sont pas ordonnés (contrairement aux séquences), donc :

Pte

Les dictionnaires dict NE sont PAS des séquences.

Techniques de parcours de dictionnaires⚓︎

Technique 1 : Parcourir les clés (une par une) avec .keys()⚓︎

d={'un':1, 'deux':2, 'trois':3}
for cle in d: # Par défaut, la variable 'cle' prend les valeurs des clés du dictionnaire
  print(cle)

for cle in d.keys(): # équivalent à la boucle précédente
  print(cle)

# Affiche : 
# un 
# deux
# trois

Pte

Les clés des dictionnaires dict sont des itérables.

Technique 2 : Parcourir les valeurs (une par une) avec .values()⚓︎

d = {'un':1, 'deux':2, 'trois':3}
for valeur in d.values(): # équivalent à la boucle précédente
  print(valeur)

# Affiche : 
# 1 
# 2 
# 3

Pte

Les valeurs des dictionnaires dict sont des itérables.

Technique 3 : Parcourir les paires clé:valeur (une par une) avec .items()⚓︎

d={'un':1, 'deux':2, 'trois':3}
for cle,valeur in d.items(): # équivalent à la boucle précédente
  print(cle,valeur)

# Affiche : 
# un 1
# deux 2
# trois 3

Pte

Les paires (clés,valeurs) des dictionnaires dict sont des itérables.

Méthodes sur les dictionnaires⚓︎

Méthodes usuelles⚓︎

Voici quelques exemples d'utilisation de quelques méthodes utiles sur les dictionnaires.

Notons d un dictionnaire : d={'un':1,'deux':2,'trois':3}

  • {}.fromkeys(iterable,valeur=None) crée un dictionnaire dont les clés sont dans iterable et toutes les valeurs égales à valeur (None par défaut)
  • d.get(cle,default=None)
    • renvoie la valeur correspondant à la cle, si elle appartient au dictionnaire
    • sinon, renvoie default (None par défaut)
  • d.pop(cle[,default]) renvoie :
    • Si la cle a été trouvée : supprime la cle et renvoie la valeur correspondante
    • Si la cle n'a pas été trouvée:
      • renvoie default s'il a été donné,
      • sinon, lève une erreur KeyError
  • d.popitem() supprime et renvoie une paire (cle,valeur) en tant que 2-tuple :
    • les 2-tuples sont renvoyés selon la logique LIFO (Last In First Out: Dernier Arrivé Premier Sorti) encore appelé Pile
    • lève une KeyError si le dictionnaire est vide
  • d.copy() renvoie une copie peu profonde / superficielle de d
    ⚠ ATTENTION ⚠ : Il existe plusieurs méthodes de copie d'un dictionnaire, avec des propriétés différentes.

    Ex 1 : si les valeurs du dictionnaire sont iterables alors la modification de d1 entraîne la modification de d2 :

    >>> d1 = {'un':[1,2],'trois':3}
    >>> d2 = d1.copy() # équivalent à d2=d1
    >>> d2
    {'un':[1,2],'trois':3}
    >>> d1['un'].append(5)
    >>> d1
    {'un': [1, 2, 5], 'trois': 3}
    >>> d2
    {'un': [1, 2, 5], 'trois': 3}
    
    On peut éviter ce comportement, de sorte que les deux copies d1 et d2 soient totalement indépendantes (si tel est votre souhait), en utilisant une copie profonde :

    >>> import copy
    >>> d1 = {'un':[1,2],'trois':3}
    >>> d2 = copy.deepcopy(d1)
    >>> d2
    {'un':[1,2],'trois':3}
    >>> d1['un'].append(5)
    >>> d1
    {'un': [1, 2, 5], 'trois': 3}
    >>> d2
    {'un': [1, 2], 'trois': 3}
    

    Ex 2 : sinon, si les valeurs du dictionnaire ne sont pas iterables alors la modification de d1 N'entraîne PAS la modification de d2 :

    >>> d1 = {'un':1,'trois':3}
    >>> d2 = d1.copy()
    >>> d2
    {'un':1,'trois':3}
    >>> d1['un'] = 2
    >>> d1
    {'un': 2, 'trois': 3}
    >>> d2
    {'un': 1, 'trois': 3}
    

    Ex 3 : affectation par Référence avec un =
    la modification de d1 entraîne TOUJOURS la modification de d2 :

    >>> d1 = {'un':1,'trois':3}
    >>> d2 = d1
    >>> d2
    {'un':1,'trois':3}
    >>> d1['un'] = 2
    >>> d1
    {'un': 2, 'trois': 3}
    >>> d2
    {'un': 2, 'trois': 3}
    

  • d.update([E,]**F)->None met à jour le dictionnaire d à partir des dict/iterables E et F
    Ex :

    >>> d1 = {'un':1, 'deux':2}
    >>> d2 = {'trois':3}
    >>> d1.update(d2)
    >>> d1
    {'un': 1, 'deux': 2, 'trois': 3}
    >>> d1.update({'deux':5})
    >>> d1
    {'un': 1, 'deux': 5, 'trois': 3}
    

  • d.clear() supprime tout le dict: équivalent à del d[:]

Liste Complète des Méthodes sur les dictionnaires⚓︎

Aide en ligne⚓︎

Vous trouverez une Liste Complète de Méthodes opérant sur les dictionnaires, sur cette page de la Documentation Officielle

Aide en Local (dans un Interpréteur Python)⚓︎

  1. Dans un interpréteur Python, dir(dict) affiche la liste complète de toutes les méthodes disponibles sur les dict, y compris les méthodes magiques/spéciales (cf ci-dessous), mais elles ne sont pas documentées (ni signature, ni docstring).

  2. Dans un interpréteur Python, help(dict) affiche la liste complète de toutes les méthodes disponibles sur les dict, y compris les méthodes magiques/spéciales (cf ci-dessous), AVEC DOCUMENTATION: AVEC LEURS SIGNATURES ET LES DOCSTRINGS.

Méthodes magiques / Méthodes spéciales sur les dictionnaires⚓︎

Méthodes magiques / Méthodes spéciales sur les dictionnaires

Parmi toutes les méthodes disponibles affichées par dir(dict), certaines sont encadrées par deux underscores (de chaque côté) __unCertainNom__() : Elles sont appelées des méthodes magiques ou méthodes spéciales sur les dictionnaires. En pratique cela signifie que :

  • elles sont accessibles via la syntaxe normale pour les méthodes : nomdictionnaire.__nomMethodeMagique__()
  • elles sont également accessibles via une syntaxe spéciale / magique (qui dépend de la méthode en question)

de Méthodes magiques / Méthodes spéciales sur les dictionnaires

On se donne deux dictionnaires d1={'un':1,'deux':2} et d2={'trois':3,'quatre':4,'cinq':5}

  • __len()__ : calcule la longueur d'une dictionnaire ...
    • Syntaxe normale : d2.__len__() renvoie le nombre \(3\)
    • Syntaxe spéciale : len(d2) renvoie le nombre \(3\)
  • __eq()__ : teste l'égalité entre deux dictionnaires ...
    • Syntaxe normale : d1.__eq__(d2) renvoie False car d1 et d2 ne sont pas égales
    • Syntaxe spéciale : d1 == d2 renvoie False (pour les mêmes raisons)
      Principe Général : À chaque fois qu'on veut tester l'égalité entre deux dictionnaires avec le symbole ==, c'est en fait la méthode magique __eq__() qui est appelée pour tester l'égalité.

Voici quelques autres méthodes magiques sur les dictionnaires :

  • __ne__() veut dire \(\ne\) : "Not Equal to c'est-à-dire Non égal, donc ! = en Python
  • __gt__() veut dire \(\gt\) : "Greater Than" c'est-à-dire Supérieur Strictement
  • __ge__() veut dire \(\ge\) : "Greater than or Equal to" c'est-à-dire Supérieur ou égal à
  • __lt__() veut dire \(\lt\) : "Less Than" c'est-à-dire Inférieur Strictement
  • __le__() veut dire \(\le\) : "Less than or Equal to" c'est-à-dire Inférieur ou égal
  • d.__getitem__(cle) renvoie d[cle], i.e. la valeur correspondant à la cle de d
  • d.__setitem__(cle,valeur) modifie la valeur de d[cle]. Équivalent à d[cle]=valeur
  • __contains__() correspond au mot-clé in utilisé pour tester l'inclusion d'un dictionnaire dans une autre
  • ⚠ __repr__() ⚠ représente un dictionnaire dans un interpréteur Python, c'est-à-dire qu'il affiche un dictionnaire dans un interpréteur Python, sous un certain format spécifique. Elle est appelée quand on tape dans l'interpréteur :
  • ou bien >>> d1 \(\quad\) (où d1 désigne le nom d'un dictionnaire)
  • ou bien >>> print(d1)
  • ⚠ __str__() ⚠ représente un dictionnaire dans un interpréteur Python, c'est-à-dire qu'il affiche un dictionnaire dans un interpréteur Python, sous un certain format spécifique, mais seulement pour le print()
  • >>> print(d1)
  • etc...

Opérations Arithmétiques sur les dictionnaires⚓︎

Addition⚓︎

  • PAS d'addition + entre deux dictionnaires.
  • La soustraction - existe, et a été définie dans les méthodes.

Multiplication⚓︎

  • PAS de produit entre deux dictionnaires, ni entre un dictionnaire et un entier
  • PAS de division entre deux dictionnaires

Les dictionnaires sont mutables⚓︎

Par exemple, on peut modifier un dictionnaire dict in situ, par exemple avec une méthode d'ajout d'élément .add(element), EN CONSERVANT LA MÊME ADRESSE MÉMOIRE (qui est en fait un pointeur vers le début du dictionnaire).

>>> d = {'un': 1, 'deux': 5}
>>> id(d) # renvoie l'adresse mémoire du début du dictionnaire
# Exemple de réponse:
140390976336960
# ajout d'élément :
>>> d.update({'deux':2})
>>> id(d) # la 'nouvelle' adresse mémoire du dictionnaire est inchangée
          # (ici, modification d'élément)
140390976336960

On s'aperçoit que les deux adresses mémoires, ou pointeurs, AVANT et APRÈS modification du dictionnaire, sont encores égales.

Mutabilité des dictionnaires

Les dictionnaires sont mutables.

Remarque Le fait que les dictionnaires soient mutables laisse à penser qu'il s'agit d'un type (/structure) de données Python spécialement prévu pour être modifiable avec une bonne efficacité.

Hashabilité (des clés)⚓︎

Pte

Les clés d'un dict doivent être hashables:

  • Types Hashables : int, float, str, tuple, (frozenset), ...
  • Types NON Hashables : set, list, dict, ...

Les valeurs d'un dict peuvent être quelconques.

# un ensemble 'set' ne peut PAS être défini comme clé d'un dictionnaire
>>> d = { {1,2}:3,4:'quatre'}
TypeError: unhashable type: 'set'
# NI une liste 'list' comme clé d'un dictionnaire
>>> d = {[1,2]:'un',3:'trois'}
TypeError: unhashable type: 'list'
# NI un dictionnaire 'dict' comme clé d'un dictionnaire
>>> d={ {1:'un',2:'deux'}:3,4:'quatre'}
TypeError: unhashable type: 'dict'

Compréhensions de dictionnaires⚓︎

Une compréhension de dictionnaires, ou dictionnaire en compréhension, est une syntaxe pour créer/générer un dictionnaire en une seule ligne de commande, en y incluant une boucle for sur une seule ligne.

Syntaxe sans if⚓︎

# 'iterable' est un itérable : une chaîne, une liste, un tuple, un dictionnaire, range(), etc...
>>> {fonctionCle(item):fonctionValeur(item) for item in iterable}
>>> dict({fonctionCle(item):fonctionValeur(item) for item in iterable})

Remarque : les chaînes, les listes, les tuples, les dictionnaires, range(), etc... sont des itérables.


Exp

>>> {i:i**2 for i in range(5)}
>>> {i:i**2 for i in [0,1,2,3,4]}
>>> {i:i**2 for i in (0,1,2,3,4)}
>>> dict({i:i**2 for i in (0,1,2,3,4)})

# Renvoient tous le même dictionnaire
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

Syntaxe avec un if⚓︎

# 'iterable' est un itérable : une chaîne, une liste, un tuple, un dictionnaire, range(), etc...
>>> {fonctionCle(item):fonctionValeur(item) for item in iterable if condition(item)}
>>> dict({fonctionCle(item):fonctionValeur(item) for item in iterable if condition(item)})

Exp

>>> {i:i**2 for i in range(10) if i%4!=0}
>>> dict({i:i**2 for i in range(10) if i%4!=0})

# Renvoient chacune :
{1: 1, 2: 4, 3: 9, 5: 25, 6: 36, 7: 49, 9: 81}


REMARQUE / ATTENTION Le if DOIT être placé APRÈS le for, du moins lorsque le if est tout seul (c'est-à-dire non accompagné d'un else). En particulier, la syntaxe suivante, que l'on pourrait naïvement croire équivalente, NE FONCTIONNE PAS:

>>> {i:**2 if i%4!=0 for i in range(10)}
>>> dict({i:i**2 if i%4!=0 for i in range(10)})
SyntaxError: invalid syntax

Syntaxe avec un if ET un else⚓︎

# 'iterable' est un itérable : une chaîne, une liste, un tuple, un dictionnaire, range(), etc...
>>> {fonctionCle(item):fonctionValeur(item) if condition(item) else autreFonction(item) for item in iterable}
>>> dict({fonctionCle(item):fonctionValeur(item) if condition(item) else autreFonction(item) for item in iterable})

Exp

>>> {i:i**2 if i%4!=0 else "bissextile" for i in range(10)}
>>> dict({i:i**2 if i%4!=0 else "bissextile" for i in range(10)})
# Renvoient chacune :
{0: 'bissextile', 1: 1, 2: 4, 3: 9, 4: 'bissextile', 5: 25, 6: 36, 7: 49, 8: 'bissextile', 9: 81}


REMARQUE / ATTENTION Le if DOIT être placé APRÈS le for, du moins lorsque le if est tout seul (c'est-à-dire non accompagné d'un else). En particulier, la syntaxe suivante, que l'on pourrait naïvement croire équivalente, NE FONCTIONNE PAS:

>>> {i:i**2 for i in range(21) if i%4!=0 else "bissextile"}
>>> dict({i:i**2 for i in range(21) if i%4!=0 else "bissextile"})
SyntaxError: invalid syntax