Un buffet de types de données spécialisés – Real Python

By | juillet 26, 2021

python pour débutant

Python collections module fournit un riche ensemble de types de données de conteneur spécialisés soigneusement conçu pour aborder des problèmes de programmation spécifiques d'une manière Pythonique et efficace. Le module fournit également des classes wrapper qui rendent plus sûr la création de classes personnalisées qui se comportent de la même manière que les types intégrés dict, liste, et str.

En savoir plus sur les types de données et les classes dans collections vous permettra de développer votre boîte à outils de programmation avec un ensemble précieux d'outils fiables et efficaces.

Dans ce didacticiel, vous apprendrez à :

  • Écrivez lisible et explicite coder avec nommétuple
  • Construire files d'attente et piles efficaces avec deque
  • Compter objets rapidement avec Compteur
  • Gérer clés de dictionnaire manquantes avec dict par défaut
  • Garantir la Ordre d'insertion de clés avec CommandéDict
  • Gérer plusieurs dictionnaires comme une seule unité avec ChainMap

Pour mieux comprendre les types de données et les classes dans collections, vous devez connaître les bases de l'utilisation des types de données intégrés de Python, tels que les listes, les tuples et les dictionnaires. De plus, la dernière partie de l'article nécessite des connaissances de base sur la programmation orientée objet en Python.

Premiers pas avec Python collections

De retour dans Python 2.4, Raymond Hettinger contribué un nouveau module appelé collections à la bibliothèque standard. L'objectif était de fournir divers types de données de collecte spécialisées pour aborder des problèmes de programmation spécifiques.

À ce moment-là, collections ne comprenait qu'une seule structure de données, deque, qui a été spécialement conçu comme une file d'attente à deux extrémités qui prend en charge l'efficacité ajouter et pop opérations à chaque extrémité de la séquence. A partir de ce moment, plusieurs modules de la bibliothèque standard ont profité de deque pour améliorer les performances de leurs classes et structures. Quelques exemples remarquables sont file d'attente et enfilage.

Avec le temps, une poignée de types de données de conteneurs spécialisés ont rempli le module :

Type de données Version Python La description
deque 2.4 Une collection de type séquence qui prend en charge l'ajout et la suppression efficaces d'éléments à chaque extrémité de la séquence
dict par défaut 2.5 Une sous-classe de dictionnaire pour construire des valeurs par défaut pour les clés manquantes et les ajouter automatiquement au dictionnaire
nommétuple() 2.6 Une fonction d'usine pour créer des sous-classes de tuple qui fournit des champs nommés qui permettent d'accéder aux éléments par leur nom tout en gardant la possibilité d'accéder aux éléments par index
CommandéDict 2.7, 3.1 Une sous-classe de dictionnaire qui conserve les paires clé-valeur ordonnées en fonction du moment où les clés sont insérées
Compteur 2.7, 3.1 Une sous-classe de dictionnaire qui prend en charge le comptage pratique d'éléments uniques dans une séquence ou itérable
ChainMap 3.3 Une classe de type dictionnaire qui permet de traiter un certain nombre de mappages comme un seul objet de dictionnaire

Outre ces types de données spécialisés, collections fournit également trois classes de base qui facilitent la création de listes personnalisées, de dictionnaires et de chaînes :

Classer La description
UserDict Une classe wrapper autour d'un objet dictionnaire qui facilite le sous-classement dict
Liste d'utilisateur Une classe wrapper autour d'un objet liste qui facilite le sous-classement liste
Chaîne utilisateur Une classe wrapper autour d'un objet chaîne qui facilite le sous-classement chaîne de caractères

Le besoin de ces classes wrapper a été partiellement éclipsé par la possibilité de sous-classer les types de données intégrés standard correspondants. Cependant, l'utilisation de ces classes est parfois plus sûre et moins sujette aux erreurs que l'utilisation de types de données standard.

Avec cette brève introduction à collections et les cas d'utilisation spécifiques que les structures de données et les classes de ce module peuvent résoudre, il est temps de les examiner de plus près. Avant cela, il est important de souligner que ce tutoriel est une introduction à collections dans son ensemble. Dans la plupart des sections suivantes, vous trouverez une boîte d'alerte bleue qui vous guidera vers un article dédié sur la classe ou la fonction à portée de main.

Amélioration de la lisibilité du code : nommétuple()

Python nommétuple() est une fonction d'usine qui vous permet de créer tuple sous-classes avec champs nommés. Ces champs vous donnent un accès direct aux valeurs dans un tuple nommé donné en utilisant le notation par points, comme dans obj.attr.

Le besoin de cette fonctionnalité est apparu car l'utilisation d'index pour accéder aux valeurs d'un tuple régulier est ennuyeuse, difficile à lire et sujette aux erreurs. Cela est particulièrement vrai si le tuple avec lequel vous travaillez contient plusieurs éléments et est construit loin de l'endroit où vous l'utilisez.

Une sous-classe de tuple avec des champs nommés auxquels les développeurs peuvent accéder avec la notation par points semblait être une fonctionnalité souhaitable dans Python 2.6. C'est l'origine de nommétuple(). Les sous-classes de tuples que vous pouvez créer avec cette fonction sont une grande victoire en lisibilité du code si vous les comparez avec des tuples normaux.

Pour mettre le problème de lisibilité du code en perspective, considérons divmod(). Cette fonction intégrée prend deux nombres (non complexes) et renvoie un tuple avec le quotient et reste qui résultent de la division entière des valeurs d'entrée :

>>>

>>> divmod(12, 5)
(2, 2)

Cela fonctionne bien. Cependant, ce résultat est-il lisible ? Pouvez-vous dire quelle est la signification de chaque nombre dans la sortie ? Heureusement, Python offre un moyen d'améliorer cela. Vous pouvez coder une version personnalisée de divmod() avec un résultat explicite en utilisant nommétuple:

>>>

>>> de collections importer nommétuple

>>> déf custom_divmod(X, oui):
...     DivMod = nommétuple("DivMod", « reste du quotient »)
...     revenir DivMod(*divmod(X, oui))
...

