2.37. Named Tuples und Deques

Listen, Tupel, Dictionaries und Sets decken die meisten Datenbedürfnisse ab. Drei weitere Container im collections-Modul passen zu spezifischen Problemen, die die eingebauten Typen unbeholfen handhaben.

2.37.1. namedtuple – typisierte Datensätze ohne Klasse

Ein einfaches Tupel speichert Werte nach Position. Das ist für ein kleines, kurzlebiges 2- oder 3-Tupel in Ordnung, aber darüber hinaus fangen point[0] und point[1] an, über ihren Inhalt zu täuschen. collections.namedtuple() gibt eine neue Tupel-Unterklasse zurück, deren Felder Namen haben:

>>> 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

Das Felder-Argument ist eine Sequenz von Namens-Strings (oder in CPython ein einzelner durch Leerzeichen getrennter String; MicroPython ist strenger – übergeben Sie ein Tupel oder eine Liste).

Warum ein namedtuple anstelle einer Klasse verwenden?

  • Es ist ein Tupel. Iteration, Entpacken, Gleichheit, Hashing und die Verwendung als Dict-Schlüssel funktionieren alle ohne weiteres Zutun.

  • Es ist unveränderlich. Eine erneute Zuweisung von r.temp = ... löst AttributeError aus, was genau das ist, was man für einen Datensatztyp möchte.

  • Es kostet weniger RAM als eine Klasseninstanz mit denselben Feldern – der Speicher des Tupels ist zusammenhängend, ohne __dict__.

Im Vergleich zur äquivalenten Klasse ist eine namedtuple-Deklaration eine Zeile. Der Kompromiss besteht darin, dass die Felder schreibgeschützt sind – um einen Messwert zu „ändern“, erzeugt man einen neuen.

2.37.2. deque – ein begrenzter Ringpuffer

Eine Liste ist am Ende schnell (append / pop) und am Anfang langsam (insert(0, ...) / pop(0) verschieben beide jedes andere Element). Eine collections.deque ist an beiden Enden schnell – es ist ein Ringpuffer, der durch Kopf- und Schwanzzeiger indiziert wird, sodass append und pop auf beiden Seiten mit demselben festen Arbeitsaufwand laufen, unabhängig davon, wie viele Elemente die Deque enthält.

Die Konstruktion in MicroPython erfordert sowohl ein anfängliches Iterable als auch eine maximale Länge, in dieser Reihenfolge:

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

Wenn eine begrenzte Deque voll ist, verwirft jedes append das älteste Element. Das macht die Deque ideal für „die letzten N Messwerte“, „die letzten N Log-Zeilen“ oder jedes gleitende Fenster, das nicht endlos wachsen muss.

Die auf MicroPythons Deque verfügbaren Methoden sind bewusst minimal gehalten:

  • append(x) – rechts hinzufügen.

  • appendleft(x) – links hinzufügen.

  • extend(iterable) – jedes Element aus dem Iterable anhängen.

  • pop() – das rechte Ende entfernen und zurückgeben. Löst IndexError aus, wenn leer.

  • popleft() – das linke Ende entfernen und zurückgeben.

Bemerkenswerte Auslassungen gegenüber CPythons Deque: kein clear, count, index, remove, reverse, rotate, kein maxlen-Attribut und kein __contains__. Iteration und Index-Zugriff per Subskript funktionieren:

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

Eine typische Verwendung: die letzten paar Sensormesswerte behalten, um Trendänderungen zu erkennen:

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 – wenn die Reihenfolge Teil der Gleichheit ist

Ein reguläres dict behält seit MicroPython 1.13 und CPython 3.7 die Einfügereihenfolge bei. Das deckt den häufigsten Grund ab, aus dem man früher zu collections.OrderedDict gegriffen hat.

Was OrderedDict Ihnen immer noch bietet, was ein einfaches Dict nicht bietet:

  • Die Gleichheit von OrderedDict berücksichtigt die Reihenfolge. Zwei reguläre Dicts sind gleich, wann immer sie dieselben Schlüssel/Wert-Paare haben, unabhängig von der Einfügereihenfolge. Zwei OrderedDict-Instanzen sind nur dann gleich, wenn ihre Paare in derselben Reihenfolge vorliegen:

    >>> 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 ist nützlich, wenn Sie eine Konfiguration in ein Format serialisieren, das sich tatsächlich um die Schlüsselreihenfolge kümmert (TOML, manche YAML-Verbraucher), oder wenn Sie einen klaren Dokumentationshinweis möchten, dass die Reihenfolge für die Leser Ihres Codes wichtig ist.

Bevorzugen Sie für den Alltagscode das eingebaute dict. Greifen Sie nur dann zu OrderedDict, wenn die Semantik „Reihenfolge ist Teil der Gleichheit“ oder der dokumentarische Wert tatsächlich etwas bringt.

Die drei Container haben jeweils einen eng umrissenen Anwendungsfall. Named Tuples ersetzen selbstgeschriebene Datensatzklassen; Deques ersetzen Listen für begrenzte Warteschlangen; geordnete Dicts machen die Einfügereihenfolge zum Teil des Vertrags.