OrderedDict против dict на Python: Подходящий инструмент для работы

Оглавление

Смотрите сейчас, к этому уроку прилагается соответствующий видеокурс, созданный командой Real Python. Посмотрите его вместе с письменным руководством, чтобы углубить свое понимание: Использование OrderedDict в Python

Иногда вам нужен словарь Python , который запоминает порядок своих элементов. В прошлом у вас был только один инструмент для решения этой конкретной проблемы: Python's OrderedDict. Это подкласс словаря, специально разработанный для запоминания порядка элементов, который определяется порядком вставки ключей.

В Python 3.6 это изменилось. Встроенный класс dict теперь также упорядочивает свои элементы. Из-за этого многие в сообществе Python теперь задаются вопросом, полезен ли по-прежнему OrderedDict. Более пристальный взгляд на OrderedDict покажет, что этот класс по-прежнему предоставляет ценные возможности.

В этом руководстве вы узнаете, как:

  • Создавайте и используйте OrderedDict объекты в вашем коде
  • Определите различия между OrderedDict и dict
  • Понять плюсы и минусы использования OrderedDict vs dict

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

В конце руководства вы увидите пример реализации очереди на основе словаря с использованием OrderedDict, что было бы сложнее, если бы вы использовали обычный объект dict.

Бесплатный бонус: Нажмите здесь, чтобы получить шпаргалку по Python и изучить основы Python 3, такие как работа с типами данных, словари, списки и функции Python.

Выбор между OrderedDict и dict

В течение многих лет словари Python были неупорядоченными структурами данных. Разработчики Python привыкли к этому факту и полагались на списков или других последовательностей, когда им нужно было привести свои данные в порядок. Со временем разработчики обнаружили необходимость в словаре нового типа, в котором элементы были бы упорядочены.

Еще в 2008 году в PEP 372 была представлена идея добавления нового класса словаря в collections. Его основная цель состояла в том, чтобы запомнить порядок расположения элементов в соответствии с порядком, в котором были вставлены ключи. Это было началом OrderedDict.

Разработчики Core Python хотели заполнить этот пробел и предоставить словарь, который мог бы сохранять порядок вставляемых ключей. Что, в свою очередь, позволило более просто реализовать конкретные алгоритмы, основанные на этом свойстве.

OrderedDict был добавлен в стандартную библиотеку в Python 3.1. Его API по сути такой же, как и в dict. Однако OrderedDict выполняет итерацию по ключам и значениям в том же порядке, в котором они были вставлены. Если новая запись перезаписывает существующую запись, порядок элементов остается неизменным. Если запись удалена и вставлена повторно, то она будет перемещена в конец словаря.

В Python 3.6 появилась новая реализация dict. Эта новая реализация представляет собой большой выигрыш с точки зрения использования памяти и эффективности итераций. Кроме того, новая реализация предоставляет новую и несколько неожиданную функцию: dict объекты теперь сохраняют свои элементы в том же порядке, в котором они были введены. Изначально эта функция считалась деталью реализации, и в документации не рекомендовалось полагаться на нее.

Примечание: В этом руководстве вы сосредоточитесь на реализациях dict и OrderedDict, которые предоставляет CPython.

По словам Раймонда Хеттингера, разработчика core Python и соавтора OrderedDict, класс был специально разработан для упорядочивания элементов, в то время как новая реализация dict был разработан таким образом, чтобы быть компактным и обеспечивать быструю итерацию:

Текущий регулярный словарь основан на проекте, который я предложил несколько лет назад. Основными целями этого проекта были компактность и ускорение итераций по плотным массивам ключей и значений. Поддержание порядка было скорее артефактом, чем целью дизайна. Дизайн может поддерживать порядок, но это не его специализация.

В отличие от этого, я придал collections.OrderedDict другой дизайн (позже закодированный на C Эриком Сноу). Основная цель состояла в том, чтобы обеспечить эффективное поддержание порядка даже при серьезных рабочих нагрузках, таких как lru_cache, которые часто изменяют порядок, не затрагивая лежащий в основе dict. Дизайн OrderedDict специально разработан таким образом, чтобы возможности упорядочивания были приоритетными за счет дополнительных затрат памяти и постоянного увеличения времени вставки.

Моя цель по-прежнему заключается в том, чтобы collections.OrderedDict иметь другой дизайн и другие эксплуатационные характеристики, отличные от обычных диктовок. В нем есть некоторые специфичные для порядка методы, которых нет в обычных диктовках (например, move_to_end() и popitem(), которые эффективно отображаются с обоих концов). OrderedDict должен быть хорош в этих операциях, потому что именно это отличает его от обычных диктовок. (Источник)

В Python 3.7 функция упорядочивания элементов dict объектов была объявлена официальной частью Спецификация языка Python. Таким образом, с этого момента разработчики могли полагаться на dict, когда им требовался словарь, в котором элементы упорядочивались бы.

На этом этапе возникает вопрос: нужен ли OrderedDict по-прежнему после этой новой реализации dict? Ответ зависит от вашего конкретного варианта использования, а также от того, насколько явным вы хотите видеть его в своем коде.

