Tutorial

Comment utiliser le module des collections dans Python 3

Python

L'auteur a choisi le COVID-19 Relief Fund pour recevoir un don dans le cadre du programme Write for DOnations.

Introduction

Python 3 possède un certain nombre de structures de données intégrées, notamment des tuples, des dictionnaires et des listes. Les structures de données nous fournissent un moyen d'organiser et de stocker les données. Le module de collections nous aide à remplir et à manipuler les structures de données de manière efficace.

Dans ce tutoriel, nous allons passer en revue trois classes des collections pour vous aider à travailler avec des tuples, des dictionnaires et des listes. Nous utiliserons namedtuples pour créer des tuples avec des champs nommés, defaultdict pour regrouper de manière concise les informations dans les dictionnaires, et deque pour ajouter efficacement des éléments de chaque côté d'un objet de type liste. 

Pour ce tutoriel, nous travaillerons principalement avec un inventaire de poissons que nous devons modifier au fur et à mesure que des poissons sont ajoutés ou retirés d'un aquarium fictif.

Conditions préalables

Pour tirer le meilleur parti de ce tutoriel, il est recommandé de se familiariser avec le tuple, le dictionnaire et les types de données de liste que ce soit en ce qui concerne leur syntaxe que de la manière d'en extraire des données. Vous pouvez consulter ces tutoriels pour obtenir les informations de base nécessaires :

Ajouter des champs nominatifs aux tuples

Les tuples de Python sont une séquence d'éléments immuables, ou inchangeables, ordonnés. Les tuples sont fréquemment utilisés pour représenter des données en colonnes ; par exemple, les lignes d'un fichier CSV ou les lignes d'une base de données SQL. Un aquarium peut garder une trace de son inventaire de poissons sous la forme d'une série de tuples.

Un tuple de poissons individuel :

("Sammy", "shark", "tank-a")

Ce tuple est composé de trois éléments de chaîne caractères.

Bien qu'utile à certains égards, ce tuple n'indique pas clairement ce que représente chacun de ses champs. En réalité, l'élément 0 est un nom, l'élément 1 est une espèce, et l'élément 2 est le réservoir de stockage. 

Explication des champs de tuples de poissons :

name species tank
Sammy shark tank-a

Ce tableau montre clairement que chacun des trois éléments du tuple a une signification claire.

namedtuple du module collections vous permet d'ajouter des noms explicites à chaque élément d'un tuple pour rendre ces significations claires dans votre programme Python.

Utilisons namedtuple pour générer une classe qui nomme clairement chaque élément du tuple de poisson :

from collections import namedtuple

Fish = namedtuple("Fish", ["name", "species", "tank"])

from collections import namedtuple donne à votre programme Python l'accès à la fonction d'usine namedtuple. L'appel de la fonction namedtuple() renvoie une classe qui est liée au nom Fish. Le namedtuple() a deux arguments : le nom souhaité de notre nouvelle classe "Fish" et une liste d'éléments nommés ["name", "species", "tank"]. 

Nous pouvons utiliser la classe Fish pour représenter le tuple de poissons de tout à l'heure :

sammy = Fish("Sammy", "shark", "tank-a")

print(sammy)

Si nous exécutons ce code, nous obtiendrons le résultat suivant :

Output
Fish(name='Sammy', species='shark', tank='tank-a')

sammy est instancié à l'aide de la classeFish. sammy est un tuple avec trois éléments clairement nommés. 

Les champs de sammy sont accessibles par leur nom ou avec un index tuple traditionnel :

print(sammy.species)
print(sammy[1])

Si nous lançons ces deux appels d’impression, nous obtiendrons le résultat suivant :

Output
shark shark

L'accès à .species a la même valeur que l'accès au deuxième élément de sammy en utilisant [1]. 

Utiliser namedtuple du module collections rend votre programme plus lisible tout en conservant les propriétés importantes d'un tuple (à savoir qu'ils sont immuables et ordonnés). 

De plus, la fonction d'usine namedtuple ajoute plusieurs méthodes supplémentaires aux instances de Fish.

Utilisez ._asdict() pour convertir une instance en dictionnaire :

print(sammy._asdict())

Si nous lançons print, vous verrez des résultats comme ceux qui suivent :

Output
{'name': 'Sammy', 'species': 'shark', 'tank': 'tank-a'}

Appeler .asdict() sur sammy renvoie un dictionnaire mettant en correspondance les noms de chacun des trois champs avec leurs valeurs correspondantes. 

