Structures de données communes Python (Guide) – Real Python

By | août 26, 2020

python pour débutant

Structures de données sont les constructions fondamentales autour desquelles vous construisez vos programmes. Chaque structure de données fournit une manière particulière d'organiser les données afin qu'elles soient accessibles efficacement, en fonction de votre cas d'utilisation. Python est livré avec un vaste ensemble de structures de données dans sa bibliothèque standard.

Cependant, la convention de dénomination de Python n'offre pas le même niveau de clarté que celui que vous trouverez dans d'autres langues. En Java, une liste n’est pas seulement un liste– c'est soit un LinkedList ou un Liste des tableaux. Ce n'est pas le cas en Python. Même les développeurs Python expérimentés se demandent parfois si le liste type est implémenté sous forme de liste chaînée ou de tableau dynamique.

Dans ce didacticiel, vous apprendrez:

  • Quelle commune types de données abstraits sont intégrés à la bibliothèque standard Python
  • Comment les types de données abstraits les plus courants correspondent à ceux de Python schéma de dénomination
  • Comment mettre des types de données abstraits dans utilisation pratique dans divers algorithmes

Dictionnaires, cartes et tables de hachage

En Python, les dictionnaires (ou dicts pour faire court) sont une structure de données centrale. Les dictionnaires stockent un nombre arbitraire d'objets, chacun identifié par un dictionnaire unique clé.

Les dictionnaires sont aussi souvent appelés Plans, hashmaps, Tables de recherche, ou tableaux associatifs. Ils permettent la recherche, l'insertion et la suppression efficaces de tout objet associé à une clé donnée.

Les annuaires téléphoniques constituent un analogue du monde réel décent pour les objets de dictionnaire. Ils vous permettent de récupérer rapidement les informations (numéro de téléphone) associées à une clé donnée (le nom d’une personne). Au lieu d'avoir à lire un annuaire téléphonique recto verso pour trouver le numéro de quelqu'un, vous pouvez accéder plus ou moins directement à un nom et rechercher les informations associées.

Cette analogie se décompose un peu en ce qui concerne Comment les informations sont organisées pour permettre des recherches rapides. Mais les caractéristiques de performance fondamentales tiennent. Les dictionnaires vous permettent de trouver rapidement les informations associées à une clé donnée.

Les dictionnaires sont l'une des structures de données les plus importantes et les plus fréquemment utilisées en informatique. Alors, comment Python gère-t-il les dictionnaires? Passons en revue les implémentations de dictionnaire disponibles dans Python principal et dans la bibliothèque standard Python.

dict: Votre dictionnaire de référence

Parce que les dictionnaires sont si importants, Python propose une implémentation de dictionnaire robuste qui est intégrée directement dans le langage de base: le dict Type de données.

Python fournit également des sucre syntaxique pour travailler avec des dictionnaires dans vos programmes. Par exemple, la syntaxe de l'expression de dictionnaire avec accolades () et la compréhension du dictionnaire vous permettent de définir facilement de nouveaux objets de dictionnaire:

>>>

>>> annuaire = 
...     "bob": 7387,
...     "Alice": 3719,
...     "jack": 7052,
... 

>>> carrés = X: X * X pour X dans gamme(6)