На момент написания статьи некоторые особенности OrderedDict все еще делали его ценным и отличным от обычного dict:

  1. Сигнализация о намерении: Если вы используете OrderedDict вместо dict, то ваш код дает понять, что порядок элементов в словаре важен. Вы ясно даете понять, что ваш код нуждается в порядке элементов в базовом словаре или полагается на него.
  2. Управление порядком элементов: Если вам нужно переставить элементы в словаре или изменить их порядок, вы можете использовать .move_to_end(), а также расширенный вариант .popitem().
  3. Поведение при проверке на равенство: Если ваш код сравнивает словари на предмет равенства, и порядок элементов важен при этом сравнении, то OrderedDict - правильный выбор.

Есть, по крайней мере, еще одна причина продолжать использовать OrderedDict в вашем коде: обратная совместимость. Использование обычных объектов dict для сохранения порядка элементов приведет к нарушению работы вашего кода в средах, использующих версии Python старше 3.6.

Трудно сказать, скоро ли dict полностью заменит OrderedDict. В настоящее время OrderedDict по-прежнему предлагает интересные и ценные функции, которые вы, возможно, захотите учесть при выборе инструмента для конкретной работы.

Начало работы с Python OrderedDict

В Python OrderedDict является подклассом dict, который сохраняет порядок, в котором пары ключ-значение, обычно известный как элементы, которые вставляются в словарь. При выполнении итерации по объекту OrderedDict элементы перемещаются в исходном порядке. Если вы обновляете значение существующего ключа, порядок остается неизменным. Если вы удалите элемент и вставите его повторно, то этот элемент будет добавлен в конец словаря.

Принадлежность к подклассу dict означает, что он наследует все методы, предоставляемые обычным словарем. OrderedDict также обладает дополнительными функциями, о которых вы узнаете из этого руководства. Однако в этом разделе вы познакомитесь с основами создания и использования OrderedDict объектов в вашем коде.

Создание OrderedDict Объектов

В отличие от dict, OrderedDict не является встроенным типом, поэтому первым шагом для создания OrderedDict объектов является импорт класса из collections. Существует несколько способов создания упорядоченных словарей. Большинство из них идентичны способу создания обычного объекта dict. Например, вы можете создать пустой объект OrderedDict, создав экземпляр класса без аргументов:

>>> from collections import OrderedDict

>>> numbers = OrderedDict()

>>> numbers["one"] = 1
>>> numbers["two"] = 2
>>> numbers["three"] = 3

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


В этом случае вы сначала импортируете OrderedDict из collections. Затем вы создаете пустой упорядоченный словарь, создавая экземпляр OrderedDict без предоставления аргументов конструктору.

Вы можете добавить пары ключ-значение в словарь, указав ключ в квадратных скобках ([]) и присвоив этому ключу значение. Когда вы ссылаетесь на numbers, вы получаете итерацию пар ключ-значение, которая содержит элементы в том же порядке, в каком они были вставлены в словарь.

Вы также можете передать итерацию элементов в качестве аргумента конструктору OrderedDict:

>>> from collections import OrderedDict

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

>>> letters = OrderedDict({("a", 1), ("b", 2), ("c", 3)})
>>> letters
OrderedDict([('c', 3), ('a', 1), ('b', 2)])


При использовании последовательности , такой как list или tuple, порядок элементов в результирующем упорядоченном словаре соответствует исходному порядок расположения элементов во входной последовательности. Если вы используете set, как во втором примере выше, то окончательный порядок элементов неизвестен до тех пор, пока не будет создан OrderedDict.

Если вы используете обычный словарь в качестве инициализатора объекта OrderedDict и используете Python 3.6 или более поздней версии, то вы получите следующее поведение:

Python 3.9.0 (default, Oct  5 2020, 17:52:02)
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from collections import OrderedDict

>>> numbers = OrderedDict({"one": 1, "two": 2, "three": 3})
>>> numbers
OrderedDict([('one', 1), ('two', 2), ('three', 3)])


Порядок элементов в объекте OrderedDict соответствует порядку в исходном словаре. С другой стороны, если вы используете версию Python ниже 3.6, то порядок элементов неизвестен:

Python 3.5.10 (default, Jan 25 2021, 13:22:52)
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from collections import OrderedDict

>>> numbers = OrderedDict({"one": 1, "two": 2, "three": 3})
>>> numbers
OrderedDict([('one', 1), ('three', 3), ('two', 2)])


Поскольку словари в Python 3.5 не запоминают порядок своих элементов, вы не узнаете порядок в результирующем упорядоченном словаре до тех пор, пока объект не будет создан. С этого момента порядок сохраняется.

Вы можете создать упорядоченный словарь, передав аргументы ключевого слова в конструктор класса:

>>> from collections import OrderedDict

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


Начиная с Python 3.6, функции сохраняют порядок аргументов ключевых слов, передаваемых при вызове. Таким образом, порядок элементов в приведенном выше OrderedDict соответствует порядку, в котором вы передаете аргументы ключевого слова конструктору. В более ранних версиях Python этот порядок неизвестен.