Les versions de Python plus anciennes que 3.8 pourraient produire cette ligne de manière légèrement différente. Vous pourriez, par exemple, voir un OrderedDict au lieu du simple dictionnaire présenté ici. 

Note : Dans Python, les méthodes avec des traits de soulignement en tête sont généralement considérées comme « privées ». Les méthodes supplémentaires fournies par namedtuple (comme _asdict(), ._make(), ._replace(), etc.), sont toutefois publiques.

Rassembler des données dans un dictionnaire

Il est souvent utile de collecter des données dans les dictionnaires Python. defaulttdict du module de collections peut nous aider à rassembler les informations dans les dictionnaires de manière rapide et concise. 

defaultdict ne soulève jamais une KeyError. Si une clé n'est pas présente, defaulttdict se contente d'insérer et de renvoyer une valeur de remplacement à la place : 

from collections import defaultdict

my_defaultdict = defaultdict(list)

print(my_defaultdict["missing"])

Si nous exécutons ce code, nous obtiendrons le résultat suivant :

Output
[]

defaultdict insère et renvoie une valeur de remplacement au lieu de lancer une KeyError. Dans ce cas, nous avons spécifié la valeur de remplacement sous forme de liste.

Les dictionnaires ordinaires, en revanche, lancent une KeyError sur les clés manquantes :

my_regular_dict = {}

my_regular_dict["missing"]

Si nous exécutons ce code, nous obtiendrons le résultat suivant :

Output
Traceback (most recent call last): File "<stdin>", line 1, in <module> KeyError: 'missing'

Le dictionnaire habituel my_regular_dict soulève une KeyError lorsque nous essayons d'accéder à une clé qui n'est pas présente. 

defaulttdict se comporte différemment d'un dictionnaire ordinaire. Au lieu de soulever une KeyError sur une clé manquante, defaultdict appelle la valeur de remplacement sans argument pour créer un nouvel objet. Dans ce cas, list() pour créer une liste vide. 

Pour continuer avec notre exemple d'aquarium fictif, disons que nous avons une liste de tuples de poissons représentant l'inventaire d'un aquarium :

fish_inventory = [
    ("Sammy", "shark", "tank-a"),
    ("Jamie", "cuttlefish", "tank-b"),
    ("Mary", "squid", "tank-a"),
]

Trois poissons existent dans l'aquarium - leur nom, leur espèce et leur bac de rétention sont notés dans ces trois tuples.

Notre objectif est d'organiser notre inventaire par réservoir - nous voulons connaître la liste des poissons présents dans chaque réservoir. En d'autres termes, nous voulons un dictionnaire qui cartographie "tank-a". à ["Jamie", "Mary"] et "tank-b" à ["Jamie"].

Nous pouvons utiliser defaulttdict pour regrouper les poissons par bassin :

from collections import defaultdict

fish_inventory = [
    ("Sammy", "shark", "tank-a"),
    ("Jamie", "cuttlefish", "tank-b"),
    ("Mary", "squid", "tank-a"),
]
fish_names_by_tank = defaultdict(list)
for name, species, tank in fish_inventory:
    fish_names_by_tank[tank].append(name)

print(fish_names_by_tank)

En exécutant ce code, nous obtiendrons le résultat suivant :

Output
defaultdict(<class 'list'>, {'tank-a': ['Sammy', 'Mary'], 'tank-b': ['Jamie']})

fish_names_by_tank est déclaré comme un paramètre par défaut qui consiste par défaut à insérer list() au lieu de lancer une KeyError.  Comme cela garantit que chaque clé dansfish_names_by_tank pointera sur une liste, nous pouvons librement appeler .append() pour ajouter des noms à la liste de chaque réservoir.

defaultdict vous aide ici car il réduit le risque d'erreurs inattendues de KeyErrors. Réduire lesKeyErrors inattendues signifie que votre programme peut être écrit plus clairement et avec moins de lignes. Plus concrètement, l'idiome defaultdict permet d'éviter d'instancier manuellement une liste vide pour chaque réservoir.

Sans défaultdict, le corps de la boucle for aurait pu ressembler davantage à ceci :

More Verbose Example Without defaultdict
...

fish_names_by_tank = {}
for name, species, tank in fish_inventory:
    if tank not in fish_names_by_tank:
      fish_names_by_tank[tank] = []
    fish_names_by_tank[tank].append(name)

