2.37. Именованные кортежи и двусторонние очереди

Списки, кортежи, словари и множества покрывают большинство потребностей в данных. Три других контейнера в модуле collections подходят для конкретных задач, которые встроенные типы решают неуклюже.

2.37.1. namedtuple – типизированные записи без класса

Обычный кортеж хранит значения по позиции. Это нормально для маленького, недолговечного кортежа из 2 или 3 элементов, но дальше point[0] и point[1] начинают вводить в заблуждение относительно того, что они содержат. collections.namedtuple() возвращает новый подкласс кортежа, поля которого имеют имена:

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

Аргумент полей – это последовательность строк-имён (или одна строка с именами через пробел в CPython; MicroPython строже – передавайте кортеж или список).

Зачем использовать namedtuple вместо класса?

  • Это кортеж. Итерация, распаковка, сравнение на равенство, хеширование и использование в качестве ключа словаря – всё работает бесплатно.

  • Он неизменяем. Переприсваивание r.temp = ... вызывает AttributeError, что именно то, что нужно для типа-записи.

  • Он расходует меньше ОЗУ, чем экземпляр класса с теми же полями – хранилище кортежа непрерывно, без __dict__.

По сравнению с эквивалентным классом объявление namedtuple занимает одну строку. Компромисс в том, что поля доступны только для чтения – чтобы «изменить» показание, вы создаёте новое.

2.37.2. deque – ограниченный кольцевой буфер

Список быстр на конце (append / pop) и медлен в начале (insert(0, ...) / pop(0) оба сдвигают каждый остальной элемент). collections.deque быстра на обоих концах – это кольцевой буфер, индексируемый указателями головы и хвоста, поэтому добавление и извлечение с любой стороны выполняются за одинаковый фиксированный объём работы независимо от того, сколько элементов содержит deque.

Создание в MicroPython требует как начального итерируемого объекта, так и максимальной длины, именно в этом порядке:

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

Когда ограниченная deque заполнена, каждый append отбрасывает самый старый элемент. Это делает deque идеальной для «последних N выборок», «последних N строк журнала» или любого скользящего окна, которому не нужно расти бесконечно.

Методы, доступные в deque MicroPython, намеренно минимальны:

  • append(x) – добавить справа.

  • appendleft(x) – добавить слева.

  • extend(iterable) – добавить каждый элемент из итерируемого объекта.

  • pop() – удалить и вернуть правый конец. Вызывает IndexError, если пусто.

  • popleft() – удалить и вернуть левый конец.

Заметные отсутствия по сравнению с deque из CPython: нет clear, count, index, remove, reverse, rotate, атрибута maxlen или __contains__. Итерация и индексация по индексу работают:

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

Типичное применение: хранение нескольких последних показаний датчика для обнаружения изменений тенденции:

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 – когда порядок является частью равенства

Обычный dict сохраняет порядок вставки начиная с MicroPython 1.13 и CPython 3.7. Это покрывает самую распространённую причину, по которой раньше обращались к collections.OrderedDict.

Что OrderedDict всё ещё даёт вам, чего не даёт обычный dict:

  • Сравнение OrderedDict на равенство учитывает порядок. Два обычных словаря равны, когда у них одни и те же пары ключ/значение, независимо от порядка вставки. Два экземпляра OrderedDict равны только если их пары идут в одном и том же порядке:

    >>> 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 полезен, когда вы сериализуете конфигурацию в формат, который действительно заботится о порядке ключей (TOML, некоторые потребители YAML), или когда вы хотите дать чёткую подсказку в документации, что порядок имеет значение для читателей вашего кода.

Для повседневного кода предпочитайте встроенный dict. Обращайтесь к OrderedDict только когда семантика «порядок является частью равенства» или документирующая ценность действительно что-то даёт.

Каждый из трёх контейнеров имеет одно узкое применение. Именованные кортежи заменяют самодельные классы-записи; двусторонние очереди заменяют списки для ограниченных очередей; упорядоченные словари делают порядок вставки частью контракта.