Наконец, OrderedDict также предоставляет .fromkeys(), который создает новый словарь из повторяемого набора ключей и присваивает всем его значениям общее значение:

>>> from collections import OrderedDict

>>> keys = ["one", "two", "three"]
>>> OrderedDict.fromkeys(keys, 0)
OrderedDict([('one', 0), ('two', 0), ('three', 0)])


В этом случае вы создаете упорядоченный словарь, используя список ключей в качестве отправной точки. Второй аргумент .fromkeys() предоставляет единое значение для всех элементов в словаре.

Управление элементами в OrderedDict

Поскольку OrderedDict является изменяемой структурой данных, вы можете выполнять операции изменения над ее экземплярами. Вы можете вставлять новые элементы, обновлять и удалять существующие элементы и т.д. Если вы вставляете новый элемент в существующий упорядоченный словарь, то этот элемент добавляется в конец словаря:

>>> from collections import OrderedDict

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

>>> numbers["four"] = 4
>>> numbers
OrderedDict([('one', 1), ('two', 2), ('three', 3), ('four', 4)])


Вновь добавленный элемент, ('four', 4), помещается в конец базового словаря, поэтому порядок существующих элементов остается неизменным, и словарь сохраняет порядок вставки.

Если вы удаляете элемент из существующего упорядоченного словаря и вставляете этот же элемент снова, то новый экземпляр элемента помещается в конец словаря:

>>> from collections import OrderedDict
>>> numbers = OrderedDict(one=1, two=2, three=3)

>>> del numbers["one"]
>>> numbers
OrderedDict([('two', 2), ('three', 3)])

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


Если вы удалите элемент ('one', 1) и вставите новый экземпляр того же элемента, то новый элемент будет добавлен в конец базового словаря.

Если вы переназначаете или обновляете значение существующей пары ключ-значение в объекте OrderedDict, то ключ сохраняет свою позицию, но получает новое значение:

>>> from collections import OrderedDict
>>> numbers = OrderedDict(one=1, two=2, three=3)

>>> numbers["one"] = 1.0
>>> numbers
OrderedDict([('one', 1.0), ('two', 2), ('three', 3)])

>>> numbers.update(two=2.0)
>>> numbers
OrderedDict([('one', 1.0), ('two', 2.0), ('three', 3)])


Если вы обновляете значение данного ключа в упорядоченном словаре, то ключ не перемещается, а присваивается новое значение на месте. Аналогичным образом, если вы используете .update() для изменения значения существующей пары ключ-значение, то словарь запоминает положение ключа и присваивает ему обновленное значение.

Выполнение итерации по OrderedDict

Как и в случае с обычными словарями, вы можете выполнить итерацию по объекту OrderedDict, используя несколько инструментов и приемов. Вы можете перебирать ключи напрямую или использовать методы со словарем, такие как .items(), .keys(), и .values():

>>> from collections import OrderedDict
>>> numbers = OrderedDict(one=1, two=2, three=3)

>>> # Iterate over the keys directly
>>> for key in numbers:
...     print(key, "->", numbers[key])
...
one -> 1
two -> 2
three -> 3

>>> # Iterate over the items using .items()
>>> for key, value in numbers.items():
...     print(key, "->", value)
...
one -> 1
two -> 2
three -> 3

>>> # Iterate over the keys using .keys()
>>> for key in numbers.keys():
...     print(key, "->", numbers[key])
...
one -> 1
two -> 2
three -> 3

>>> # Iterate over the values using .values()
>>> for value in numbers.values():
...     print(value)
...
1
2
3


Первый for цикл выполняется непосредственно по ключам numbers. Остальные три цикла используют методы словаря для перебора элементов, ключей и значений numbers.

Повторение в обратном порядке с reversed()

Еще одной важной особенностью, которую OrderedDict предоставляет Python 3.5, является то, что его элементы, ключи и значения поддерживают обратную итерацию с использованием reversed(). Эта функция была добавлена в обычные словари в Python 3.8. Таким образом, если ваш код использует его, то ваша обратная совместимость с обычными словарями гораздо более ограничена.

Вы можете использовать reversed() с элементами, ключами и значениями объекта OrderedDict:

>>> from collections import OrderedDict
>>> numbers = OrderedDict(one=1, two=2, three=3)

>>> # Iterate over the keys directly in reverse order
>>> for key in reversed(numbers):
...     print(key, "->", numbers[key])
...
three -> 3
two -> 2
one -> 1

>>> # Iterate over the items in reverse order
>>> for key, value in reversed(numbers.items()):
...     print(key, "->", value)
...
three -> 3
two -> 2
one -> 1

>>> # Iterate over the keys in reverse order
>>> for key in reversed(numbers.keys()):
...     print(key, "->", numbers[key])
...
three -> 3
two -> 2
one -> 1

>>> # Iterate over the values in reverse order
>>> for value in reversed(numbers.values()):
...     print(value)
...
3
2
1


Каждый цикл в этом примере использует reversed() для перебора различных элементов упорядоченного словаря в обратном порядке.

Обычные словари также поддерживают обратную итерацию. Однако, если вы попытаетесь использовать reversed() с обычным объектом dict в версии Python ниже 3.8, то получите TypeError:

Python 3.7.9 (default, Jan 14 2021, 11:41:20)
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> numbers = dict(one=1, two=2, three=3)

>>> for key in reversed(numbers):
...     print(key)
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'dict' object is not reversible


Если вам нужно перебрать элементы в словаре в обратном порядке, то OrderedDict - хороший помощник. Использование обычного словаря значительно снижает вашу обратную совместимость, поскольку обратная итерация не была добавлена в обычные словари до версии Python 3.8.

Изучение уникальных возможностей языка Python OrderedDict

Начиная с версии Python 3.6, обычные словари сохраняют свои элементы в том же порядке, в каком они были вставлены в базовый словарь. Как вы уже видели, это ограничивает полезность OrderedDict. Однако OrderedDict предоставляет некоторые уникальные возможности, которые вы не сможете найти в обычном объекте dict.

При использовании упорядоченного словаря у вас есть доступ к следующим дополнительным и расширенным методам:

  • .move_to_end() добавлен ли новый метод в Python 3.2, который позволяет перемещать существующий элемент либо в в конец или в начало словаря.

  • .popitem() является расширенной вариацией своего аналога dict.popitem(), который позволяет вам удалять и возвращать элемент либо из конца, либо из начала базового упорядоченного словаря.

OrderedDict и dict также ведут себя по-разному, когда проверяются на равенство. В частности, при сравнении упорядоченных словарей важен порядок элементов. С обычными словарями дело обстоит иначе.

Наконец, экземпляры OrderedDict предоставляют атрибут с именем .__dict__, который вы не можете найти в экземпляре обычного словаря. Этот атрибут позволяет добавлять пользовательские атрибуты, доступные для записи, в существующий упорядоченный словарь.

Изменение порядка Элементов С помощью .move_to_end()

Одно из наиболее примечательных различий между dict и OrderedDict заключается в том, что в последнем есть дополнительный метод, называемый .move_to_end(). Этот метод позволяет перемещать существующие элементы либо в конец, либо в начало базового словаря, так что это отличный инструмент для изменения порядка в словаре.

Когда вы используете .move_to_end(), вы можете указать два аргумента:

  1. key содержит клавишу, которая идентифицирует элемент, который вы хотите переместить. Если key не существует, то вы получаете KeyError.

  2. last содержит логическое значение , которое определяет, в какой конец словаря вы хотите переместить элемент под рукой. По умолчанию используется значение True, что означает, что элемент будет перемещен в конец, или правую часть, словаря. False означает, что элемент будет перемещен в начало, или левую часть, упорядоченного словаря.

Вот пример того, как использовать .move_to_end() с аргументом key и полагаться на значение по умолчанию last:

>>> from collections import OrderedDict
>>> numbers = OrderedDict(one=1, two=2, three=3)
>>> numbers
OrderedDict([('one', 1), ('two', 2), ('three', 3)])

>>> numbers.move_to_end("one")
>>> numbers
OrderedDict([('two', 2), ('three', 3), ('one', 1)])


Когда вы вызываете .move_to_end() с key в качестве аргумента, вы перемещаете имеющуюся пару ключ-значение в конец словаря. Вот почему ('one', 1) сейчас находится на последней позиции. Обратите внимание, что остальные элементы остаются в том же первоначальном порядке.

Если вы перейдете от False к last, то переместите элемент в начало:

>>> numbers.move_to_end("one", last=False)
>>> numbers
OrderedDict([('one', 1), ('two', 2), ('three', 3)])


В этом случае вы перемещаете ('one', 1) в начало словаря. Это обеспечивает интересную и мощную функцию. Например, с помощью .move_to_end() вы можете отсортировать упорядоченный словарь по ключам:

>>> from collections import OrderedDict
>>> letters = OrderedDict(b=2, d=4, a=1, c=3)

>>> for key in sorted(letters):
...     letters.move_to_end(key)
...
>>> letters
OrderedDict([('a', 1), ('b', 2), ('c', 3), ('d', 4)])


В этом примере сначала создается упорядоченный словарь, letters. Цикл for выполняет итерацию по отсортированным ключам и перемещает каждый элемент в конец словаря. Когда цикл завершится, элементы вашего упорядоченного словаря будут отсортированы по ключам.

Сортировка словаря по значениям была бы интересным упражнением, поэтому разверните блок ниже и попробуйте!

Отсортируйте следующий словарь по значениям:

>>> from collections import OrderedDict
>>> letters = OrderedDict(a=4, b=3, d=1, c=2)


как полезную подсказку для реализации решения, рассмотрите возможность использования lambda Функция.

Вы можете развернуть блок ниже, чтобы увидеть возможное решение.

Вы можете использовать функцию lambda для получения значения каждой пары ключ-значение в letters и использовать эту функцию в качестве аргумента key для sorted():

>>> for key, _ in sorted(letters.items(), key=lambda item: item[1]):
...     letters.move_to_end(key)
...
>>> letters
OrderedDict([('d', 1), ('c', 2), ('b', 3), ('a', 4)])