L'utilisation d'un simple dictionnaire (au lieu d'un defaultdict) signifie que le corps de la boucle for doit toujours vérifier l'existence du tank donné dans fish_names_by_tank. Ce n'est qu'après avoir vérifié que tank est déjà présent dans fish_names_by_tank, ou vient d'être initialisé avec un [], qu'on peut ajouter le nom du poisson.

defaultdict peut aider à réduire le nombre de codes passe-partout lors du remplissage des dictionnaires car il ne provoque jamais de KeyError.

Utilisation de deque pour ajouter efficacement des éléments de chaque côté d'une collection

Les listes Python sont une séquence d'éléments ordonnée, mutable ou modifiable. Python peut ajouter des listes en temps constant (la longueur de la liste n'a aucun effet sur le temps qu'il faut pour l'ajout), mais l'insertion au début d'une liste peut être plus lente (le temps nécessaire augmente à mesure que la liste s'agrandit).

En termes de notation Big O, l'ajout à une liste est une opération O(1) à temps constant. L'insertion au début d'une liste, en revanche, est plus lente avec O(n) performance. 

Note : Les informaticiens mesurent souvent la performance des procédures en utilisant ce qu'on appelle la notation « Big O ». Lorsque la taille d'une entrée n'a aucun effet sur le temps nécessaire pour exécuter une procédure, on dit qu'elle se déroule en temps constant ou O(1) (“Big O of 1”). Comme vous l'avez appris plus haut, Python peut s'ajouter aux listes à performance temporelle constante, autrement dit O(1). 

Parfois, la taille d'une entrée a une incidence directe sur le temps nécessaire à l'exécution d'une procédure. L'insertion au début d'une liste Python, par exemple, est d'autant plus lente qu'il y a plus d'éléments dans la liste. La notation Big O utilise la lettre n pour représenter la taille de l'entrée. Cela signifie que l'ajout d'éléments au début d'une liste Python se fait en « temps linéaire » ou O(n) (“Big O of n”). 

En général, les procédures O(1) sont plus rapides que les procédures O(n).

On peut l'insérer au début d'une liste Python :

favorite_fish_list = ["Sammy", "Jamie", "Mary"]

# O(n) performance
favorite_fish_list.insert(0, "Alice")

print(favorite_fish_list)

Si nous exécutons ce qui suit, nous obtiendrons le résultat suivant :

Output
['Alice', 'Sammy', 'Jamie', 'Mary']

La méthode .insert (index, objet) sur list nous permet d'insérer "Alice" au début de favorite_fish_list. Toutefois, il est à noter que l'insertion au début d'une liste a O(n) performance. Comme la durée de favorite_fish_list augmente, le temps nécessaire pour insérer un poisson au début de la liste augmentera proportionnellement et prendra de plus en plus de temps.

deque (prononcé « deck ») du module collections est un objet de type liste qui nous permet d'insérer des éléments au début ou à la fin d'une séquence avec une performance à temps constant (O(1)).

Insérez un élément au début d'un deque: 

from collections import deque

favorite_fish_deque = deque(["Sammy", "Jamie", "Mary"])

# O(1) performance
favorite_fish_deque.appendleft("Alice")

print(favorite_fish_deque)

En exécutant ce code, nous obtiendrons le résultat suivant :

Output
deque(['Alice', 'Sammy', 'Jamie', 'Mary'])

Nous pouvons instancier un deque en utilisant un ensemble d'éléments préexistants, en l'occurrence une liste de trois noms de poissons favoris. L'appel de la méthode appendleft de favorite_fish_deque nous permet d'insérer un élément au début de notre collection avec la performance O (1). O(1) performance signifie que le temps nécessaire pour ajouter un élément au début de favorite_fish_deque n'augmentera pas, même si favorite_fish_deque a des milliers ou des millions d'éléments. 

Note : Bien que deque ajoute des entrées au début d'une séquence plus efficacement qu'une liste, deque n'effectue pas toutes ses opérations plus efficacement qu'une liste. Par exemple, l'accès à un article aléatoire dans un deque a une performance O(n), mais l'accès à un élément aléatoire d'une liste a une performance O(1). Utilisez deque lorsqu'il est important d'insérer ou de retirer rapidement des éléments de chaque côté de votre collection. Une comparaison complète des performances temporelles est disponible sur le wiki de Python.

Conclusion

Le module des collections est une partie puissante de la bibliothèque standard Python qui vous permet de travailler avec des données de manière concise et efficace.  Ce tutoriel couvrait trois des cours fournis par le module des collections comprenant namedtuple, defaultdict, et deque. 

D'ici, vous pouvez utiliser la documentation du module de collecte pour en savoir plus sur les autres classes et utilités disponibles. Pour en savoir plus sur Python en général, vous pouvez lire notre Série de tutoriels Comment coder en Python 3. 

Creative Commons License