>>> annuaire[[[["Alice"]
3719

>>> carrés
0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25

Il existe certaines restrictions sur les objets pouvant être utilisés comme clés valides.

Les dictionnaires de Python sont indexés par des clés qui peuvent être de n'importe quel type hachable. UNE hashable objet a une valeur de hachage qui ne change jamais pendant sa durée de vie (voir __hacher__), et il peut être comparé à d'autres objets (voir __eq__). Les objets hachables qui se comparent comme égaux doivent avoir la même valeur de hachage.

Immuable les types comme les chaînes et les nombres sont hachables et fonctionnent bien comme clés de dictionnaire. Vous pouvez aussi utiliser tuple objets en tant que clés de dictionnaire tant qu'ils ne contiennent eux-mêmes que des types hachables.

Dans la plupart des cas d’utilisation, l’implémentation du dictionnaire intégré de Python fera tout ce dont vous avez besoin. Les dictionnaires sont hautement optimisés et sous-tendent de nombreuses parties de la langue. Par exemple, les attributs de classe et les variables dans un cadre de pile sont tous deux stockés en interne dans des dictionnaires.

Les dictionnaires Python sont basés sur une implémentation de table de hachage bien testée et finement réglée qui fournit les caractéristiques de performances que vous attendez: O(1) complexité du temps pour les opérations de recherche, d'insertion, de mise à jour et de suppression dans le cas moyen.

Il y a peu de raisons de ne pas utiliser la norme dict implémentation incluse avec Python. Cependant, il existe des implémentations de dictionnaires tiers spécialisés, telles que les listes à ignorer ou les dictionnaires basés sur des arbres B.

Outre plaine dict objets, la bibliothèque standard de Python comprend également un certain nombre d’implémentations de dictionnaires spécialisés. Ces dictionnaires spécialisés sont tous basés sur la classe de dictionnaire intégrée (et partagent ses caractéristiques de performances), mais incluent également des fonctionnalités de commodité supplémentaires.

Jetons un œil à eux.

collections.OrderedDict: Souvenez-vous de l'ordre d'insertion des clés

Python comprend un dict sous-classe qui se souvient de l'ordre d'insertion des clés qui lui sont ajoutées: collections.OrderedDict.

Bien que la norme dict les instances conservent l'ordre d'insertion des clés dans CPython 3.6 et supérieur, c'était simplement un effet secondaire de l'implémentation CPython et n'était pas défini dans la spécification du langage avant Python 3.7. Par conséquent, si l'ordre des clés est important pour que votre algorithme fonctionne, il est préférable de le communiquer clairement en utilisant explicitement le OrdonnéDict classe:

>>>

>>> importer collections
>>>  = collections.OrdonnéDict(une=1, deux=2, Trois=3)

>>> 
OrderedDict ([('one', 1), ('two', 2), ('three', 3)])

>>> [[[["quatre"] = 4
>>> 
OrderedDict ([('one'1)('two'2)[('one'1)('two'2)[('one'1)('two'2)[('one'1)('two'2)
                                                    ('trois', 3), ('quatre', 4)])

>>> .clés()
odict_keys (['one', 'two', 'three', 'four'])

Jusqu'à Python 3.8, vous ne pouviez pas parcourir les éléments du dictionnaire dans l'ordre inverse en utilisant renversé(). Seulement OrdonnéDict les instances offraient cette fonctionnalité. Même en Python 3.8, dict et OrdonnéDict les objets ne sont pas exactement les mêmes. OrdonnéDict les instances ont un .move_to_end () méthode qui n'est pas disponible en clair dict instance, ainsi qu'un plus personnalisable .popitem () méthode que celle simple dict les instances.

collections.defaultdict: Renvoyer les valeurs par défaut pour les clés manquantes

le defaultdict class est une autre sous-classe de dictionnaire qui accepte un appelable dans son constructeur dont la valeur de retour sera utilisée si une clé demandée est introuvable.

Cela peut vous éviter de taper et rendre vos intentions plus claires par rapport à l'utilisation avoir() ou attraper un KeyError exception dans les dictionnaires réguliers:

>>>

>>> de collections importer defaultdict
>>> jj = defaultdict(liste)

>>> # L'accès à une clé manquante la crée et
>>> # l'initialise en utilisant la fabrique par défaut,
>>> # i.e. list () dans cet exemple:
>>> jj[[[["chiens"].ajouter(«Rufus»)
>>> jj[[[["chiens"].ajouter(«Kathrin»)
>>> jj[[[["chiens"].ajouter(«M. Sniffles»)

>>> jj[[[["chiens"]
['Rufus', 'Kathrin', 'Mr Sniffles']

collections.ChainMap: Rechercher plusieurs dictionnaires sous forme de mappage unique

le collections.ChainMap la structure de données regroupe plusieurs dictionnaires en un seul mappage. Les recherches recherchent les mappages sous-jacents un par un jusqu'à ce qu'une clé soit trouvée. Les insertions, mises à jour et suppressions n'affectent que le premier mappage ajouté à la chaîne:

>>>

>>> de collections importer ChainMap
>>> dict1 = "une": 1, "deux": 2
>>> dict2 = "Trois": 3, "quatre": 4
>>> chaîne = ChainMap(dict1, dict2)

>>> chaîne
ChainMap ('un': 1, 'deux': 2, 'trois': 3, 'quatre': 4)

>>> # ChainMap recherche chaque collection de la chaîne
>>> # de gauche à droite jusqu'à ce qu'il trouve la clé (ou échoue):
>>> chaîne[[[["Trois"]
3
>>> chaîne[[[["une"]
1
>>> chaîne[[[["manquant"]
Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
KeyError: 'manquant'

types.MappingProxyType: Un wrapper pour créer des dictionnaires en lecture seule

MappingProxyType est un wrapper autour d'un dictionnaire standard qui fournit une vue en lecture seule dans les données du dictionnaire encapsulé. Cette classe a été ajoutée dans Python 3.3 et peut être utilisée pour créer des versions proxy immuables de dictionnaires.

MappingProxyType peut être utile si, par exemple, vous souhaitez renvoyer un dictionnaire transportant l’état interne d’une classe ou d’un module tout en décourageant l’accès en écriture à cet objet. En utilisant MappingProxyType vous permet de mettre en place ces restrictions sans avoir à créer au préalable une copie complète du dictionnaire:

>>>

>>> de les types importer MappingProxyType
>>> inscriptible = "une": 1, "deux": 2
>>> lecture seulement = MappingProxyType(inscriptible)

>>> # Le proxy est en lecture seule:
>>> lecture seulement[[[["une"]
1
>>> lecture seulement[[[["une"] = 23
Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
Erreur-type: L'objet 'mappingproxy' ne prend pas en charge l'attribution d'éléments

>>> # Les mises à jour de l'original sont reflétées dans le proxy:
>>> inscriptible[[[["une"] = 42
>>> lecture seulement
mappingproxy ('un': 42, 'deux': 2)

Dictionnaires en Python: résumé

Toutes les implémentations de dictionnaire Python répertoriées dans ce didacticiel sont des implémentations valides intégrées à la bibliothèque standard Python.

Si vous recherchez une recommandation générale sur le type de mappage à utiliser dans vos programmes, je vous oriente vers la fonction intégrée dict Type de données. Il s'agit d'une implémentation de table de hachage polyvalente et optimisée qui est intégrée directement dans le langage de base.

Je vous recommande d'utiliser l'un des autres types de données répertoriés ici uniquement si vous avez des exigences particulières qui vont au-delà de ce qui est fourni par dict.

Toutes les implémentations sont des options valides, mais votre code sera plus clair et plus facile à maintenir s'il repose la plupart du temps sur des dictionnaires Python standard.

Structures de données de tableau

Une tableau est une structure de données fondamentale disponible dans la plupart des langages de programmation, et elle a un large éventail d'utilisations à travers différents algorithmes.

Dans cette section, vous examinerez les implémentations de tableaux en Python qui utilisent uniquement les fonctionnalités principales du langage ou les fonctionnalités incluses dans la bibliothèque standard Python. Vous verrez les forces et les faiblesses de chaque approche afin que vous puissiez décider quelle mise en œuvre convient à votre cas d'utilisation.

Mais avant de nous lancer, abordons d'abord certaines des bases. Comment fonctionnent les tableaux et à quoi servent-ils? Les tableaux se composent d'enregistrements de données de taille fixe qui permettent à chaque élément d'être localisé efficacement en fonction de son index:

Représentation visuelle d'un tableau

Étant donné que les tableaux stockent des informations dans des blocs de mémoire adjacents, ils sont considérés contigu structures de données (par opposition à lié structures de données comme les listes liées, par exemple).

Une analogie réelle pour une structure de données de tableau est un parking. Vous pouvez regarder le parking dans son ensemble et le traiter comme un seul objet, mais à l'intérieur du parking il y a des places de parking indexées par un numéro unique. Les places de stationnement sont des conteneurs pour véhicules – chaque place de stationnement peut être vide ou avoir une voiture, une moto ou un autre véhicule garé dessus.

Mais tous les parkings ne sont pas identiques. Certains parkings peuvent être limités à un seul type de véhicule. Par exemple, un parking pour autocaravane ne permettrait pas de garer les vélos dessus. Un parking restreint correspond à un tapé structure de données de tableau qui autorise uniquement les éléments contenant le même type de données.

En termes de performances, il est très rapide de rechercher un élément contenu dans un tableau en fonction de l’index de l’élément. Une mise en œuvre correcte du tableau garantit une constante O(1) temps d'accès pour ce cas.

Python inclut plusieurs structures de données de type tableau dans sa bibliothèque standard qui ont chacune des caractéristiques légèrement différentes. Nous allons jeter un coup d'oeil.

liste: Tableaux dynamiques mutables

Les listes font partie du langage Python principal. Malgré leur nom, les listes de Python sont implémentées comme tableaux dynamiques Dans les coulisses.

Cela signifie qu'une liste permet d'ajouter ou de supprimer des éléments, et la liste ajustera automatiquement le magasin de stockage qui contient ces éléments en allouant ou en libérant de la mémoire.

Les listes Python peuvent contenir des éléments arbitraires – tout est un objet en Python, y compris les fonctions. Par conséquent, vous pouvez mélanger et assortir différents types de types de données et les stocker tous dans une seule liste.

Cela peut être une fonctionnalité puissante, mais l'inconvénient est que la prise en charge de plusieurs types de données en même temps signifie que les données sont généralement moins compactées. En conséquence, toute la structure prend plus de place:

>>>

>>> arr = [[[["une", "deux", "Trois"]
>>> arr[[[[0]
'une'

>>> # Les listes ont une belle reproduction:
>>> arr
['one', 'two', 'three']

>>> # Les listes sont modifiables:
>>> arr[[[[1] = "Bonjour"
>>> arr
['one', 'hello', 'three']

>>> del arr[[[[1]
>>> arr
['one', 'three']

>>> # Les listes peuvent contenir des types de données arbitraires:
>>> arr.ajouter(23)
>>> arr
['one', 'three', 23]

tuple: Conteneurs immuables

Tout comme les listes, les tuples font partie du langage de base Python. Contrairement aux listes, cependant, Python tuple les objets sont immuables. Cela signifie que les éléments ne peuvent pas être ajoutés ou supprimés de manière dynamique. Tous les éléments d'un tuple doivent être définis au moment de la création.

Les tuples sont une autre structure de données qui peut contenir des éléments de types de données arbitraires. Cette flexibilité est puissante, mais encore une fois, cela signifie également que les données sont moins compactées qu'elles ne le seraient dans un tableau typé:

>>>

>>> arr = ("une", "deux", "Trois")
>>> arr[[[[0]
'une'

>>> # Tuples ont une belle repr:
>>> arr
('un deux trois')

>>> # Les tuples sont immuables:
>>> arr[[[[1] = "Bonjour"
Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
Erreur-type: L'objet 'tuple' ne prend pas en charge l'attribution d'éléments

>>> del arr[[[[1]
Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
Erreur-type: L'objet 'tuple' ne prend pas en charge la suppression d'éléments

>>> # Les tuples peuvent contenir des types de données arbitraires:
>>> # (L'ajout d'éléments crée une copie du tuple)
>>> arr + (23,)
("un", "deux", "trois", 23)

array.array: Tableaux typés de base

Python tableau Le module fournit un stockage efficace de l'espace des types de données de base de style C tels que les octets, les entiers 32 bits, les nombres à virgule flottante, etc.

Tableaux créés avec le array.array class sont modifiables et se comportent de la même manière que les listes, sauf une différence importante: ils sont tableaux typés limité à un seul type de données.

En raison de cette contrainte, array.array les objets avec de nombreux éléments sont plus efficaces en termes d'espace que les listes et les tuples. Les éléments qui y sont stockés sont étroitement emballés, ce qui peut être utile si vous avez besoin de stocker de nombreux éléments du même type.

En outre, les tableaux prennent en charge plusieurs des mêmes méthodes que les listes régulières, et vous pourrez peut-être les utiliser comme remplacement instantané sans nécessiter d'autres modifications du code de votre application.

>>>

>>> importer tableau
>>> arr = tableau.tableau("F", (1.0, 1,5, 2.0, 2,5))
>>> arr[[[[1]
1,5

>>> # Les tableaux ont une belle reproduction:
>>> arr
tableau ('f', [1.0, 1.5, 2.0, 2.5])

>>> # Les tableaux sont mutables:
>>> arr[[[[1] = 23,0
>>> arr
tableau ('f', [1.0, 23.0, 2.0, 2.5])

>>> del arr[[[[1]
>>> arr
tableau ('f', [1.0, 2.0, 2.5])

>>> arr.ajouter(42,0)
>>> arr
tableau ('f', [1.0, 2.0, 2.5, 42.0])

>>> # Les tableaux sont "typés":
>>> arr[[[[1] = "Bonjour"
Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
Erreur-type: doit être un nombre réel, pas str

str: Tableaux immuables de caractères Unicode

Utilisations de Python 3.x str objets pour stocker des données textuelles sous forme de séquences immuables de caractères Unicode. En pratique, cela signifie un str est un tableau immuable de caractères. Curieusement, c'est aussi un récursif structure de données: chaque caractère d'une chaîne est lui-même un str objet de longueur 1.

Les objets String sont peu encombrants car ils sont très compacts et se spécialisent dans un seul type de données. Si vous stockez du texte Unicode, vous devez utiliser une chaîne.

Étant donné que les chaînes sont immuables en Python, la modification d'une chaîne nécessite la création d'une copie modifiée. L'équivalent le plus proche d'une chaîne mutable est le stockage de caractères individuels dans une liste:

>>>

>>> arr = "a B c d"
>>> arr[[[[1]
«b»

>>> arr
'a B c d'

>>> # Les chaînes sont immuables:
>>> arr[[[[1] = "e"
Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
Erreur-type: L'objet 'str' ne prend pas en charge l'attribution d'éléments

>>> del arr[[[[1]
Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
Erreur-type: L'objet 'str' ne prend pas en charge la suppression d'éléments

>>> # Les chaînes peuvent être décompressées dans une liste pour
>>> # obtenir une représentation mutable:
>>> liste("a B c d")
['a', 'b', 'c', 'd']
>>> "".joindre(liste("a B c d"))
'a B c d'

>>> # Les chaînes sont des structures de données récursives:
>>> type("abc")
""
>>> type("abc"[[[[0])
""

octets: Tableaux immuables d'octets simples

octets les objets sont des séquences immuables d'octets simples ou des entiers dans la plage 0 ≤ X ≤ 255. Conceptuellement, octets les objets sont similaires à str objets, et vous pouvez également les considérer comme des tableaux d'octets immuables.

Comme des cordes, octets ont leur propre syntaxe littérale pour créer des objets et sont peu encombrants. octets les objets sont immuables, mais contrairement aux chaînes, il existe un type de données de tableau d'octets mutable dédié appelé bytearray qu'ils peuvent être décompressés dans:

>>>

>>> arr = octets((0, 1, 2, 3))
>>> arr[[[[1]
1

>>> # Les littéraux d'octets ont leur propre syntaxe:
>>> arr
b ' x00  x01  x02  x03'
>>> arr = b" x00  x01  x02  x03"

>>> # Seuls les octets valides sont autorisés:
>>> octets((0, 300))
Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
ValueError: les octets doivent être compris dans la plage (0, 256)

>>> # Les octets sont immuables:
>>> arr[[[[1] = 23
Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
Erreur-type: L'objet 'bytes' ne prend pas en charge l'affectation d'élément

>>> del arr[[[[1]
Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
Erreur-type: L'objet 'bytes' ne prend pas en charge la suppression d'éléments

bytearray: Tableaux mutables d'octets simples

le bytearray type est une séquence mutable d'entiers compris entre 0 ≤ X ≤ 255. Le bytearray l'objet est étroitement lié à la octets objet, la principale différence étant qu'un bytearray peuvent être modifiés librement: vous pouvez écraser des éléments, supprimer des éléments existants ou en ajouter de nouveaux. le bytearray l'objet grandira et rétrécira en conséquence.

UNE bytearray peut être reconverti en immuable octets objets, mais cela implique de copier les données stockées dans leur intégralité – une opération lente prenant O(n) temps:

>>>

>>> arr = bytearray((0, 1, 2, 3))
>>> arr[[[[1]
1

>>> # Le bytearray repr:
>>> arr
bytearray (b ' x00  x01  x02  x03')

>>> # Les Bytearrays sont mutables:
>>> arr[[[[1] = 23
>>> arr
bytearray (b ' x00  x17  x02  x03')

>>> arr[[[[1]
23

>>> # Les Bytearrays peuvent grossir et rétrécir:
>>> del arr[[[[1]
>>> arr
bytearray (b ' x00  x02  x03')

>>> arr.ajouter(42)
>>> arr
bytearray (b ' x00  x02  x03 *')

>>> # Bytearrays ne peut contenir que des `octets`
>>> # (nombres entiers compris entre 0 <= x <= 255)
>>> arr[[[[1] = "Bonjour"
Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
Erreur-type: L'objet 'str' ne peut pas être interprété comme un entier

>>> arr[[[[1] = 300
Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
ValueError: l'octet doit être compris dans la plage (0, 256)

>>> # Les Bytearrays peuvent être reconvertis en objets bytes:
>>> # (Cela copiera les données)
>>> octets(arr)
b ' x00  x02  x03 *'

Tableaux en Python: résumé

Il existe un certain nombre de structures de données intégrées parmi lesquelles vous pouvez choisir pour implémenter des tableaux en Python. Dans cette section, vous vous êtes concentré sur les principales fonctionnalités du langage et les structures de données incluses dans la bibliothèque standard.

Si vous souhaitez aller au-delà de la bibliothèque standard Python, des packages tiers tels que NumPy et pandas offrent une large gamme d'implémentations de baies rapides pour le calcul scientifique et la science des données.

Si vous souhaitez vous limiter aux structures de données de tableau incluses avec Python, voici quelques instructions:

  • Si vous avez besoin de stocker des objets arbitraires, potentiellement avec des types de données mixtes, utilisez un liste ou un tuple, selon que vous souhaitez ou non une structure de données immuable.

  • Si vous avez des données numériques (entières ou à virgule flottante) et que la compression et les performances sont importantes, essayez array.array.

  • Si vous avez des données textuelles représentées sous forme de caractères Unicode, utilisez la fonction intégrée de Python str. Si vous avez besoin d'une structure de données de type chaîne mutable, utilisez un liste de personnages.

  • Si vous souhaitez stocker un bloc d'octets contigu, utilisez la propriété immuable octets type ou un bytearray si vous avez besoin d'une structure de données mutable.

Dans la plupart des cas, j'aime commencer par un simple liste. Je ne me spécialiserai plus tard que si les performances ou l'espace de stockage deviennent un problème. La plupart du temps, en utilisant une structure de données de tableau à usage général comme liste vous offre la vitesse de développement la plus rapide et la plus grande commodité de programmation.

J’ai trouvé que c’est généralement beaucoup plus important au début que d’essayer de tirer la moindre goutte de performance dès le début.

Enregistrements, structures et objets de transfert de données

Par rapport aux tableaux, record les structures de données fournissent un nombre fixe de champs. Chaque champ peut avoir un nom et peut également avoir un type différent.

Dans cette section, vous découvrirez comment implémenter des enregistrements, des structures et des objets de données anciens en Python en utilisant uniquement des types de données et des classes intégrés de la bibliothèque standard.

Python propose plusieurs types de données que vous pouvez utiliser pour implémenter des enregistrements, des structures et des objets de transfert de données. Dans cette section, vous aurez un aperçu rapide de chaque mise en œuvre et de ses caractéristiques uniques. À la fin, vous trouverez un résumé et un guide de prise de décision qui vous aideront à faire vos propres choix.

Très bien, commençons!

dict: Objets de données simples

Comme mentionné précédemment, les dictionnaires Python stockent un nombre arbitraire d'objets, chacun identifié par une clé unique. Les dictionnaires sont aussi souvent appelés Plans ou tableaux associatifs et permettent une recherche, une insertion et une suppression efficaces de tout objet associé à une clé donnée.

L'utilisation de dictionnaires comme type de données d'enregistrement ou objet de données en Python est possible. Les dictionnaires sont faciles à créer en Python car ils ont leur propre sucre syntaxique intégré au langage sous la forme de littéraux du dictionnaire. La syntaxe du dictionnaire est concise et assez pratique à taper.

Les objets de données créés à l'aide de dictionnaires sont modifiables et il existe peu de protection contre les noms de champs mal orthographiés, car les champs peuvent être ajoutés et supprimés librement à tout moment. Ces deux propriétés peuvent introduire des bogues surprenants, et il y a toujours un compromis à faire entre la commodité et la résilience aux erreurs:

>>>

>>> voiture1 = 
...     "Couleur": "rouge",
...     "kilométrage": 3812,4,
...     "automatique": Vrai,
... 
>>> voiture2 = 
...     "Couleur": "bleu",
...     "kilométrage": 40231,
...     "automatique": Faux,
... 

>>> # Les dictionnaires ont une belle reproduction:
>>> voiture2
'color': 'blue', 'automatic': False, 'mileage': 40231

>>> # Obtenez le kilométrage:
>>> voiture2[[[["kilométrage"]
40231

>>> # Les dictionnaires sont mutables:
>>> voiture2[[[["kilométrage"] = 12
>>> voiture2[[[["pare-brise"] = "cassé"
>>> voiture2
'pare-brise': 'cassé', 'couleur': 'bleu',
    'automatique': Faux, 'kilométrage': 12

>>> # Aucune protection contre les noms de champs incorrects,
>>> # ou champs manquants / supplémentaires:
>>> voiture3 = 
...     "colr": "vert",
...     "automatique": Faux,
...     "pare-brise": "cassé",
... 

tuple: Groupes d'objets immuables

Les tuples de Python sont une structure de données simple pour regrouper des objets arbitraires. Les tuples sont immuables: ils ne peuvent pas être modifiés une fois qu'ils ont été créés.

En termes de performances, les tuples prennent un peu moins de mémoire que les listes en CPython, et ils sont également plus rapides à construire.

Comme vous pouvez le voir dans le désassemblage du bytecode ci-dessous, la construction d'une constante de tuple prend un seul LOAD_CONST opcode, tandis que la construction d'un objet liste avec le même contenu nécessite plusieurs opérations supplémentaires:

>>>

>>> importer dis
>>> dis.dis(compiler("(23, 'a', 'b', 'c')", "", "eval"))
                        0 LOAD_CONST 4 ((23, "a", "b", "c"))
                        3 RETURN_VALUE

>>> dis.dis(compiler("[23, 'a', 'b', 'c']", "", "eval"))
                        0 LOAD_CONST 0 (23)
                        3 LOAD_CONST 1 ('a')
                        6 LOAD_CONST 2 ('b')
                        9 LOAD_CONST 3 ('c')
                    12 BUILD_LIST 4
                    15 RETURN_VALUE

Cependant, vous ne devez pas trop insister sur ces différences. Dans la pratique, la différence de performances sera souvent négligeable, et essayer d'extraire des performances supplémentaires d'un programme en passant des listes aux tuples sera probablement la mauvaise approche.

Un inconvénient potentiel des tuples simples est que les données que vous y stockez ne peuvent être extraites qu'en y accédant via des index entiers. Vous ne pouvez pas donner de noms aux propriétés individuelles stockées dans un tuple. Cela peut avoir un impact sur la lisibilité du code.

De plus, un tuple est toujours une structure ad hoc: il est difficile de garantir que deux tuples contiennent le même nombre de champs et les mêmes propriétés stockées.

Cela facilite l'introduction de bogues glissants, tels que le mélange de l'ordre des champs. Par conséquent, je vous recommande de garder le nombre de champs stockés dans un tuple aussi bas que possible:

>>>

>>> # Champs: couleur, kilométrage, automatique
>>> voiture1 = ("rouge", 3812,4, Vrai)
>>> voiture2 = ("bleu", 40231,0, Faux)

>>> # Les instances Tuple ont une belle repr:
>>> voiture1
('rouge', 3812,4, vrai)
>>> voiture2
('bleu', 40231.0, Faux)

>>> # Obtenez le kilométrage:
>>> voiture2[[[[1]
40231,0

>>> # Les tuples sont immuables:
>>> voiture2[[[[1] = 12
Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
Erreur-type: L'objet 'tuple' ne prend pas en charge l'attribution d'éléments

>>> # Aucune protection contre les champs manquants ou supplémentaires
>>> # ou une mauvaise commande:
>>> voiture3 = (3431,5, "vert", Vrai, "argent")

Rédigez une classe personnalisée: plus de travail, plus de contrôle

Des classes vous permettent de définir des plans réutilisables pour les objets de données afin de garantir que chaque objet fournit le même ensemble de champs.

L'utilisation de classes Python régulières comme types de données d'enregistrement est possible, mais il faut également un travail manuel pour obtenir les fonctionnalités pratiques d'autres implémentations. Par exemple, l'ajout de nouveaux champs au __init__ Le constructeur est verbeux et prend du temps.

De plus, la représentation sous forme de chaîne par défaut pour les objets instanciés à partir de classes personnalisées n’est pas très utile. Pour résoudre ce problème, vous devrez peut-être ajouter le vôtre __repr__ méthode, qui est encore une fois assez détaillée et doit être mise à jour chaque fois que vous ajoutez un nouveau champ.

Les champs stockés sur les classes sont modifiables, et de nouveaux champs peuvent être ajoutés librement, que vous aimerez ou non. Il est possible de fournir un meilleur contrôle d’accès et de créer des champs en lecture seule à l’aide de @propriété décorateur, mais encore une fois, cela nécessite d'écrire plus de code de colle.

L'écriture d'une classe personnalisée est une excellente option chaque fois que vous souhaitez ajouter une logique métier et un comportement à vos objets d'enregistrement à l'aide de méthodes. Cependant, cela signifie que ces objets ne sont techniquement plus des objets de données simples:

>>>

>>> classe Voiture:
...     def __init__(soi, Couleur, kilométrage, automatique):
...         soi.Couleur = Couleur
...         soi.kilométrage = kilométrage
...         soi.automatique = automatique
...
>>> voiture1 = Voiture("rouge", 3812,4, Vrai)
>>> voiture2 = Voiture("bleu", 40231,0, Faux)

>>> # Obtenez le kilométrage:
>>> voiture2.kilométrage
40231,0

>>> # Les classes sont modifiables:
>>> voiture2.kilométrage = 12
>>> voiture2.pare-brise = "cassé"

>>> # La représentation sous forme de chaîne n'est pas très utile
>>> # (doit ajouter une méthode __repr__ écrite manuellement):
>>> voiture1

dataclasses.dataclass: Classes de données Python 3.7+

Les classes de données sont disponibles dans Python 3.7 et supérieur. Ils offrent une excellente alternative à la définition de vos propres classes de stockage de données à partir de zéro.

En écrivant une classe de données au lieu d'une classe Python simple, vos instances d'objet obtiennent quelques fonctionnalités utiles prêtes à l'emploi qui vous permettront d'économiser du travail de saisie et d'implémentation manuelle:

  • La syntaxe de définition des variables d'instance est plus courte, car vous n'avez pas besoin de mettre en œuvre la .__ init __ () méthode.
  • Les instances de votre classe de données obtiennent automatiquement une belle représentation sous forme de chaîne via un .__ repr __ () méthode.
  • Les variables d'instance acceptent les annotations de type, ce qui rend votre classe de données auto-documentée dans une certaine mesure. Gardez à l'esprit que les annotations de type ne sont que des conseils qui ne sont pas appliqués sans un outil de vérification de type distinct.

Les classes de données sont généralement créées à l'aide du @dataclass décorateur, comme vous le verrez dans l'exemple de code ci-dessous:

>>>

>>> de classes de données importer dataclass
>>> @dataclass
... classe Voiture:
...     Couleur: str
...     kilométrage: flotte
...     automatique: booléen
...
>>> voiture1 = Voiture("rouge", 3812,4, Vrai)

>>> # Les instances ont une belle reproduction:
>>> voiture1
Voiture (couleur = 'rouge', kilométrage = 3812,4, automatique = vrai)

>>> # Accès aux champs:
>>> voiture1.kilométrage
3812,4

>>> # Les champs sont mutables:
>>> voiture1.kilométrage = 12
>>> voiture1.pare-brise = "cassé"

>>> # Les annotations de type ne sont pas appliquées sans
>>> # un outil de vérification de type distinct comme mypy:
>>> Voiture("rouge", "NOT_A_FLOAT", 99)
Voiture (couleur = 'rouge', kilométrage = 'NOT_A_FLOAT', automatique = 99)

Pour en savoir plus sur les classes de données Python, consultez le Guide ultime des classes de données en Python 3.7.

collections.namedtuple: Objets de données pratiques

le namedtuple La classe disponible dans Python 2.6+ fournit une extension de la tuple Type de données. Similaire à la définition d'une classe personnalisée, en utilisant namedtuple vous permet de définir des plans réutilisables pour vos enregistrements qui garantissent que les noms de champ corrects sont utilisés.

namedtuple les objets sont immuables, tout comme les tuples normaux. Cela signifie que vous ne pouvez pas ajouter de nouveaux champs ni modifier les champs existants après le namedtuple l'instance est créée.

Par ailleurs, namedtuple les objets sont, bien. . . tuples nommés. Chaque objet qui y est stocké est accessible via un identifiant unique. Cela vous évite d'avoir à vous souvenir des index entiers ou à recourir à des solutions de contournement comme la définition constantes entières comme mnémoniques pour vos index.

namedtuple Les objets sont implémentés en tant que classes Python normales en interne. En ce qui concerne l'utilisation de la mémoire, ils sont également meilleurs que les classes ordinaires et tout aussi efficaces en mémoire que les tuples classiques:

>>>

>>> de collections importer namedtuple
>>> de sys importer getizeof

>>> p1 = namedtuple("Point", "x y z") (1, 2, 3)
>>> p2 = (1, 2, 3)

>>> getizeof(p1)
64
>>> getizeof(p2)
64

namedtuple Les objets peuvent être un moyen facile de nettoyer votre code et de le rendre plus lisible en appliquant une meilleure structure pour vos données.

Je trouve que passer de types de données ad-hoc comme les dictionnaires avec un format fixe à namedtuple objets m'aide à exprimer plus clairement l'intention de mon code. Souvent, lorsque j'applique ce refactoring, je trouve comme par magie une meilleure solution pour le problème auquel je suis confronté.

En utilisant namedtuple les objets sur des tuples et des dictionnaires normaux (non structurés) peuvent également faciliter la vie de vos collègues en rendant les données transmises autodocumentées, au moins dans une certaine mesure:

>>>

>>> de collections importer namedtuple
>>> Voiture = namedtuple("Voiture" , "kilométrage couleur automatique")
>>> voiture1 = Voiture("rouge", 3812,4, Vrai)

>>> # Les instances ont une belle reproduction:
>>> voiture1
Voiture (couleur = "rouge", kilométrage = 3812,4, automatique = vrai)

>>> # Accès aux champs:
>>> voiture1.kilométrage
3812,4

>>> # Les champs sont immuables:
>>> voiture1.kilométrage = 12
Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
AttributeError: impossible de définir l'attribut

>>> voiture1.pare-brise = "cassé"
Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
AttributeError: L'objet 'Car' n'a pas d'attribut 'windshield'

typing.NamedTuple: Amélioration des tuples nommés

Ajouté dans Python 3.6, typing.NamedTuple est le plus jeune frère du namedtuple classe dans le collections module. C'est très similaire à namedtuple, la principale différence étant une syntaxe mise à jour pour définir de nouveaux types d'enregistrement et une prise en charge supplémentaire des indicateurs de type.

Veuillez noter que les annotations de type ne sont pas appliquées sans un outil de vérification de type distinct comme mypy. Mais même sans support d'outil, ils peuvent fournir des conseils utiles pour d'autres programmeurs (ou être terriblement déroutants si les indices de type deviennent obsolètes):

>>>

>>> de dactylographie importer NamedTuple

>>> classe Voiture(NamedTuple):
...     Couleur: str
...     kilométrage: flotte
...     automatique: booléen

>>> voiture1 = Voiture("rouge", 3812,4, Vrai)

>>> # Les instances ont une belle reproduction:
>>> voiture1
Voiture (couleur = 'rouge', kilométrage = 3812,4, automatique = vrai)

>>> # Accès aux champs:
>>> voiture1.kilométrage
3812,4

>>> # Les champs sont immuables:
>>> voiture1.kilométrage = 12
Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
AttributeError: impossible de définir l'attribut

>>> voiture1.pare-brise = "cassé"
Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
AttributeError: L'objet 'Car' n'a pas d'attribut 'windshield'

>>> # Les annotations de type ne sont pas appliquées sans
>>> # un outil de vérification de type distinct comme mypy:
>>> Voiture("rouge", "NOT_A_FLOAT", 99)
Voiture (couleur = 'rouge', kilométrage = 'NOT_A_FLOAT', automatique = 99)

struct.Struct: Structures C sérialisées

le struct.Struct classe convertit entre les valeurs Python et les structures C sérialisées en Python octets objets. For example, it can be used to handle binary data stored in files or coming in from network connections.

Structs are defined using a mini language based on format strings that allows you to define the arrangement of various C data types like char, int, et longue as well as their unsigned variants.

Serialized structs are seldom used to represent data objects meant to be handled purely inside Python code. They’re intended primarily as a data exchange format rather than as a way of holding data in memory that’s only used by Python code.

In some cases, packing primitive data into structs may use less memory than keeping it in other data types. However, in most cases that would be quite an advanced (and probably unnecessary) optimization:

>>>

>>> de struct importer Struct
>>> MyStruct = Struct("i?f")
>>> Les données = MyStruct.pack(23, False, 42.0)

>>> # All you get is a blob of data:
>>> Les données
b'x17x00x00x00x00x00x00x00x00x00(B'

>>> # Data blobs can be unpacked again:
>>> MyStruct.unpack(Les données)
(23, False, 42.0)

types.SimpleNamespace: Fancy Attribute Access

Here’s one more slightly obscure choice for implementing data objects in Python: types.SimpleNamespace. This class was added in Python 3.3 and provides attribute access to its namespace.

This means SimpleNamespace instances expose all of their keys as class attributes. You can use obj.key dotted attribute access instead of the obj['key'] square-bracket indexing syntax that’s used by regular dicts. All instances also include a meaningful __repr__ by default.

As its name proclaims, SimpleNamespace is simple! It’s basically a dictionary that allows attribute access and prints nicely. Attributes can be added, modified, and deleted freely:

>>>

>>> de types importer SimpleNamespace
>>> car1 = SimpleNamespace(Couleur="red", mileage=3812.4, automatic=True)

>>> # The default repr:
>>> car1
namespace(automatic=True, color='red', mileage=3812.4)

>>> # Instances support attribute access and are mutable:
>>> car1.mileage = 12
>>> car1.windshield = "broken"
>>> del car1.automatic
>>> car1
namespace(color='red', mileage=12, windshield='broken')

Records, Structs, and Data Objects in Python: Summary

As you’ve seen, there’s quite a number of different options for implementing records or data objects. Which type should you use for data objects in Python? Generally your decision will depend on your use case:

  • If you have only a few fields, then using a plain tuple object may be okay if the field order is easy to remember or field names are superfluous. For example, think of an (x, y, z) point in three-dimensional space.

  • If you need immutable fields, then plain tuples, collections.namedtuple, et typing.NamedTuple are all good options.

  • If you need to lock down field names to avoid typos, then collections.namedtuple et typing.NamedTuple are your friends.

  • If you want to keep things simple, then a plain dictionary object might be a good choice due to the convenient syntax that closely resembles JSON.

  • If you need full control over your data structure, then it’s time to write a custom class with @property setters and getters.

  • If you need to add behavior (methods) to the object, then you should write a custom class, either from scratch or by extending collections.namedtuple ou typing.NamedTuple.

  • If you need to pack data tightly to serialize it to disk or to send it over the network, then it’s time to read up on struct.Struct because this is a great use case for it!

If you’re looking for a safe default choice, then my general recommendation for implementing a plain record, struct, or data object in Python would be to use collections.namedtuple in Python 2.x and its younger sibling, typing.NamedTuple in Python 3.

Sets and Multisets

In this section, you’ll see how to implement mutable and immutable set and multiset (bag) data structures in Python using built-in data types and classes from the standard library.

UNE set is an unordered collection of objects that doesn’t allow duplicate elements. Typically, sets are used to quickly test a value for membership in the set, to insert or delete new values from a set, and to compute the union or intersection of two sets.

In a proper set implementation, membership tests are expected to run in fast O(1) time. Union, intersection, difference, and subset operations should take O(n) time on average. The set implementations included in Python’s standard library follow these performance characteristics.

Just like dictionaries, sets get special treatment in Python and have some syntactic sugar that makes them easy to create. For example, the curly-brace set expression syntax and set comprehensions allow you to conveniently define new set instances:

vowels = "a", "e", "i", "o", "u"
squares = X * X pour X dans range(dix)

But be careful: To create an empty set you’ll need to call the set() constructor. Using empty curly-braces () is ambiguous and will create an empty dictionary instead.

Python and its standard library provide several set implementations. Let’s have a look at them.

set: Your Go-To Set

le set type is the built-in set implementation in Python. It’s mutable and allows for the dynamic insertion and deletion of elements.

Python’s sets are backed by the dict data type and share the same performance characteristics. Any hashable object can be stored in a set:

>>>

>>> vowels = "a", "e", "i", "o", "u"
>>> "e" dans vowels
True

>>> letters = set("alice")
>>> letters.intersection(vowels)
'a', 'e', 'i'

>>> vowels.ajouter("x")
>>> vowels
'i', 'a', 'u', 'o', 'x', 'e'

>>> len(vowels)
6

frozenset: Immutable Sets

le frozenset class implements an immutable version of set that can’t be changed after it’s been constructed.

frozenset objects are static and allow only query operations on their elements, not inserts or deletions. Because frozenset objects are static and hashable, they can be used as dictionary keys or as elements of another set, something that isn’t possible with regular (mutable) set objects:

>>>

>>> vowels = frozenset("a", "e", "i", "o", "u")
>>> vowels.ajouter("p")
Traceback (most recent call last):
  File "", line 1, in 
AttributeError: 'frozenset' object has no attribute 'add'

>>> # Frozensets are hashable and can
>>> # be used as dictionary keys:
>>> d =  frozenset(1, 2, 3): "hello" 
>>> d[[[[frozenset(1, 2, 3)]
'hello'

collections.Counter: Multisets

le collections.Counter class in the Python standard library implements a multiset, or bag, type that allows elements in the set to have more than one occurrence.

This is useful if you need to keep track of not only if an element is part of a set, but also how many times it’s included in the set:

>>>

>>> de collections importer Counter
>>> inventory = Counter()

>>> loot = "sword": 1, "bread": 3
>>> inventory.update(loot)
>>> inventory
Counter('bread': 3, 'sword': 1)

>>> more_loot = "sword": 1, "apple": 1
>>> inventory.update(more_loot)
>>> inventory
Counter('bread': 3, 'sword': 2, 'apple': 1)

One caveat for the Counter class is that you’ll want to be careful when counting the number of elements in a Counter object. Calling len() returns the number of unique elements in the multiset, whereas the total number of elements can be retrieved using sum():

>>>

>>> len(inventory)
3  # Unique elements

>>> sum(inventory.values())
6  # Total no. of elements

Sets and Multisets in Python: Summary

Sets are another useful and commonly used data structure included with Python and its standard library. Here are a few guidelines for deciding which one to use:

  • If you need a mutable set, then use the built-in set type.
  • If you need hashable objects that can be used as dictionary or set keys, then use a frozenset.
  • If you need a multiset, or bag, data structure, then use collections.Counter.

Stacks (LIFOs)

UNE stack is a collection of objects that supports fast Last-In/First-Out (LIFO) semantics for inserts and deletes. Unlike lists or arrays, stacks typically don’t allow for random access to the objects they contain. The insert and delete operations are also often called pousser et pop.

A useful real-world analogy for a stack data structure is a stack of plates. New plates are added to the top of the stack, and because the plates are precious and heavy, only the topmost plate can be moved. In other words, the last plate on the stack must be the first one removed (LIFO). To reach the plates that are lower down in the stack, the topmost plates must be removed one by one.

Performance-wise, a proper stack implementation is expected to take O(1) time for insert and delete operations.

Stacks have a wide range of uses in algorithms. For example, they’re used in language parsing as well as runtime memory management, which relies on a call stack. A short and beautiful algorithm using a stack is depth-first search (DFS) on a tree or graph data structure.

Python ships with several stack implementations that each have slightly different characteristics. Let’s take a look at them and compare their characteristics.

liste: Simple, Built-In Stacks

Python’s built-in liste type makes a decent stack data structure as it supports push and pop operations in amortized O(1) time.

Python’s lists are implemented as dynamic arrays internally, which means they occasionally need to resize the storage space for elements stored in them when elements are added or removed. The list over-allocates its backing storage so that not every push or pop requires resizing. As a result, you get an amortized O(1) time complexity for these operations.

The downside is that this makes their performance less consistent than the stable O(1) inserts and deletes provided by a linked list–based implementation (as you’ll see below with collections.deque). On the other hand, lists do provide fast O(1) time random access to elements on the stack, and this can be an added benefit.

There’s an important performance caveat that you should be aware of when using lists as stacks: To get the amortized O(1) performance for inserts and deletes, new items must be added to the end of the list with the append() method and removed again from the end using pop(). For optimum performance, stacks based on Python lists should grow towards higher indexes and shrink towards lower ones.

Adding and removing from the front is much slower and takes O(n) time, as the existing elements must be shifted around to make room for the new element. This is a performance antipattern that you should avoid as much as possible:

>>>

>>> s = []
>>> s.append("eat")
>>> s.append("sleep")
>>> s.append("code")

>>> s
['eat', 'sleep', 'code']

>>> s.pop()
'code'
>>> s.pop()
'sleep'
>>> s.pop()
'eat'

>>> s.pop()
Traceback (most recent call last):
  File "", line 1, in 
IndexError: pop from empty list

collections.deque: Fast and Robust Stacks

le deque class implements a double-ended queue that supports adding and removing elements from either end in O(1) time (non-amortized). Because deques support adding and removing elements from either end equally well, they can serve both as queues and as stacks.

Python’s deque objects are implemented as doubly-linked lists, which gives them excellent and consistent performance for inserting and deleting elements but poor O(n) performance for randomly accessing elements in the middle of a stack.

Overall, collections.deque is a great choice if you’re looking for a stack data structure in Python’s standard library that has the performance characteristics of a linked-list implementation:

>>>

>>> de collections importer deque
>>> s = deque()
>>> s.append("eat")
>>> s.append("sleep")
>>> s.append("code")

>>> s
deque(['eat', 'sleep', 'code'])

>>> s.pop()
'code'
>>> s.pop()
'sleep'
>>> s.pop()
'eat'

>>> s.pop()
Traceback (most recent call last):
  File "", line 1, in 
IndexError: pop from an empty deque

queue.LifoQueue: Locking Semantics for Parallel Computing

le LifoQueue stack implementation in the Python standard library is synchronized and provides locking semantics to support multiple concurrent producers and consumers.

Besides LifoQueue, les queue module contains several other classes that implement multi-producer, multi-consumer queues that are useful for parallel computing.

Depending on your use case, the locking semantics might be helpful, or they might just incur unneeded overhead. In this case, you’d be better off using a liste ou un deque as a general-purpose stack:

>>>

>>> de queue importer LifoQueue
>>> s = LifoQueue()
>>> s.put("eat")
>>> s.put("sleep")
>>> s.put("code")

>>> s


>>> s.avoir()
'code'
>>> s.avoir()
'sleep'
>>> s.avoir()
'eat'

>>> s.get_nowait()
queue.Empty

>>> s.avoir()  # Blocks/waits forever...

Stack Implementations in Python: Summary

As you’ve seen, Python ships with several implementations for a stack data structure. All of them have slightly different characteristics as well as performance and usage trade-offs.

If you’re not looking for parallel processing support (or if you don’t want to handle locking and unlocking manually), then your choice comes down to the built-in liste type or collections.deque. The difference lies in the data structure used behind the scenes and overall ease of use.

liste is backed by a dynamic array, which makes it great for fast random access but requires occasional resizing when elements are added or removed.

The list over-allocates its backing storage so that not every push or pop requires resizing, and you get an amortized O(1) time complexity for these operations. But you do need to be careful to only insert and remove items using append() et pop(). Otherwise, performance slows down to O(n).

collections.deque is backed by a doubly-linked list, which optimizes appends and deletes at both ends and provides consistent O(1) performance for these operations. Not only is its performance more stable, the deque class is also easier to use because you don’t have to worry about adding or removing items from the wrong end.

En résumé, collections.deque is an excellent choice for implementing a stack (LIFO queue) in Python.

Queues (FIFOs)

In this section, you’ll see how to implement a First-In/First-Out (FIFO) queue data structure using only built-in data types and classes from the Python standard library.

UNE queue is a collection of objects that supports fast FIFO semantics for inserts and deletes. The insert and delete operations are sometimes called enqueue et dequeue. Unlike lists or arrays, queues typically don’t allow for random access to the objects they contain.

Here’s a real-world analogy for a FIFO queue:

Imagine a line of Pythonistas waiting to pick up their conference badges on day one of PyCon registration. As new people enter the conference venue and queue up to receive their badges, they join the line (enqueue) at the back of the queue. Developers receive their badges and conference swag bags and then exit the line (dequeue) at the front of the queue.

Another way to memorize the characteristics of a queue data structure is to think of it as a pipe. You add ping-pong balls to one end, and they travel to the other end, where you remove them. While the balls are in the queue (a solid metal pipe) you can’t get at them. The only way to interact with the balls in the queue is to add new ones at the back of the pipe (enqueue) or to remove them at the front (dequeue).

Queues are similar to stacks. The difference between them lies in how items are removed. Avec un queue, you remove the item least recently added (FIFO) but with a stack, you remove the item plus recently added (LIFO).

Performance-wise, a proper queue implementation is expected to take O(1) time for insert and delete operations. These are the two main operations performed on a queue, and in a correct implementation, they should be fast.

Queues have a wide range of applications in algorithms and often help solve scheduling and parallel programming problems. A short and beautiful algorithm using a queue is breadth-first search (BFS) on a tree or graph data structure.

Scheduling algorithms often use priority queues internally. These are specialized queues. Instead of retrieving the next element by insertion time, a priority queue retrieves the highest-priority element. The priority of individual elements is decided by the queue based on the ordering applied to their keys.

A regular queue, however, won’t reorder the items it carries. Just like in the pipe example, you get out what you put in, and in exactly that order.

Python ships with several queue implementations that each have slightly different characteristics. Let’s review them.

liste: Terribly Sloooow Queues

It’s possible to use a regular liste as a queue, but this is not ideal from a performance perspective. Lists are quite slow for this purpose because inserting or deleting an element at the beginning requires shifting all the other elements by one, requiring O(n) time.

Therefore, I would ne pas recommend using a liste as a makeshift queue in Python unless you’re dealing with only a small number of elements:

>>>

>>> q = []
>>> q.append("eat")
>>> q.append("sleep")
>>> q.append("code")

>>> q
['eat', 'sleep', 'code']

>>> # Careful: This is slow!
>>> q.pop(0)
'eat'

collections.deque: Fast and Robust Queues

le deque class implements a double-ended queue that supports adding and removing elements from either end in O(1) time (non-amortized). Because deques support adding and removing elements from either end equally well, they can serve both as queues and as stacks.

Python’s deque objects are implemented as doubly-linked lists. This gives them excellent and consistent performance for inserting and deleting elements, but poor O(n) performance for randomly accessing elements in the middle of the stack.

As a result, collections.deque is a great default choice if you’re looking for a queue data structure in Python’s standard library:

>>>

>>> de collections importer deque
>>> q = deque()
>>> q.append("eat")
>>> q.append("sleep")
>>> q.append("code")

>>> q
deque(['eat', 'sleep', 'code'])

>>> q.popleft()
'eat'
>>> q.popleft()
'sleep'
>>> q.popleft()
'code'

>>> q.popleft()
Traceback (most recent call last):
  File "", line 1, in 
IndexError: pop from an empty deque

queue.Queue: Locking Semantics for Parallel Computing

le queue.Queue implementation in the Python standard library is synchronized and provides locking semantics to support multiple concurrent producers and consumers.

le queue module contains several other classes implementing multi-producer, multi-consumer queues that are useful for parallel computing.

Depending on your use case, the locking semantics might be helpful or just incur unneeded overhead. In this case, you’d be better off using collections.deque as a general-purpose queue:

>>>

>>> de queue importer Queue
>>> q = Queue()
>>> q.put("eat")
>>> q.put("sleep")
>>> q.put("code")

>>> q


>>> q.avoir()
'eat'
>>> q.avoir()
'sleep'
>>> q.avoir()
'code'

>>> q.get_nowait()
queue.Empty

>>> q.avoir()  # Blocks/waits forever...

multiprocessing.Queue: Shared Job Queues

multiprocessing.Queue is a shared job queue implementation that allows queued items to be processed in parallel by multiple concurrent workers. Process-based parallelization is popular in CPython due to the global interpreter lock (GIL) that prevents some forms of parallel execution on a single interpreter process.

As a specialized queue implementation meant for sharing data between processes, multiprocessing.Queue makes it easy to distribute work across multiple processes in order to work around the GIL limitations. This type of queue can store and transfer any pickleable object across process boundaries:

>>>

>>> de multiprocessing importer Queue
>>> q = Queue()
>>> q.put("eat")
>>> q.put("sleep")
>>> q.put("code")

>>> q


>>> q.avoir()
'eat'
>>> q.avoir()
'sleep'
>>> q.avoir()
'code'

>>> q.avoir()  # Blocks/waits forever...

Queues in Python: Summary

Python includes several queue implementations as part of the core language and its standard library.

liste objects can be used as queues, but this is generally not recommended due to slow performance.

If you’re not looking for parallel processing support, then the implementation offered by collections.deque is an excellent default choice for implementing a FIFO queue data structure in Python. It provides the performance characteristics you’d expect from a good queue implementation and can also be used as a stack (LIFO queue).

Priority Queues

UNE priority queue is a container data structure that manages a set of records with totally-ordered keys to provide quick access to the record with the smallest or largest key in the set.

You can think of a priority queue as a modified queue. Instead of retrieving the next element by insertion time, it retrieves the highest-priority element. The priority of individual elements is decided by the order applied to their keys.

Priority queues are commonly used for dealing with scheduling problems. For example, you might use them to give precedence to tasks with higher urgency.

Think about the job of an operating system task scheduler:

Ideally, higher-priority tasks on the system (such as playing a real-time game) should take precedence over lower-priority tasks (such as downloading updates in the background). By organizing pending tasks in a priority queue that uses task urgency as the key, the task scheduler can quickly select the highest-priority tasks and allow them to run first.

In this section, you’ll see a few options for how you can implement priority queues in Python using built-in data structures or data structures included in Python’s standard library. Each implementation will have its own upsides and downsides, but in my mind there’s a clear winner for most common scenarios. Let’s find out which one it is.

liste: Manually Sorted Queues

You can use a sorted liste to quickly identify and delete the smallest or largest element. The downside is that inserting new elements into a list is a slow O(n) operation.

While the insertion point can be found in O(log n) time using bisect.insort in the standard library, this is always dominated by the slow insertion step.

Maintaining the order by appending to the list and re-sorting also takes at least O(n log n) time. Another downside is that you must manually take care of re-sorting the list when new elements are inserted. It’s easy to introduce bugs by missing this step, and the burden is always on you, the developer.

This means sorted lists are only suitable as priority queues when there will be few insertions:

>>>

>>> q = []
>>> q.append((2, "code"))
>>> q.append((1, "eat"))
>>> q.append((3, "sleep"))
>>> # Remember to re-sort every time a new element is inserted,
>>> # or use bisect.insort()
>>> q.sort(reverse=True)

>>> while q:
...     next_item = q.pop()
...     impression(next_item)
...
(1, 'eat')
(2, 'code')
(3, 'sleep')

heapq: List-Based Binary Heaps

heapq is a binary heap implementation usually backed by a plain liste, and it supports insertion and extraction of the smallest element in O(log n) time.

This module is a good choice for implementing priority queues in Python. Puisque heapq technically provides only a min-heap implementation, extra steps must be taken to ensure sort stability and other features typically expected from a practical priority queue:

>>>

>>> importer heapq
>>> q = []
>>> heapq.heappush(q, (2, "code"))
>>> heapq.heappush(q, (1, "eat"))
>>> heapq.heappush(q, (3, "sleep"))

>>> while q:
...     next_item = heapq.heappop(q)
...     impression(next_item)
...
(1, 'eat')
(2, 'code')
(3, 'sleep')

queue.PriorityQueue: Beautiful Priority Queues

queue.PriorityQueue uses heapq internally and shares the same time and space complexities. The difference is that PriorityQueue is synchronized and provides locking semantics to support multiple concurrent producers and consumers.

Depending on your use case, this might be helpful, or it might just slow your program down slightly. In any case, you might prefer the class-based interface provided by PriorityQueue over the function-based interface provided by heapq:

>>>

>>> de queue importer PriorityQueue
>>> q = PriorityQueue()
>>> q.put((2, "code"))
>>> q.put((1, "eat"))
>>> q.put((3, "sleep"))

>>> while ne pas q.empty():
...     next_item = q.avoir()
...     impression(next_item)
...
(1, 'eat')
(2, 'code')
(3, 'sleep')

Priority Queues in Python: Summary

Python includes several priority queue implementations ready for you to use.

queue.PriorityQueue stands out from the pack with a nice object-oriented interface and a name that clearly states its intent. It should be your preferred choice.

If you’d like to avoid the locking overhead of queue.PriorityQueue, then using the heapq module directly is also a good option.

Conclusion: Python Data Structures

That concludes your tour of common data structures in Python. With the knowledge you’ve gained here, you’re ready to implement efficient data structures that are just right for your specific algorithm or use case.

In this tutorial, you’ve learned:

  • Which common abstract data types are built into the Python standard library
  • How the most common abstract data types map to Python’s naming scheme
  • How to put abstract data types to practical use in various algorithms

If you enjoyed what you learned in this sample from Python Tricks: The Book, then be sure to check out the rest of the book.

If you’re interested in brushing up on your general data structures knowledge, then I highly recommend Steven S. Skiena’s The Algorithm Design Manual. It strikes a great balance between teaching you fundamental (and more advanced) data structures and showing you how to implement them in your code. Steve’s book was a great help in the writing of this tutorial.