В этом коде вы используете функцию lambda, которая возвращает значение каждой пары ключ-значение в letters. Вызов sorted() использует эту функцию lambda для извлечения ключа сравнения из каждого элемента входного итеративного параметра letters.items(). Затем вы используете .move_to_end() для сортировки letters.

Отлично! Теперь вы знаете, как изменить порядок упорядоченных словарей с помощью .move_to_end(). Вы готовы перейти к следующему разделу.

Удаление элементов С помощью .popitem()

Еще одной интересной особенностью OrderedDict является расширенная версия .popitem(). По умолчанию .popitem() удаляет и возвращает элемент в порядке LIFO (Последний пришедший/первый вышедший). Другими словами, он удаляет элементы из правого конца упорядоченного словаря:

>>> from collections import OrderedDict
>>> numbers = OrderedDict(one=1, two=2, three=3)

>>> numbers.popitem()
('three', 3)
>>> numbers.popitem()
('two', 2)
>>> numbers.popitem()
('one', 1)
>>> numbers.popitem()
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    numbers.popitem()
KeyError: 'dictionary is empty'


Здесь вы удаляете все элементы из numbers, используя .popitem(). Каждый вызов этого метода удаляет один элемент из конца базового словаря. Если вы вызовете .popitem() в пустом словаре, то получите KeyError. До этого момента .popitem() ведет себя так же, как и в обычных словарях.

Однако

В OrderedDict .popitem() также принимает логический аргумент last, значение которого по умолчанию равно True. Если вы установите для last значение False, то .popitem() удалит элементы в порядке FIFO (первый вход/первый выход), что означает, что он удаляет элементы из начала словаря:

>>> from collections import OrderedDict
>>> numbers = OrderedDict(one=1, two=2, three=3)

>>> numbers.popitem(last=False)
('one', 1)
>>> numbers.popitem(last=False)
('two', 2)
>>> numbers.popitem(last=False)
('three', 3)
>>> numbers.popitem(last=False)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    numbers.popitem(last=False)
KeyError: 'dictionary is empty'


Если для параметра last задано значение False, вы можете использовать .popitem() для удаления и возврата элементов из начала упорядоченного словаря. В этом примере последний вызов .popitem() вызывает KeyError, поскольку базовый словарь уже пуст.

Проверка на равенство между словарями

Когда вы проверяете два OrderedDict объекта на равенство в логическом контексте, порядок элементов играет важную роль. Например, если ваши упорядоченные словари содержат один и тот же набор элементов, то результат теста зависит от их порядка:

>>> from collections import OrderedDict
>>> letters_0 = OrderedDict(a=1, b=2, c=3, d=4)
>>> letters_1 = OrderedDict(b=2, a=1, c=3, d=4)
>>> letters_2 = OrderedDict(a=1, b=2, c=3, d=4)

>>> letters_0 == letters_1
False

>>> letters_0 == letters_2
True


В этом примере letters_1 имеет небольшое отличие в порядке следования элементов по сравнению с letters_0 и letters_2, поэтому первый тест возвращает False. Во втором тесте letters_0 и letters_2 содержат одинаковый набор элементов, расположенных в одинаковом порядке, поэтому тест возвращает True.

Если вы попробуете повторить этот же пример с помощью обычных словарей, то получите другой результат:

>>> letters_0 = dict(a=1, b=2, c=3, d=4)
>>> letters_1 = dict(b=2, a=1, c=3, d=4)
>>> letters_2 = dict(a=1, b=2, c=3, d=4)

>>> letters_0 == letters_1
True

>>> letters_0 == letters_2
True

>>> letters_0 == letters_1 == letters_2
True


Здесь, когда вы проверяете два обычных словаря на равенство, вы получаете True, если оба словаря содержат одинаковый набор элементов. В этом случае порядок элементов не влияет на конечный результат.

Наконец, тесты на равенство между объектом OrderedDict и обычным словарем не учитывают порядок элементов:

>>> from collections import OrderedDict
>>> letters_0 = OrderedDict(a=1, b=2, c=3, d=4)
>>> letters_1 = dict(b=2, a=1, c=3, d=4)

>>> letters_0 == letters_1
True


При сравнении упорядоченных словарей с обычными порядок элементов не имеет значения. Если оба словаря содержат одинаковый набор элементов, то они сравниваются одинаково, независимо от порядка их элементов.

Добавление новых атрибутов к экземпляру словаря

OrderedDict объекты имеют атрибут .__dict__, который вы не можете найти в обычных словарных объектах. Взгляните на следующий код:

>>> from collections import OrderedDict
>>> letters = OrderedDict(b=2, d=4, a=1, c=3)
>>> letters.__dict__
{}

>>> letters1 = dict(b=2, d=4, a=1, c=3)
>>> letters1.__dict__
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    letters1.__dict__
AttributeError: 'dict' object has no attribute '__dict__'


В первом примере вы получаете доступ к атрибуту .__dict__ в упорядоченном словаре letters. Python внутренне использует этот атрибут для хранения доступных для записи атрибутов экземпляра. Во втором примере показано, что обычные словарные объекты не имеют атрибута .__dict__.

