2.37. Tuples nommés et deques

Les listes, tuples, dictionnaires et ensembles couvrent la plupart des besoins en matière de données. Trois autres conteneurs du module collections répondent à des problèmes spécifiques que les types intégrés gèrent maladroitement.

2.37.1. namedtuple – des enregistrements typés sans classe

Un tuple ordinaire stocke les valeurs par position. C’est parfait pour un petit tuple éphémère de 2 ou 3 éléments, mais au-delà, point[0] et point[1] commencent à mentir sur ce qu’ils contiennent. collections.namedtuple() renvoie une nouvelle sous-classe de tuple dont les champs portent des noms

>>> from collections import namedtuple
>>> Reading = namedtuple('Reading', ('temp', 'humidity', 'ts'))
>>> r = Reading(22.5, 41.0, 137204)
>>> r.temp
22.5
>>> r.humidity
41.0
>>> r[0]
22.5

L’argument des champs est une séquence de chaînes de noms (ou une seule chaîne séparée par des espaces en CPython ; MicroPython est plus strict – passez un tuple ou une liste).

Pourquoi utiliser un namedtuple plutôt qu’une classe ?

  • C’est un tuple. L’itération, le déballage, l’égalité, le hachage et l’utilisation comme clé de dictionnaire fonctionnent tous gratuitement.

  • Il est immuable. Réaffecter r.temp = ... lève une AttributeError, ce qui est exactement ce que l’on veut pour un type d’enregistrement.

  • Il coûte moins de RAM qu’une instance de classe ayant les mêmes champs – le stockage du tuple est contigu, sans __dict__.

Comparée à la classe équivalente, une déclaration de namedtuple tient en une ligne. Le compromis est que les champs sont en lecture seule – pour « modifier » un relevé, vous en créez un nouveau.

2.37.2. deque – un tampon circulaire borné

Une liste est rapide à la fin (append / pop) et lente au début (insert(0, ...) / pop(0) décalent tous deux chaque autre élément). Une collections.deque est rapide aux deux extrémités – c’est un tampon circulaire indexé par des pointeurs de tête et de queue, de sorte que l’ajout et le retrait de chaque côté s’exécutent avec la même quantité de travail fixe, quel que soit le nombre d’éléments que contient la deque.

La construction en MicroPython exige à la fois un itérable initial et une longueur maximale, dans cet ordre

>>> from collections import deque
>>> events = deque((), 5)
>>> for i in range(8):
...     events.append(i)
>>> list(events)
[3, 4, 5, 6, 7]

Lorsqu’une deque bornée est pleine, chaque append supprime l’élément le plus ancien. Cela rend la deque idéale pour « les N derniers échantillons », « les N dernières lignes de journal » ou toute fenêtre glissante qui n’a pas besoin de croître indéfiniment.

Les méthodes exposées sur la deque de MicroPython sont délibérément minimales :

  • append(x) – ajoute à droite.

  • appendleft(x) – ajoute à gauche.

  • extend(iterable) – ajoute chaque élément de l’itérable.

  • pop() – retire et renvoie l’extrémité droite. Lève IndexError si vide.

  • popleft() – retire et renvoie l’extrémité gauche.

Omissions notables par rapport à la deque de CPython : pas de clear, count, index, remove, reverse, rotate, d’attribut maxlen ni de __contains__. L’itération et l’indexation par indice fonctionnent

>>> events[0]
3
>>> for e in events:
...     print(e)

Une utilisation typique : conserver les derniers relevés de capteur pour détecter les changements de tendance

history = deque((), 10)

def push(reading):
    history.append(reading)
    if len(history) == 10 and history[-1] > 2 * history[0]:
        print('reading is climbing')

2.37.3. OrderedDict – quand l’ordre fait partie de l’égalité

Un dict ordinaire préserve l’ordre d’insertion depuis MicroPython 1.13 et CPython 3.7. Cela couvre la raison la plus courante pour laquelle on avait autrefois recours à collections.OrderedDict.

Ce que OrderedDict vous apporte encore qu’un dict ordinaire n’offre pas :

  • L’égalité de OrderedDict tient compte de l’ordre. Deux dicts ordinaires sont égaux dès qu’ils ont les mêmes paires clé/valeur, indépendamment de l’ordre d’insertion. Deux instances de OrderedDict ne sont égales que si leurs paires sont dans le même ordre

    >>> from collections import OrderedDict
    >>> OrderedDict([('a', 1), ('b', 2)]) == OrderedDict([('b', 2), ('a', 1)])
    False
    >>> {'a': 1, 'b': 2} == {'b': 2, 'a': 1}
    True
    
  • OrderedDict est utile lorsque vous sérialisez une configuration vers un format qui se soucie de l’ordre des clés (TOML, certains consommateurs de YAML), ou lorsque vous voulez fournir un indice de documentation clair indiquant que l’ordre compte pour les lecteurs de votre code.

Pour le code de tous les jours, préférez le dict intégré. N’ayez recours à OrderedDict que lorsque la sémantique « l’ordre fait partie de l’égalité » ou la valeur documentaire apporte réellement quelque chose.

Les trois conteneurs ont chacun un usage précis et étroit. Les tuples nommés remplacent les classes d’enregistrement écrites à la main ; les deques remplacent les listes pour les files bornées ; les dictionnaires ordonnés font de l’ordre d’insertion une partie du contrat.