2.37. Krotki nazwane i kolejki dwustronne

Listy, krotki, słowniki i zbiory pokrywają większość potrzeb dotyczących danych. Trzy inne kontenery w module collections pasują do specyficznych problemów, które wbudowane typy obsługują niezgrabnie.

2.37.1. namedtuple – typowane rekordy bez klasy

Zwykła krotka przechowuje wartości według pozycji. To jest w porządku dla małej, krótko żyjącej 2- lub 3-elementowej krotki, ale powyżej tego point[0] i point[1] zaczynają kłamać o tym, co przechowują. collections.namedtuple() zwraca nową podklasę krotki, której pola mają nazwy:

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

Argument fields to sekwencja łańcuchów z nazwami (lub jeden łańcuch rozdzielany białymi znakami w CPythonie; MicroPython jest bardziej rygorystyczny – przekaż krotkę lub listę).

Dlaczego używać namedtuple zamiast klasy?

  • To krotka. Iteracja, rozpakowywanie, równość, haszowanie i użycie jako klucz słownika – wszystko działa za darmo.

  • Jest niezmienna. Ponowne przypisanie r.temp = ... zgłasza AttributeError, co jest dokładnie tym, czego chcesz dla typu rekordu.

  • Kosztuje mniej RAM niż instancja klasy z tymi samymi polami – pamięć krotki jest ciągła, bez __dict__.

W porównaniu z równoważną klasą, deklaracja namedtuple to jedna linia. Kompromisem jest to, że pola są tylko do odczytu – aby „zmienić” odczyt, tworzysz nowy.

2.37.2. deque – ograniczony bufor cykliczny

Lista jest szybka na końcu (append / pop) i wolna na początku (insert(0, ...) / pop(0) – oba przesuwają każdy inny element). collections.deque jest szybka na obu końcach – to bufor cykliczny indeksowany wskaźnikami głowy i ogona, więc dodawanie i zdejmowanie po dowolnej stronie wykonuje tę samą stałą ilość pracy niezależnie od tego, ile elementów przechowuje deque.

Konstrukcja w MicroPythonie wymaga zarówno początkowego obiektu iterowalnego, jak i maksymalnej długości, w tej kolejności:

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

Gdy ograniczona deque jest pełna, każde append usuwa najstarszy element. To czyni deque idealną do „ostatnich N próbek”, „ostatnich N linii dziennika” lub dowolnego przesuwnego okna, które nie musi rosnąć w nieskończoność.

Metody udostępniane przez deque w MicroPythonie są celowo minimalne:

  • append(x) – dodaje na prawo.

  • appendleft(x) – dodaje na lewo.

  • extend(iterable) – dołącza każdy element z obiektu iterowalnego.

  • pop() – usuwa i zwraca prawy koniec. Zgłasza IndexError przy pustej kolejce.

  • popleft() – usuwa i zwraca lewy koniec.

Godne uwagi braki względem deque z CPythona: brak clear, count, index, remove, reverse, rotate, atrybutu maxlen oraz __contains__. Iteracja i indeksowanie przez subskrypt działają:

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

Typowe zastosowanie: przechowywanie kilku ostatnich odczytów sensorów w celu wykrywania zmian trendu:

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 – gdy kolejność jest częścią równości

Zwykły dict zachowuje kolejność wstawiania od czasu MicroPythona 1.13 i CPythona 3.7. To pokrywa najczęstszy powód, dla którego ludzie sięgali wcześniej po collections.OrderedDict.

To, co OrderedDict nadal daje, a czego zwykły dict nie:

  • Równość OrderedDict uwzględnia kolejność. Dwa zwykłe słowniki porównują się jako równe zawsze, gdy mają te same pary klucz/wartość, niezależnie od kolejności wstawiania. Dwie instancje OrderedDict są równe tylko wtedy, gdy ich pary są w tej samej kolejności:

    >>> 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 jest przydatny, gdy serializujesz konfigurację do formatu, który faktycznie dba o kolejność kluczy (TOML, niektórzy konsumenci YAML), lub gdy chcesz dać czytelną podpowiedź w dokumentacji, że kolejność ma znaczenie dla czytelników twojego kodu.

W codziennym kodzie preferuj wbudowany dict. Sięgaj po OrderedDict tylko wtedy, gdy semantyka kolejność-jako-część-równości lub wartość dokumentacyjna faktycznie coś daje.

Każdy z tych trzech kontenerów ma jedno wąskie zastosowanie. Krotki nazwane zastępują ręcznie pisane klasy rekordów; deque zastępują listy dla ograniczonych kolejek; uporządkowane słowniki czynią kolejność wstawiania częścią kontraktu.