Вы можете использовать атрибут упорядоченного словаря .__dict__ для хранения динамически создаваемых атрибутов экземпляра, доступных для записи. Существует несколько способов сделать это. Например, вы можете использовать назначение в стиле словаря, как в ordered_dict.__dict__["attr"] = value. Вы также можете использовать точечную запись, как в ordered_dict.attr = value.

Вот пример использования .__dict__ для присоединения новой функции к существующему упорядоченному словарю:

>>> from collections import OrderedDict
>>> letters = OrderedDict(b=2, d=4, a=1, c=3)

>>> letters.sorted_keys = lambda: sorted(letters.keys())
>>> vars(letters)
{'sorted_keys': <function <lambda> at 0x7fa1e2fe9160>}

>>> letters.sorted_keys()
['a', 'b', 'c', 'd']

>>> letters["e"] = 5
>>> letters.sorted_keys()
['a', 'b', 'c', 'd', 'e']


Теперь у вас есть .sorted_keys() lambda функция, подключенная к вашему letters упорядоченному словарю. Обратите внимание, что вы можете просмотреть содержимое .__dict__, либо обратившись к нему напрямую с помощью точечной записи, либо используя vars().

Примечание: Этот тип динамического атрибута добавляется к конкретному экземпляру данного класса. В приведенном выше примере этим экземпляром является letters. Это не влияет ни на другие экземпляры, ни на сам класс, поэтому у вас есть доступ к .sorted_keys() только через letters.

Вы можете использовать эту динамически добавляемую функцию для перебора ключей словаря в отсортированном порядке без изменения исходного порядка в letters:

>>> for key in letters.sorted_keys():
...     print(key, "->", letters[key])
...
a -> 1
b -> 2
c -> 3
d -> 4
e -> 5

>>> letters
OrderedDict([('b', 2), ('d', 4), ('a', 1), ('c', 3), ('e', 5)])


Это всего лишь пример того, насколько полезной может быть функция OrderedDict. Обратите внимание, что вы не можете сделать что-то подобное с обычным словарем:

>>> letters = dict(b=2, d=4, a=1, c=3)
>>> letters.sorted_keys = lambda: sorted(letters.keys())
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    letters.sorted_keys = lambda: sorted(letters.keys())
AttributeError: 'dict' object has no attribute 'sorted_keys'


Если вы попытаетесь динамически добавить пользовательские атрибуты экземпляра в обычный словарь, то получите сообщение AttributeError, сообщающее вам, что в базовом словаре нет соответствующего атрибута. Это связано с тем, что в обычных словарях нет атрибута .__dict__ для хранения новых атрибутов экземпляра.

Объединение и обновление словарей с помощью операторов

Python 3.9 добавил два новых оператора в пространство словаря. Теперь вам нужно объединить (|) и обновить (|=) операторы словаря. Эти операторы также работают с OrderedDict экземплярами:

Python 3.9.0 (default, Oct  5 2020, 17:52:02)
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from collections import OrderedDict

>>> physicists = OrderedDict(newton="1642-1726", einstein="1879-1955")
>>> biologists = OrderedDict(darwin="1809-1882", mendel="1822-1884")

>>> scientists = physicists | biologists
>>> scientists
OrderedDict([
   ('newton', '1642-1726'),
   ('einstein', '1879-1955'),
   ('darwin', '1809-1882'),
   ('mendel', '1822-1884')
])


Как следует из названия, оператор слияния объединяет два словаря в новый, содержащий элементы обоих исходных словарей. Если словари в выражении имеют общие ключи, то значения крайнего правого словаря будут преобладать.

Оператор update удобен, когда у вас есть словарь и вы хотите обновить некоторые его значения без вызова .update():

>>> physicists = OrderedDict(newton="1642-1726", einstein="1879-1955")

>>> physicists_1 = OrderedDict(newton="1642-1726/1727", hawking="1942-2018")
>>> physicists |= physicists_1
>>> physicists
OrderedDict([
    ('newton', '1642-1726/1727'),
    ('einstein', '1879-1955'),
    ('hawking', '1942-2018')
])


В этом примере вы используете оператор обновления словаря для обновления информации о времени жизни Ньютона . Оператор обновляет словарь на месте. Если словарь, предоставляющий обновленные данные, содержит новые ключи, то эти ключи добавляются в конец исходного словаря.

Учитывая производительность

Производительность - важный вопрос в программировании. Обычно важно знать, насколько быстро работает алгоритм или сколько памяти он использует. OrderedDict изначально был закодирован на Python, а затем написан на C, чтобы максимально повысить эффективность его методов и операций. Эти две реализации в настоящее время доступны в стандартной библиотеке. Однако реализация на Python служит альтернативой, если по какой-либо причине недоступна реализация на C.

Обе реализации OrderedDict предполагают использование двусвязного списка для определения порядка элементов. Несмотря на то, что для некоторых операций требуется линейное время, реализация связанного списка в OrderedDict сильно оптимизирована для сохранения быстрого времени работы соответствующих методов словаря. Тем не менее, операции с упорядоченным словарем являются O(1), но с большим постоянным коэффициентом по сравнению с обычными словарями.