>>> résultat = custom_divmod(12, 5)
>>> résultat
DivMod(quotient=2, reste=2)

>>> résultat.quotient
2
>>> résultat.reste
2

Vous connaissez maintenant la signification de chaque valeur dans le résultat. Vous pouvez également accéder à chaque valeur indépendante à l'aide de la notation par points et d'un nom de champ descriptif.

Pour créer une nouvelle sous-classe de tuple en utilisant nommétuple(), vous avez besoin de deux arguments obligatoires :

  1. nom de type est le nom de la classe que vous créez. Il doit s'agir d'une chaîne avec un identifiant Python valide.
  2. nom_champ est la liste des noms de champs que vous utiliserez pour accéder aux éléments du tuple résultant. Ça peut être:
    • Un itérable de chaînes, tel que ["field1", "field2", ..., "fieldN"]
    • Une chaîne avec des noms de champs séparés par des espaces, tels que "champ1 champ2 ... champN"
    • Une chaîne avec des noms de champs séparés par des virgules, comme "champ1, champ2, ..., champN"

Par exemple, voici différentes manières de créer un exemple 2D Indiquer avec deux coordonnées (X et oui) en utilisant nommétuple():

>>>

>>> de collections importer nommétuple

>>> # Utiliser une liste de chaînes comme noms de champs
>>> Indiquer = nommétuple("Indiquer", [[[["X", "ou"])
>>> indiquer = Indiquer(2, 4)
>>> indiquer
Point(x=2, y=4)

>>> # Accéder aux coordonnées
>>> indiquer.X
2
>>> indiquer.oui
4
>>> indiquer[[[[0]
2

>>> # Utiliser une expression génératrice comme noms de champs
>>> Indiquer = nommétuple("Indiquer", (domaine pour domaine dans "xy"))
>>> Indiquer(2, 4)
Point(x=2, y=4)

>>> # Utilisez une chaîne avec des noms de champs séparés par des virgules
>>> Indiquer = nommétuple("Indiquer", "x, y")
>>> Indiquer(2, 4)
Point(x=2, y=4)

>>> # Utilisez une chaîne avec des noms de champs séparés par des espaces
>>> Indiquer = nommétuple("Indiquer", "x y")
>>> Indiquer(2, 4)
Point(x=2, y=4)

Dans ces exemples, vous créez d'abord Indiquer utilisant un liste des noms de champs. Ensuite, vous instanciez Indiquer faire un indiquer objet. Notez que vous pouvez accéder X et oui par nom de champ et aussi par index.

Les exemples restants montrent comment créer un tuple nommé équivalent avec une chaîne de noms de champs séparés par des virgules, une expression génératrice et une chaîne de noms de champs séparés par des espaces.

Les tuples nommés fournissent également un tas de fonctionnalités intéressantes qui vous permettent de définir des valeurs par défaut pour vos champs, de créer un dictionnaire à partir d'un tuple nommé donné, de remplacer la valeur d'un champ donné, et plus encore :

>>>

>>> de collections importer nommétuple

>>> # Définir les valeurs par défaut pour les champs
>>> Personne = nommétuple("Personne", "nom du travail", valeurs par défaut=[[[["Développeur Python"])
>>> personne = Personne("Jeanne")
>>> personne
Personne(nom='Jane', job='Développeur Python')

>>> # Créer un dictionnaire à partir d'un tuple nommé
>>> personne._asdict()
'name' : 'Jane', 'job' : 'Python Developer'

>>> # Remplacer la valeur d'un champ
>>> personne = personne._remplacer(travail="Développeur web")
>>> personne
Personne(nom='Jane', job='Développeur Web')

Ici, vous créez d'abord un Personne classe en utilisant nommétuple(). Cette fois, vous utilisez un argument facultatif appelé valeurs par défaut qui accepte une séquence de valeurs par défaut pour les champs du tuple. Notez que nommétuple() applique les valeurs par défaut aux champs les plus à droite.

Dans le deuxième exemple, vous créez un dictionnaire à partir d'un tuple nommé existant en utilisant ._asdict(). Cette méthode renvoie un nouveau dictionnaire qui utilise les noms de champs comme clés.

Enfin, vous utilisez ._remplacer() remplacer la valeur d'origine de travail. Cette méthode ne met pas à jour le tuple en place mais renvoie un nouveau tuple nommé avec la nouvelle valeur stockée dans le champ correspondant. Avez-vous une idée de pourquoi ._remplacer() renvoie un nouveau tuple nommé ?

Construire des files d'attente et des piles efficaces : deque

Python deque a été la première structure de données dans collections. Ce type de données de type séquence est une généralisation de piles et de files d'attente conçues pour prendre en charge une mémoire efficace et rapide ajouter et pop opérations aux deux extrémités de la structure de données.

En Python, les opérations d'ajout et de suppression au début ou à gauche de liste les objets sont inefficaces, avec O(m) complexité temporelle. Ces opérations sont particulièrement coûteuses si vous travaillez avec de grandes listes car Python doit déplacer tous les éléments vers la droite pour insérer de nouveaux éléments au début de la liste.

D'un autre côté, les opérations d'ajout et de suppression sur le côté droit d'une liste sont normalement efficaces (O(1)) sauf dans les cas où Python a besoin de réaffecter de la mémoire pour augmenter la liste sous-jacente pour accepter de nouveaux éléments.

Python deque a été créé pour surmonter ce problème. Opérations d'ajout et de suppression des deux côtés d'un deque object sont stables et tout aussi efficaces car deques sont implémentés sous la forme d'une liste doublement chaînée. C'est pourquoi les deques sont particulièrement utiles pour créer des piles et des files d'attente.

Prenons l'exemple d'une file d'attente. Il gère les éléments dans un Premier entré, premier sorti mode (FIFO). Cela fonctionne comme un tuyau, où vous insérez de nouveaux éléments à une extrémité du tuyau et sortez les anciens éléments de l'autre extrémité. L'ajout d'un élément à la fin d'une file d'attente est appelé file d'attente opération. La suppression d'un élément au début ou au début d'une file d'attente est appelée file d'attente.

Supposons maintenant que vous modélisez une file d'attente de personnes attendant d'acheter des billets pour un film. Vous pouvez le faire avec un deque. Chaque fois qu'une nouvelle personne arrive, vous la mettez en file d'attente. Lorsque la personne en tête de file obtient ses billets, vous la retirez de la file d'attente.

Voici comment vous pouvez émuler le processus à l'aide d'un deque objet:

>>>

>>> de collections importer deque

>>> ticket_queue = deque()
>>> ticket_queue
deque([])

>>> # Les gens arrivent dans la file d'attente
>>> ticket_queue.ajouter("Jeanne")
>>> ticket_queue.ajouter("John")
>>> ticket_queue.ajouter(" Linda ")

>>> ticket_queue
deque(['Jane', 'John', 'Linda'])

>>> # Les gens ont acheté leurs billets
>>> ticket_queue.popleft()
'Jeanne'
>>> ticket_queue.popleft()
'John'
>>> ticket_queue.popleft()
'Linda'

>>> # Aucune personne dans la file d'attente
>>> ticket_queue.popleft()
Traceback (appel le plus récent en dernier) :
  Fichier "", ligne 1, dans 
IndexError: pop d'un deque vide

Ici, vous créez d'abord un vide deque objet pour représenter la file d'attente des personnes. Pour mettre une personne en file d'attente, vous pouvez utiliser .ajouter(), qui ajoute des éléments à l'extrémité droite d'un deque. Pour retirer une personne de la file d'attente, vous utilisez .popleft(), qui supprime et renvoie les éléments à l'extrémité gauche d'un deque.

Le deque initializer prend deux arguments facultatifs :

  1. itérable contient un itérable qui sert d'initialiseur.
  2. maxlen contient un nombre entier qui spécifie la longueur maximale du deque.

Si vous ne fournissez pas de itérable, alors vous obtenez un deque vide. Si vous fournissez une valeur à maxlen, alors votre deque ne stockera que jusqu'à maxlen éléments.

Avoir un maxlen est une fonctionnalité pratique. Par exemple, supposons que vous deviez implémenter une liste de fichiers récents dans l'une de vos applications. Dans ce cas, vous pouvez effectuer les opérations suivantes :

>>>

>>> de collections importer deque

>>> fichiers récents = deque([[[["core.py", "README.md", "__init__.py"], maxlen=3)

>>> fichiers récents.ajouter à gauche("base de données.py")
>>> fichiers récents
deque(['database.py', 'core.py', 'README.md'], maxlen=3)

>>> fichiers récents.ajouter à gauche("exigences.txt")
>>> fichiers récents
deque(['requirements.txt', 'database.py', 'core.py'], maxlen=3)

Une fois que le deque atteint sa taille maximale (trois fichiers dans ce cas), l'ajout d'un nouveau fichier à une extrémité du deque supprime automatiquement le fichier à l'extrémité opposée. Si vous ne fournissez pas de valeur à maxlen, le deque peut atteindre un nombre arbitraire d'éléments.

Jusqu'à présent, vous avez appris les bases des deques, y compris comment les créer et comment ajouter et extraire des éléments des deux extrémités d'un deque donné. Deques fournit des fonctionnalités supplémentaires avec une interface de type liste. En voici quelques uns:

>>>

>>> de collections importer deque

>>> # Utilisez différents itérables pour créer des demandes
>>> deque((1, 2, 3, 4))
deque([1, 2, 3, 4])

>>> deque([[[[1, 2, 3, 4])
deque([1, 2, 3, 4])

>>> deque("a B c d")
deque(['a', 'b', 'c', 'd'])

>>> # Contrairement aux listes, deque ne prend pas en charge .pop() avec des indices arbitraires
>>> deque("a B c d").pop(2)
Traceback (appel le plus récent en dernier) :
  Fichier "", ligne 1, dans 
Erreur-type: pop() ne prend aucun argument (1 donné)

>>> # Prolonger un deque existant
>>> Nombres = deque([[[[1, 2])
>>> Nombres.se déployer([[[[3, 4, 5])
>>> Nombres
deque([1, 2, 3, 4, 5])

>>> Nombres.étendre vers la gauche([[[[-1, -2, -3, -4, -5])
>>> Nombres
deque([-5, -4, -3, -2, -1, 1, 2, 3, 4, 5])

>>> # Insérer un élément à une position donnée
>>> Nombres.insérer(5, 0)
>>> Nombres
deque([-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5])

Dans ces exemples, vous créez d'abord des demandes en utilisant différents types d'itérables pour les initialiser. Une différence entre deque et liste est-ce deque.pop() ne prend pas en charge le popping de l'élément à un index donné.

Notez que deque fournit des méthodes sœurs pour .ajouter(), .pop(), et .se déployer() avec le suffixe la gauche pour indiquer qu'ils effectuent l'opération correspondante sur l'extrémité gauche du deque sous-jacent.

Deques prend également en charge les opérations de séquence :

Méthode La description
.dégager() Supprimer tous les éléments d'un deque
.copie() Créer une copie superficielle d'un deque
.count(x) Compter le nombre d'éléments deque égal à X
.remove(valeur) Supprimer la première occurrence de valeur

Une autre caractéristique intéressante des deques est la possibilité de faire pivoter leurs éléments en utilisant .tourner():

>>>

>>> de collections importer deque

>>> ordinaux = deque([[[["première", "seconde", "la troisième"])
>>> ordinaux.tourner()
>>> ordinaux
deque(['third', 'first', 'second'])

>>> ordinaux.tourner(2)
>>> ordinaux
deque(['first', 'second', 'third'])

>>> ordinaux.tourner(-2)
>>> ordinaux
deque(['third', 'first', 'second'])

>>> ordinaux.tourner(-1)
>>> ordinaux
deque(['first', 'second', 'third'])

Cette méthode fait tourner le deque m marches vers la droite. La valeur par défaut de m est 1. Si vous fournissez une valeur négative à m, alors la rotation est vers la gauche.

Enfin, vous pouvez utiliser des indices pour accéder aux éléments d'un deque, mais vous ne pouvez pas découper un deque :

>>>

>>> de collections importer deque

>>> ordinaux = deque([[[["première", "seconde", "la troisième"])
>>> ordinaux[[[[1]
'seconde'

>>> ordinaux[[[[0:2]
Traceback (appel le plus récent en dernier) :
  Fichier "", ligne 1, dans 
Erreur-type: l'index de séquence doit être un entier, pas une "tranche"

Deques prend en charge l'indexation mais, fait intéressant, ils ne prennent pas en charge le découpage. Lorsque vous essayez de récupérer une tranche d'un deque existant, vous obtenez un Erreur-type. C'est parce que l'exécution d'une opération de découpage sur une liste chaînée serait inefficace, donc l'opération n'est pas disponible.

Gestion des clés manquantes : dict par défaut

Un problème courant auquel vous serez confronté lorsque vous travaillerez avec des dictionnaires en Python est de savoir comment gérer les clés manquantes. Si vous essayez d'accéder à une clé qui n'existe pas dans un dictionnaire donné, vous obtenez un KeyError:

>>>

>>> favoris = "animal de compagnie": "chien", "Couleur": "bleu", "Langue": "Python"

>>> favoris[[[["fruit"]
Traceback (appel le plus récent en dernier) :
  Fichier "", ligne 1, dans 
KeyError: 'fruit'

Il existe quelques approches pour contourner ce problème. Par exemple, vous pouvez utiliser .définir par defaut(). Cette méthode prend une clé comme argument. Si la clé existe dans le dictionnaire, elle renvoie la valeur correspondante. Sinon, la méthode insère la clé, lui attribue une valeur par défaut et renvoie cette valeur :

>>>

>>> favoris = "animal de compagnie": "chien", "Couleur": "bleu", "Langue": "Python"

>>> favoris.définir par defaut("fruit", "Pomme")
'Pomme'

>>> favoris
'pet': 'dog', 'color': 'blue', 'language': 'Python', 'fruit': 'apple'

>>> favoris.définir par defaut("animal de compagnie", "chat")
'chien'

>>> favoris
'pet': 'dog', 'color': 'blue', 'language': 'Python', 'fruit': 'apple'

Dans cet exemple, vous utilisez .définir par defaut() pour générer une valeur par défaut pour fruit. Comme cette clé n'existe pas dans favoris, .définir par defaut() le crée et lui attribue la valeur de Pomme. Si vous appelez .définir par defaut() avec une clé existante, l'appel n'affectera pas le dictionnaire et votre clé conservera la valeur d'origine au lieu de la valeur par défaut.

Vous pouvez aussi utiliser .avoir() pour renvoyer une valeur par défaut appropriée si une clé donnée est manquante :

>>>

>>> favoris = "animal de compagnie": "chien", "Couleur": "bleu", "Langue": "Python"

>>> favoris.avoir("fruit", "Pomme")
'Pomme'

>>> favoris
'pet': 'dog', 'color': 'blue', 'language': 'Python'

Ici, .avoir() Retour Pomme car la clé est manquante dans le dictionnaire sous-jacent. Pourtant, .avoir() ne crée pas la nouvelle clé pour vous.

Étant donné que la gestion des clés manquantes dans les dictionnaires est un besoin courant, Python collections fournit également un outil pour cela. Le dict par défaut type est une sous-classe de dict conçu pour vous aider avec les clés manquantes.

Le constructeur de dict par défaut prend un objet fonction comme premier argument. Lorsque vous accédez à une clé qui n'existe pas, dict par défaut appelle automatiquement cette fonction sans arguments pour créer une valeur par défaut appropriée pour la clé à portée de main.

Pour fournir sa fonctionnalité, dict par défaut stocke la fonction d'entrée dans .default_factory puis remplace .__manquant__() pour appeler automatiquement la fonction et générer une valeur par défaut lorsque vous accédez à des clés manquantes.

Vous pouvez utiliser n'importe quel appelable pour initialiser votre dict par défaut objets. Par exemple, avec entier() vous pouvez créer un compteur pour compter différents objets :

>>>

>>> de collections importer dict par défaut

>>> compteur = dict par défaut(entier)
>>> compteur
defaultdict(, )
>>> compteur[[[["chiens"]
0
>>> compteur
defaultdict(, 'chiens': 0)

>>> compteur[[[["chiens"] += 1
>>> compteur[[[["chiens"] += 1
>>> compteur[[[["chiens"] += 1
>>> compteur[[[["chats"] += 1
>>> compteur[[[["chats"] += 1
>>> compteur
defaultdict(, 'chiens' : 3, 'chats' : 2)

Dans cet exemple, vous créez un vide dict par défaut avec entier() comme premier argument. Lorsque vous accédez à une clé qui n'existe pas, le dictionnaire appelle automatiquement entier(), qui renvoie 0 comme valeur par défaut pour la clé à portée de main. Ce genre de dict par défaut object est très utile lorsqu'il s'agit de compter des choses en Python.

Un autre cas d'utilisation courant de dict par défaut est de grouper des choses. Dans ce cas, la fonction d'usine pratique est liste():

>>>

>>> de collections importer dict par défaut

>>> animaux domestiques = [[[[
...     ("chien", "Affenpinscher"),
...     ("chien", "Terrier"),
...     ("chien", "Boxeur"),
...     ("chat", "Abyssinien"),
...     ("chat", "Birman"),
... ]

>>> groupe_animaux = dict par défaut(liste)

>>> pour animal de compagnie, élever dans animaux domestiques:
...     groupe_animaux[[[[animal de compagnie].ajouter(élever)
...

>>> pour animal de compagnie, races dans groupe_animaux.éléments():
...     imprimer(animal de compagnie, "->", races)
...
chien -> ['Affenpinscher', 'Terrier', 'Boxer']
chat -> ['Abyssinian', 'Birman']

Dans cet exemple, vous disposez de données brutes sur les animaux et leur race, et vous devez les regrouper par animal. Pour ce faire, vous utilisez liste() comme .default_factory lorsque vous créez le dict par défaut exemple. Cela permet à votre dictionnaire de créer automatiquement une liste vide ([]) comme valeur par défaut pour chaque clé manquante à laquelle vous accédez. Ensuite, vous utilisez cette liste pour stocker les races de vos animaux de compagnie.

Enfin, il faut noter que puisque dict par défaut est une sous-classe de dict, il fournit la même interface. Cela signifie que vous pouvez utiliser votre dict par défaut objets comme vous utiliseriez un dictionnaire ordinaire.

Garder vos dictionnaires ordonnés : CommandéDict

Parfois, vous avez besoin que vos dictionnaires mémorisent l'ordre dans lequel les paires clé-valeur sont insérées. Les dictionnaires réguliers de Python étaient non ordonné structures de données pendant des années. Ainsi, en 2008, PEP 372 a introduit l'idée d'ajouter une nouvelle classe de dictionnaire à collections.

La nouvelle classe se souviendrait de l'ordre des éléments en fonction du moment où les clés ont été insérées. Ce fut l'origine de CommandéDict.

CommandéDict a été introduit dans Python 3.1. Son interface de programmation d'applications (API) est sensiblement la même que dict. Pourtant, CommandéDict itère sur les clés et les valeurs dans le même ordre que les clés ont d'abord été insérées dans le dictionnaire. Si vous attribuez une nouvelle valeur à une clé existante, l'ordre de la paire clé-valeur reste inchangé. Si une entrée est supprimée et réinsérée, elle sera déplacée à la fin du dictionnaire.

Il existe plusieurs façons de créer CommandéDict objets. La plupart d'entre eux sont identiques à la façon dont vous créez un dictionnaire ordinaire. Par exemple, vous pouvez créer un dictionnaire ordonné vide en instanciant la classe sans arguments, puis insérer des paires clé-valeur selon vos besoins :

>>>

>>> de collections importer CommandéDict

>>> Les étapes de la vie = CommandéDict()

>>> Les étapes de la vie[[[["enfance"] = "0-9"
>>> Les étapes de la vie[[[["adolescence"] = "9-18"
>>> Les étapes de la vie[[[["l'âge adulte"] = "18-65"
>>> Les étapes de la vie[[[["vieille"] = "+65"

>>> pour étape, ans dans Les étapes de la vie.éléments():
...     imprimer(étape, "->", ans)
...
enfance -> 0-9
adolescence -> 9-18
âge adulte -> 18-65
vieux -> +65

Dans cet exemple, vous créez un dictionnaire ordonné vide en instanciant CommandéDict sans argumentation. Ensuite, vous ajoutez des paires clé-valeur au dictionnaire comme vous le feriez avec un dictionnaire ordinaire.

Lorsque vous parcourez le dictionnaire, Les étapes de la vie, vous obtenez les paires clé-valeur dans le même ordre que vous les avez insérées dans le dictionnaire. Garantir l'ordre des articles est le principal problème qui CommandéDict résout.

Python 3.6 a introduit une nouvelle implémentation de dict. Cette implémentation fournit une nouvelle fonctionnalité inattendue : désormais, les dictionnaires classiques conservent leurs éléments dans le même ordre qu'ils ont été insérés pour la première fois.

Initialement, la fonctionnalité était considérée comme un détail d'implémentation et la documentation conseillait de ne pas s'y fier. Cependant, depuis Python 3.7, la fonctionnalité fait officiellement partie de la spécification du langage. Alors, quel est l'intérêt d'utiliser CommandéDict?

Il y a certaines caractéristiques de CommandéDict qui le rendent encore précieux :

  1. Communication d'intention : Avec CommandéDict, votre code indiquera clairement que l'ordre des éléments dans le dictionnaire est important. Vous communiquez clairement que votre code a besoin ou repose sur l'ordre des éléments dans le dictionnaire sous-jacent.
  2. Contrôle de l'ordre des articles : Avec CommandéDict, vous avez accès à .move_to_end(), qui est une méthode qui vous permet de manipuler l'ordre des éléments dans votre dictionnaire. Vous aurez également une variation améliorée de .popitem() qui permet de supprimer des éléments de chaque extrémité du dictionnaire sous-jacent.
  3. Comportement du test d'égalité : Avec CommandéDict, les tests d'égalité entre dictionnaires tiennent compte de l'ordre des éléments. Ainsi, si vous avez deux dictionnaires ordonnés avec le même groupe d'éléments mais dans un ordre différent, alors vos dictionnaires seront considérés comme non égaux.

Il y a au moins une autre raison d'utiliser CommandéDict: rétrocompatibilité. S'appuyer régulièrement dict les objets pour préserver l'ordre des éléments casseront votre code dans les environnements qui exécutent des versions de Python antérieures à 3.6.

D'accord, il est maintenant temps de voir certaines de ces fonctionnalités intéressantes de CommandéDict en action :

>>>

>>> de collections importer CommandéDict

>>> des lettres = CommandéDict(b=2, =4, une=1, c=3)
>>> des lettres
CommandéDict([('b', 2), ('d', 4), ('a', 1), ('c', 3)])

>>> # Déplacez b à l'extrémité droite
>>> des lettres.move_to_end("b")
>>> des lettres
CommandéDict([('d', 4), ('a', 1), ('c', 3), ('b', 2)])

>>> # Déplacez b vers l'extrémité gauche
>>> des lettres.move_to_end("b", dernier=Faux)
>>> des lettres
CommandéDict([('b', 2), ('d', 4), ('a', 1), ('c', 3)])

>>> # Trier les lettres par clé
>>> pour clé dans trié(des lettres):
...     des lettres.move_to_end(clé)
...

>>> des lettres
CommandéDict([('a', 1), ('b', 2), ('c', 3), ('d', 4)])

Dans ces exemples, vous utilisez .move_to_end() pour déplacer des articles et réorganiser des lettres. Notez que .move_to_end() accepte un argument optionnel appelé dernier qui vous permet de contrôler à quelle extrémité du dictionnaire vous souhaitez déplacer les éléments. Cette méthode est très pratique lorsque vous devez trier les éléments de vos dictionnaires ou lorsque vous devez manipuler leur ordre de quelque manière que ce soit.

Une autre différence importante entre CommandéDict et un dictionnaire ordinaire est la façon dont ils se comparent pour l'égalité :

>>>

>>> de collections importer CommandéDict

>>> # Les dictionnaires normaux ne comparent que le contenu
>>> lettres_0 = dict(une=1, b=2, c=3, =4)
>>> lettres_1 = dict(b=2, une=1, =4, c=3)
>>> lettres_0 == lettres_1
Vrai

>>> # Les dictionnaires ordonnés comparent le contenu et l'ordre
>>> lettres_0 = CommandéDict(une=1, b=2, c=3, =4)
>>> lettres_1 = CommandéDict(b=2, une=1, =4, c=3)
>>> lettres_0 == lettres_1
Faux

>>> lettres_2 = CommandéDict(une=1, b=2, c=3, =4)
>>> lettres_0 == lettres_2
Vrai

Ici, lettres_1 a une commande d'article différente de lettres_0. Lorsque vous utilisez des dictionnaires normaux, cette différence n'a pas d'importance et les deux dictionnaires se comparent égaux. En revanche, lorsque vous utilisez des dictionnaires ordonnés, lettres_0 et lettres_1 ne sont pas égaux. En effet, les tests d'égalité entre les dictionnaires ordonnés prennent en compte le contenu et également l'ordre des éléments.

Comptage d'objets en une seule fois : Compteur

Le comptage d'objets est une opération courante en programmation. Supposons que vous deviez compter combien de fois un élément donné apparaît dans une liste ou un itérable. Si votre liste est courte, le comptage de ses éléments peut être simple et rapide. Si vous avez une longue liste, il sera plus difficile de compter les éléments.

To count objects, you typically use a compteur, or an integer variable with an initial value of zero. Then you increment the counter to reflect the number of times a given object occurs.

In Python, you can use a dictionary to count several different objects at once. In this case, the keys will store individual objects, and the values will hold the number of repetitions of a given object, or the object’s compter.

Here’s an example that counts the letters in the word "mississippi" with a regular dictionary and a pour loop:

>>>

>>> mot = "mississippi"
>>> compteur = 

>>> pour lettre dans mot:
...     si lettre ne pas dans compteur:
...         compteur[[[[lettre] = 0
...     compteur[[[[lettre] += 1
...

>>> compteur
'm': 1, 'i': 4, 's': 4, 'p': 2

The loop iterates over the letters in mot. The conditional statement checks if the letters aren’t already in the dictionary and initializes the letter’s count to zero accordingly. The final step is to increment the letter’s count as the loop goes.

As you already know, defaultdict objects are convenient when it comes to counting things because you don’t need to check if the key exists. The dictionary guarantees appropriate default values for any missing keys:

>>>

>>> from collections importer defaultdict

>>> compteur = defaultdict(entier)

>>> pour lettre dans "mississippi":
...     compteur[[[[lettre] += 1
...

>>> compteur
defaultdict(, 'm': 1, 'i': 4, 's': 4, 'p': 2)

In this example, you create a defaultdict object and initialize it using int(). Avec int() as a factory function, the underlying default dictionary automatically creates missing keys and conveniently initializes them to zero. Then you increment the value of the current key to compute the final count of the letter in "mississippi".

Just like with other common programming problems, Python also has an efficient tool for approaching the counting problem. Dans collections, you’ll find Counter, which is a dict subclass specially designed for counting objects.

Here’s how you can write the "mississippi" example using Counter:

>>>

>>> from collections importer Counter

>>> Counter("mississippi")
Counter('i': 4, 's': 4, 'p': 2, 'm': 1)

Wow! That was quick! A single line of code and you’re done. In this example, Counter iterates over "mississippi", producing a dictionary with the letters as keys and their frequency as values.

There are a few different ways to instantiate Counter. You can use lists, tuples, or any iterables with repeated objects. The only restriction is that your objects need to be hashable:

>>>

>>> from collections importer Counter

>>> Counter([[[[1, 1, 2, 3, 3, 3, 4])
Counter(3: 3, 1: 2, 2: 1, 4: 1)

>>> Counter(([[[[1], [[[[1]))
Traceback (most recent call last):
  ...
TypeError: unhashable type: 'list'

Integer numbers are hashable, so Counter works correctly. On the other hand, lists aren’t hashable, so Counter fails with a TypeError.

Being hashable means that your objects must have a hash value that never changes during their lifetime. This is a requirement because these objects will work as dictionary keys. In Python, immutable objects are also hashable.

Depuis Counter is a subclass of dict, their interfaces are mostly the same. However, there are some subtle differences. The first difference is that Counter doesn’t implement .fromkeys(). This avoids inconsistencies, such as Counter.fromkeys("abbbc", 2), in which every letter would have an initial count of 2 regardless of the real count it has in the input iterable.

The second difference is that .update() doesn’t replace the count (value) of an existing object (key) with a new count. It adds both counts together:

>>>

>>> from collections importer Counter

>>> des lettres = Counter("mississippi")
>>> des lettres
Counter('i': 4, 's': 4, 'p': 2, 'm': 1)

>>> # Update the counts of m and i
>>> des lettres.mettre à jour(m=3, je=4)
>>> des lettres
Counter('i': 8, 'm': 4, 's': 4, 'p': 2)

>>> # Add a new key-count pair
>>> des lettres.mettre à jour("a": 2)
>>> des lettres
Counter('i': 8, 'm': 4, 's': 4, 'p': 2, 'a': 2)

>>> # Update with another counter
>>> des lettres.mettre à jour(Counter([[[["s", "s", "p"]))
>>> des lettres
Counter('i': 8, 's': 6, 'm': 4, 'p': 3, 'a': 2)

Here, you update the count for m et je. Now those letters hold the somme of their initial count plus the value you passed to them through .update(). If you use a key that isn’t present in the original counter, then .update() creates the new key with the corresponding value. Finally, .update() accepts iterables, mappings, keyword arguments, and also other counters.

Another difference between Counter et dict is that accessing a missing key returns 0 instead of raising a KeyError:

>>>

>>> from collections importer Counter

>>> des lettres = Counter("mississippi")
>>> des lettres[[[["a"]
0

This behavior signals that the count of an object that doesn’t exist in the counter is zero. In this example, the letter "a" isn’t in the original word, so its count is 0.

In Python, Counter is also useful to emulate a multiset or sac. Multisets are similar to sets, but they allow multiple instances of a given element. The number of instances of an element is known as its multiplicity. For example, you can have a multiset like 1, 1, 2, 3, 3, 3, 4, 4.

When you use Counter to emulate multisets, the keys represent the elements, and the values represent their respective multiplicity:

>>>

>>> from collections importer Counter

>>> multiset = Counter(1, 1, 2, 3, 3, 3, 4, 4)
>>> multiset
Counter(1: 1, 2: 1, 3: 1, 4: 1)

>>> multiset.clés() == 1, 2, 3, 4
True

Here, the keys of multiset are equivalent to a Python set. The values hold the multiplicity of each element in the set.

Python’ Counter provides a few additional features that help you work with them as multisets. For example, you can initialize your counters with a mapping of elements and their multiplicity. You can also perform math operations on the elements’ multiplicity and more.

Say you’re working at the local pet shelter. You have a given number of pets, and you need to have a record of how many pets are adopted each day and how many pets enter and leave the shelter. In this case, you can use Counter:

>>>

>>> from collections importer Counter

>>> inventaire = Counter(chiens=23, chats=14, pythons=7)

>>> adopté = Counter(chiens=2, chats=5, pythons=1)
>>> inventaire.subtract(adopté)
>>> inventaire
Counter('dogs': 21, 'cats': 9, 'pythons': 6)

>>> new_pets = "dogs": 4, "cats": 1
>>> inventaire.mettre à jour(new_pets)
>>> inventaire
Counter('dogs': 25, 'cats': 10, 'pythons': 6)

>>> inventaire = inventaire - Counter(chiens=2, chats=3, pythons=1)
>>> inventaire
Counter('dogs': 23, 'cats': 7, 'pythons': 5)

>>> new_pets = "dogs": 4, "pythons": 2
>>> inventaire += new_pets
>>> inventaire
Counter('dogs': 27, 'cats': 7, 'pythons': 7)

That’s neat! Now you can keep a record of your pets using Counter. Note that you can use .subtract() et .update() to subtract and add counts or multiplicities. You can also use the addition (+) and subtraction (-) operators.

There’s a lot more you can do with Counter objects as multisets in Python, so go ahead and give it a try!

Chaining Dictionaries Together: ChainMap

Python’s ChainMap groups multiple dictionaries and other mappings together to create a single object that works pretty much like a regular dictionary. In other words, it takes several mappings and makes them logically appear as one.

ChainMap objects are updateable views, which means that changes in any of the chained mappings affect the ChainMap object as a whole. This is because ChainMap doesn’t merge the input mappings together. It keeps a list of mappings and reimplements common dictionary operations on top of that list. For example, a key lookup searches the list of mappings successively until it finds the key.

When you’re working with ChainMap objects, you can have several dictionaries with either unique or repeated keys.

In either case, ChainMap allows you to treat all your dictionaries as one. If you have unique keys across your dictionaries, you can access and update the keys as if you were working with a single dictionary.

If you have repeated keys across your dictionaries, besides managing your dictionaries as one, you can also take advantage of the internal list of mappings to define some sort of access priority. Because of this feature, ChainMap objects are great for handling multiple contexts.

For example, say you’re working on a command-line interface (CLI) application. The application allows the user to use a proxy service for connecting to the Internet. The settings priorities are:

  1. Command-line options (--proxy, -p)
  2. Local configuration files in the user’s home directory
  3. Global proxy configuration

If the user supplies a proxy at the command line, then the application must use that proxy. Otherwise, the application should use the proxy provided in the next configuration object, and so on. This is one of the most common use cases of ChainMap. In this situation, you can do the following:

>>>

>>> from collections importer ChainMap

>>> cmd_proxy =   # The user doesn't provide a proxy
>>> local_proxy = "proxy": "proxy.local.com"
>>> global_proxy = "proxy": "proxy.global.com"

>>> configuration = ChainMap(cmd_proxy, local_proxy, global_proxy)
>>> configuration[[[["proxy"]
'proxy.local.com'

ChainMap allows you to define the appropriate priority for the application’s proxy configuration. A key lookup searches cmd_proxy, ensuite local_proxy, and finally global_proxy, returning the first instance of the key at hand. In this example, the user doesn’t provide a proxy at the command line, so your application uses the proxy in local_proxy.

In general, ChainMap objects behave similarly to regular dict objects. However, they have some additional features. For example, they have a .maps public attribute that holds the internal list of mappings:

>>>

>>> from collections importer ChainMap

>>> Nombres = "one": 1, "two": 2
>>> des lettres = "a": "A", "b": "B"

>>> alpha_nums = ChainMap(Nombres, des lettres)
>>> alpha_nums.Plans
['one': 1, 'two': 2, 'a': 'A', 'b': 'B']

The instance attribute .maps gives you access to the internal list of mappings. This list is updatable. You can add and remove mappings manually, iterate through the list, and more.

Additionally, ChainMap provides a .new_child() method and a .parents property:

>>>

>>> from collections importer ChainMap

>>> papa = "name": "John", "age": 35
>>> maman = "name": "Jane", "age": 31
>>> famille = ChainMap(maman, papa)
>>> famille
ChainMap('name': 'Jane', 'age': 31, 'name': 'John', 'age': 35)

>>> fils = "name": "Mike", "age": 0
>>> famille = famille.new_child(fils)

>>> pour personne dans famille.Plans:
...     imprimer(personne)
...
'name': 'Mike', 'age': 0
'name': 'Jane', 'age': 31
'name': 'John', 'age': 35

>>> famille.Parents
ChainMap('name': 'Jane', 'age': 31, 'name': 'John', 'age': 35)

Avec .new_child(), you create a new ChainMap object containing a new map (fils) followed by all the maps in the current instance. The map passed as a first argument becomes the first map in the list of maps. If you don’t pass a map, then the method uses an empty dictionary.

Le Parents property returns a new ChainMap objects containing all the maps in the current instance except for the first one. This is useful when you need to skip the first map in a key lookup.

A final feature to highlight in ChainMap is that mutating operations, such as updating keys, adding new keys, deleting existing keys, popping keys, and clearing the dictionary, act on the first mapping in the internal list of mappings:

>>>

>>> from collections importer ChainMap

>>> Nombres = "one": 1, "two": 2
>>> des lettres = "a": "A", "b": "B"

>>> alpha_nums = ChainMap(Nombres, des lettres)
>>> alpha_nums
ChainMap('one': 1, 'two': 2, 'a': 'A', 'b': 'B')

>>> # Add a new key-value pair
>>> alpha_nums[[[["c"] = "C"
>>> alpha_nums
ChainMap('one': 1, 'two': 2, 'c': 'C', 'a': 'A', 'b': 'B')

>>> # Pop a key that exists in the first dictionary
>>> alpha_nums.pop("two")
2
>>> alpha_nums
ChainMap('one': 1, 'c': 'C', 'a': 'A', 'b': 'B')

>>> # Delete keys that don't exist in the first dict but do in others
>>> del alpha_nums[[[["a"]
Traceback (most recent call last):
  ...
KeyError: "Key not found in the first mapping: 'a'"

>>> # Clear the dictionary
>>> alpha_nums.dégager()
>>> alpha_nums
ChainMap(, 'a': 'A', 'b': 'B')

These examples show that mutating operations on a ChainMap object only affect the first mapping in the internal list. This is an important detail to consider when you’re working with ChainMap.

The tricky part is that, at first glance, it could look like it’s possible to mutate any existing key-value pair in a given ChainMap. However, you can only mutate the key-value pairs in the first mapping unless you use .maps to access and mutate other mappings in the list directly.

Customizing Built-Ins: UserString, UserList, et UserDict

Sometimes you need to customize built-in types, such as strings, lists, and dictionaries to add and modify certain behavior. Since Python 2.2, you can do that by subclassing those types directly. However, you could face some issues with this approach, as you’ll see in a minute.

Python’s collections provides three convenient wrapper classes that mimic the behavior of the built-in data types:

  1. UserString
  2. UserList
  3. UserDict

With a combination of regular and special methods, you can use these classes to mimic and customize the behavior of strings, lists, and dictionaries.

Nowadays, developers often ask themselves if there’s a reason to use UserString, UserList, et UserDict when they need to customize the behavior of built-in types. La réponse est oui.

Built-in types were designed and implemented with the open-closed principle in mind. This means that they’re open for extension but closed for modification. Allowing modifications on the core features of these classes can potentially break their invariants. So, Python core developers decided to protect them from modifications.

For example, say you need a dictionary that automatically lowercases the keys when you insert them. You could subclass dict and override .__setitem__() so every time you insert a key, the dictionary lowercases the key name:

>>>

>>> classer LowerDict(dict):
...     déf __setitem__(soi, clé, valeur):
...         clé = clé.inférieur()
...         super().__setitem__(clé, valeur)
...

>>> ordinals = LowerDict("FIRST": 1, "SECOND": 2)
>>> ordinals[[[["THIRD"] = 3
>>> ordinals.mettre à jour("FOURTH": 4)

>>> ordinals
'FIRST': 1, 'SECOND': 2, 'third': 3, 'FOURTH': 4

>>> isinstance(ordinals, dict)
True

This dictionary works correctly when you insert new keys using dictionary-style assignment with square brackets ([]). However, it doesn’t work when you pass an initial dictionary to the class constructor or when you use .update(). This means that you would need to override .__init__(), .update(), and probably some other methods for your custom dictionary to work correctly.

Now take a look at the same dictionary but using UserDict as a base class:

>>>

>>> from collections importer UserDict

>>> classer LowerDict(UserDict):
...     déf __setitem__(soi, clé, valeur):
...         clé = clé.inférieur()
...         super().__setitem__(clé, valeur)
...

>>> ordinals = LowerDict("FIRST": 1, "SECOND": 2)
>>> ordinals[[[["THIRD"] = 3
>>> ordinals.mettre à jour("FOURTH": 4)

>>> ordinals
'first': 1, 'second': 2, 'third': 3, 'fourth': 4

>>> isinstance(ordinals, dict)
False

It works! Your custom dictionary now converts all the new keys into lowercase letters before inserting them into the dictionary. Note that since you don’t inherit from dict directly, your class doesn’t return instances of dict as in the example above.

UserDict stores a regular dictionary in an instance attribute called .data. Then it implements all its methods around that dictionary. UserList et UserString work the same way, but their .data attribute holds a liste and a str object, respectively.

If you need to customize either of these classes, then you just need to override the appropriate methods and change what they do as required.

In general, you should use UserDict, UserList, et UserString when you need a class that acts almost identically to the underlying wrapped built-in class and you want to customize some part of its standard functionalities.

Another reason to use these classes rather than the built-in equivalent classes is to access the underlying .data attribute to manipulate it directly.

The ability to inherit from built-in types directly has largely superseded the use of UserDict, UserList, et UserString. However, the internal implementation of built-in types makes it hard to safely inherit from them without rewriting a significant amount of code. In most cases, it’s safer to use the appropriate class from collections. It’ll save you from several issues and weird behaviors.

Conclusion

In Python’s collections module, you have several specialized container data types that you can use to approach common programming problems, such as counting objects, creating queues and stacks, handling missing keys in dictionaries, and more.

The data types and classes in collections were designed to be efficient and Pythonic. They can be tremendously helpful in your Python programming journey, so learning about them is well worth your time and effort.

In this tutorial, you learned how to:

  • Write readable et explicit code using namedtuple
  • Build efficient queues et stacks en utilisant deque
  • Count objects efficiently using Counter
  • Handle missing dictionary keys avec defaultdict
  • Remember the insertion order of keys with OrderedDict
  • Chain multiple dictionaries in a single view with ChainMap

You also learned about three convenient wrapper classes: UserDict, UserList, et UserString. These classes are handy when you need to create custom classes that mimic the behavior of the built-in types dict, liste, et str.



[ad_2]