В целом, OrderedDict имеет более низкую производительность, чем обычные словари. Вот пример, в котором измеряется время выполнения нескольких операций в обоих классах словарей:

# time_testing.py

from collections import OrderedDict
from time import perf_counter

def average_time(dictionary):
    time_measurements = []
    for _ in range(1_000_000):
        start = perf_counter()
        dictionary["key"] = "value"
        "key" in dictionary
        "missing_key" in dictionary
        dictionary["key"]
        del dictionary["key"]
        end = perf_counter()
        time_measurements.append(end - start)
    return sum(time_measurements) / len(time_measurements) * int(1e9)

ordereddict_time = average_time(OrderedDict.fromkeys(range(1000)))
dict_time = average_time(dict.fromkeys(range(1000)))
gain = ordereddict_time / dict_time

print(f"OrderedDict: {ordereddict_time:.2f} ns")
print(f"dict:        {dict_time:.2f} ns ({gain:.2f}x faster)")


В этом сценарии вы вычисляете average_time(), необходимое для выполнения нескольких распространенных операций с данным словарем. Цикл for использует time.perf_counter() для измерения времени выполнения набора операций. Функция возвращает среднее время в наносекундах, необходимое для выполнения выбранного набора операций.

Примечание: Если вам интересно узнать о других способах определения времени выполнения вашего кода, вы можете ознакомиться с Функциями таймера Python: три способа отслеживания вашего кода.

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

$ python time_testing.py
OrderedDict: 272.93 ns
dict:        197.88 ns (1.38x faster)


Как вы видите в выходных данных, операции с объектами dict выполняются быстрее, чем операции с объектами OrderedDict.

Что касается потребления памяти, OrderedDict экземпляры должны платить за хранение из-за упорядоченного списка ключей. Вот сценарий, который дает вам представление об этой стоимости памяти:

>>> import sys
>>> from collections import OrderedDict

>>> ordereddict_memory = sys.getsizeof(OrderedDict.fromkeys(range(1000)))
>>> dict_memory = sys.getsizeof(dict.fromkeys(range(1000)))
>>> gain = 100 - dict_memory / ordereddict_memory * 100

>>> print(f"OrderedDict: {ordereddict_memory} bytes")
OrderedDict: 85408 bytes

>>> print(f"dict:        {dict_memory} bytes ({gain:.2f}% lower)")
dict:        36960 bytes (56.73% lower)


В этом примере вы используете sys.getsizeof() для измерения объема памяти в байтах двух объектов справочника. В выходных данных вы можете видеть, что обычный словарь занимает меньше памяти, чем его OrderedDict аналог.

Выбор подходящего словаря для задания

Итак, вы узнали о тонких различиях между OrderedDict и dict. Вы узнали, что, несмотря на то, что обычные словари стали упорядоченными структурами данных начиная с версии Python 3.6, все еще есть некоторая ценность в использовании OrderedDict из-за набора полезных функций, которых нет в Python 3.6. dict.

Вот краткое описание наиболее важных различий и особенностей обоих классов, которые вам следует учитывать при принятии решения о том, какой из них использовать:

Feature OrderedDict dict
Key insertion order maintained Yes (since Python 3.1) Yes (since Python 3.6)
Readability and intent signaling regarding the order of items High Low
Control over the order of items High (.move_to_end(), enhanced .popitem()) Low (removing and reinserting items is required)
Operations performance Low High
Memory consumption High Low
Equality tests consider the order of items Yes No
Support for reverse iteration Yes (since Python 3.5) Yes (since Python 3.8)
Ability to append new instance attributes Yes (.__dict__ attribute) No
Support for the merge (|) and update (|=) dictionary operators Yes (since Python 3.9) Yes (since Python 3.9)

В этой таблице приведены некоторые основные различия между OrderedDict и dict, которые следует учитывать, когда вам нужно выбрать класс словаря для решения задачи или реализации определенного алгоритма. В общем, если порядок элементов в словаре важен для корректной работы вашего кода, то в первую очередь следует обратить внимание на OrderedDict.

Построение очереди на основе словаря

Вариант использования, в котором вам следует рассмотреть возможность использования объекта OrderedDict, а не объекта dict, - это когда вам нужно реализовать очередь на основе словаря. Очереди - это распространенные и полезные структуры данных, которые управляют своими элементами в режиме FIFO. Это означает, что вы помещаете новые элементы в конец очереди, а старые элементы выводите из начала очереди.

Обычно очереди реализуют операцию добавления элемента в свой конец, которая известна как операция постановки в очередь. Очереди также реализуют операцию удаления элементов с самого начала, которая известна как операция удаления из очереди.

Чтобы создать очередь на основе словаря, запустите редактор кода или IDE, создайте новый модуль Python с именем queue.py и добавьте в него следующий код:

# queue.py

from collections import OrderedDict

class Queue:
    def __init__(self, initial_data=None, /, **kwargs):
        self.data = OrderedDict()
        if initial_data is not None:
            self.data.update(initial_data)
        if kwargs:
            self.data.update(kwargs)

    def enqueue(self, item):
        key, value = item
        if key in self.data:
            self.data.move_to_end(key)
        self.data[key] = value

    def dequeue(self):
        try:
            return self.data.popitem(last=False)
        except KeyError:
            print("Empty queue")

    def __len__(self):
        return len(self.data)

    def __repr__(self):
        return f"Queue({self.data.items()})"


В Queue сначала вы инициализируете атрибут экземпляра , называемый .data. Этот атрибут содержит пустой упорядоченный словарь, который вы будете использовать для хранения данных. Инициализатор класса принимает первый необязательный аргумент initial_data, который позволяет вам предоставить исходные данные при создании экземпляра класса. Инициализатор также принимает необязательные аргументы ключевого слова (kwargs), чтобы вы могли использовать аргументы ключевого слова в конструкторе.

Затем вы вводите код .enqueue(), который позволяет добавлять пары ключ-значение в очередь. В этом случае вы используете .move_to_end(), если ключ уже существует, и вы используете обычное назначение для новых ключей. Обратите внимание, что для работы этого метода вам необходимо предоставить два элемента tuple или list с допустимой парой ключ-значение.

Реализация .dequeue() использует .popitem() с last равным False для удаления и возврата элементов из начала базового упорядоченного словаря, .data. В этом случае вы используете try ... except блок для обработки KeyError, который возникает при вызове .popitem() в пустом словаре.

Специальный метод , .__len__(), предоставляет необходимую функциональность для получения длины внутреннего упорядоченного словаря, .data. Наконец, специальный метод .__repr__() обеспечивает удобное для пользователя строковое представление очереди при выводе структуры данных на экран.

Вот несколько примеров того, как вы можете использовать Queue:

>>> from queue import Queue

>>> # Create an empty queue
>>> empty_queue = Queue()
>>> empty_queue
Queue(odict_items([]))

>>> # Create a queue with initial data
>>> numbers_queue = Queue([("one", 1), ("two", 2)])
>>> numbers_queue
Queue(odict_items([('one', 1), ('two', 2)]))

>>> # Create a queue with keyword arguments
>>> letters_queue = Queue(a=1, b=2, c=3)
>>> letters_queue
Queue(odict_items([('a', 1), ('b', 2), ('c', 3)]))

>>> # Add items
>>> numbers_queue.enqueue(("three", 3))
>>> numbers_queue
Queue(odict_items([('one', 1), ('two', 2), ('three', 3)]))

>>> # Remove items
>>> numbers_queue.dequeue()
('one', 1)
>>> numbers_queue.dequeue()
('two', 2)
>>> numbers_queue.dequeue()
('three', 3)
>>> numbers_queue.dequeue()
Empty queue


В этом примере кода вы сначала создаете три разных объекта Queue, используя разные подходы. Затем вы используете .enqueue(), чтобы добавить один элемент в конец numbers_queue. Наконец, вы вызываете .dequeue() несколько раз, чтобы удалить все элементы из numbers_queue. Обратите внимание, что при последнем вызове .dequeue() на экран выводится сообщение, информирующее вас о том, что очередь уже пуста.

Заключение

В течение многих лет словари Python представляли собой неупорядоченные структуры данных. Это выявило необходимость в упорядоченном словаре, который помогает в ситуациях, когда важен порядок элементов . Поэтому разработчики Python создали OrderedDict,, который был специально разработан для упорядочивания элементов.

В Python 3.6 появилась новая функция для обычных словарей. Теперь они также запоминают порядок элементов. После этого добавления большинство программистов на Python задаются вопросом, стоит ли им по-прежнему использовать OrderedDict.

В этом уроке вы узнали:

  • Как создавать и использовать OrderedDict объекты в вашем коде
  • В чем основные различия между OrderedDict и dict
  • В чем плюсы и минусы использования OrderedDict против dict

Теперь вы в лучшем положении, чтобы принять обоснованное решение о том, использовать ли dict или OrderedDict, если вашему коду нужен упорядоченный словарь.

В этом руководстве вы написали пример того, как реализовать очередь на основе словаря, который является примером использования, показывающим, что OrderedDict все еще может быть полезен в ваших ежедневных приключениях по написанию кода на Python.

<статус завершения article-slug="python-ordereddict" class="btn-group mb-0" data-api-article-bookmark-url="/api/v1/статьи/python-ordereddict/bookmark/" статус завершения data-api-article-url="/api/v1/articles/python-ordereddict/завершение_статуса/"> <поделиться-кнопка bluesky-text="Интересная статья о Python от @realpython.com:" email-body="Ознакомьтесь с этой статьей о Python:%0A%0AOrderedDict против dict в Python: Подходящий инструмент для работы" email-subject="Статья на Python для вас" twitter-text="Интересная статья на #Python от @realpython:" url="https://realpython.com/python-ordereddict/" url-title="OrderedDict против dict в Python: Подходящий инструмент для работы">

Смотрите сейчас, к этому уроку прилагается соответствующий видеокурс, созданный командой Real Python. Посмотрите его вместе с письменным руководством, чтобы углубить свое понимание: Использование OrderedDict в Python

Back to Top