Коллекции Python: Широкий выбор специализированных типов данных

Оглавление

Модуль Python collections предоставляет богатый набор специализированных контейнерных типов данных, тщательно разработанных для решения конкретных задач программирования эффективным способом на языке Python. Модуль также предоставляет классы-оболочки, которые делают более безопасным создание пользовательских классов, которые ведут себя аналогично встроенным типам dict, list, и str.

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

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

  • Написать читаемый и явный код с namedtuple
  • Создавать эффективные очереди и стеки с помощью deque
  • Быстро подсчитайте объектов с помощью Counter
  • Обработать отсутствующие ключи словаря с помощью defaultdict
  • Гарантировать порядок ввода ключей с OrderedDict
  • Управление несколькими словарями как единым целым с ChainMap

Чтобы лучше понять типы данных и классы в collections, вы должны знать основы работы со встроенными типами данных Python, такими как списки, кортежи и словари. Кроме того, последняя часть статьи требует некоторых базовых знаний о объектно-ориентированном программировании на Python.

Скачать бесплатно: Ознакомьтесь с примером главы из книги "Приемы работы с Python: Книга", в которой показаны лучшие практики Python на простых примерах, которые вы можете подайте заявку немедленно, чтобы написать более красивый + Pythonic код.

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

Еще в Python 2.4, Раймонд Хеттингер добавил новый модуль под названием collections в стандартную библиотеку. Цель состояла в том, чтобы предоставить различные специализированные типы данных для сбора данных для решения конкретных задач программирования.

В то время collections включал только одну структуру данных, deque, которая была специально разработана как двойная-завершенная очередь, которая поддерживает эффективные операции добавления и извлечения на обоих концах последовательности. С этого момента несколько модулей стандартной библиотеки использовали преимущества deque для повышения производительности своих классов и структур. Вот несколько выдающихся примеров queue и threading.

Со временем модуль пополнился несколькими специализированными типами контейнерных данных:

Data type Python version Description
deque 2.4 A sequence-like collection that supports efficient addition and removal of items from either end of the sequence
defaultdict 2.5 A dictionary subclass for constructing default values for missing keys and automatically adding them to the dictionary
namedtuple() 2.6 A factory function for creating subclasses of tuple that provides named fields that allow accessing items by name while keeping the ability to access items by index
OrderedDict 2.7, 3.1 A dictionary subclass that keeps the key-value pairs ordered according to when the keys are inserted
Counter 2.7, 3.1 A dictionary subclass that supports convenient counting of unique items in a sequence or iterable
ChainMap 3.3 A dictionary-like class that allows treating a number of mappings as a single dictionary object

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

Class Description
UserDict A wrapper class around a dictionary object that facilitates subclassing dict
UserList A wrapper class around a list object that facilitates subclassing list
UserString A wrapper class around a string object that facilitates subclassing string

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

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

Улучшение читаемости кода: namedtuple()

namedtuple() в Python - это заводская функция, которая позволяет создавать tuple подклассов с именованными полями. Эти поля предоставляют вам прямой доступ к значениям в заданном именованном кортеже, используя точечное обозначение, как в obj.attr.

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

Примечание: Ознакомьтесь с Написанием кода на Python и его очисткой с помощью namedtuple для более глубокого понимания того, как использовать namedtuple в Python.

Подкласс кортежей с именованными полями, к которым разработчики могут обращаться с помощью точечной записи, казался желательной функцией еще в Python 2.6. Это источник namedtuple(). Подклассы кортежей, которые вы можете создать с помощью этой функции, значительно улучшают читаемость кода, если сравнить их с обычными кортежами.

Чтобы рассмотреть проблему удобочитаемости кода в перспективе, рассмотрим divmod(). Эта встроенная функция принимает два (несложных) числа и возвращает кортеж с частным и остатком, которые получаются в результате целочисленного деления входных значений:

>>> divmod(12, 5)
(2, 2)


Это прекрасно работает. Однако читабелен ли этот результат? Можете ли вы сказать, что означает каждое число в выходных данных? К счастью, Python предлагает способ улучшить это. Вы можете закодировать пользовательскую версию divmod() с явным результатом, используя namedtuple:

>>> from collections import namedtuple

>>> def custom_divmod(x, y):
...     DivMod = namedtuple("DivMod", "quotient remainder")
...     return DivMod(*divmod(x, y))
...

>>> result = custom_divmod(12, 5)
>>> result
DivMod(quotient=2, remainder=2)

>>> result.quotient
2
>>> result.remainder
2


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

Чтобы создать новый подкласс кортежей с помощью namedtuple(), вам нужны два обязательных аргумента:

  1. typename это имя класса, который вы создаете. Это должна быть строка с допустимым идентификатором Python.
  2. field_names это список имен полей, которые вы будете использовать для доступа к элементам в результирующем кортеже. Это может быть:
    • Набор повторяемых строк, таких как ["field1", "field2", ..., "fieldN"]
    • Строка с именами полей, разделенными пробелами, например "field1 field2 ... fieldN"
    • Строка с именами полей, разделенными запятыми, например "field1, field2, ..., fieldN"

Например, вот различные способы создания образца 2D Point с двумя координатами (x и y) с помощью namedtuple():

>>> from collections import namedtuple

>>> # Use a list of strings as field names
>>> Point = namedtuple("Point", ["x", "y"])
>>> point = Point(2, 4)
>>> point
Point(x=2, y=4)

>>> # Access the coordinates
>>> point.x
2
>>> point.y
4
>>> point[0]
2

>>> # Use a generator expression as field names
>>> Point = namedtuple("Point", (field for field in "xy"))
>>> Point(2, 4)
Point(x=2, y=4)

>>> # Use a string with comma-separated field names
>>> Point = namedtuple("Point", "x, y")
>>> Point(2, 4)
Point(x=2, y=4)

>>> # Use a string with space-separated field names
>>> Point = namedtuple("Point", "x y")
>>> Point(2, 4)
Point(x=2, y=4)


В этих примерах вы сначала создаете Point, используя list имен полей. Затем вы создаете экземпляр Point, чтобы создать объект point. Обратите внимание, что вы можете получить доступ к x и y по имени поля, а также по индексу.

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

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

>>> from collections import namedtuple

>>> # Define default values for fields
>>> Person = namedtuple("Person", "name job", defaults=["Python Developer"])
>>> person = Person("Jane")
>>> person
Person(name='Jane', job='Python Developer')

>>> # Create a dictionary from a named tuple
>>> person._asdict()
{'name': 'Jane', 'job': 'Python Developer'}

>>> # Replace the value of a field
>>> person = person._replace(job="Web Developer")
>>> person
Person(name='Jane', job='Web Developer')


Здесь вы сначала создаете класс Person, используя namedtuple(). На этот раз вы используете необязательный аргумент defaults, который принимает последовательность значений по умолчанию для полей кортежа. Обратите внимание, что namedtuple() применяет значения по умолчанию к крайним правым полям.

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

Наконец, вы используете ._replace() для замены исходного значения job. Этот метод не обновляет кортеж вместо , а возвращает новый именованный кортеж с новым значением, сохраненным в соответствующем поле. У вас есть представление о том, почему ._replace() возвращает новый именованный кортеж?

Создание эффективных очередей и стеков: deque

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

Примечание: Слово deque произносится как “палуба” и означает double-eнайдено что-тонапример.

В Python операции добавления и извлечения в начале или слева от объектов list неэффективны, поскольку O(n) требуют времени. Эти операции особенно дороги, если вы работаете с большими списками, потому что Python должен перемещать все элементы вправо, чтобы вставлять новые элементы в начало списка.

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

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

Возьмем в качестве примера очередь. Он управляет элементами в режиме "Первый вход/первый выход". ( FIFO) мода. Это работает как конвейер, в который вы помещаете новые элементы на одном конце конвейера и извлекаете старые элементы с другого конца. Добавление элемента в конец очереди называется операцией постановка в очередь. Удаление элемента из начала очереди называется Удалением из очереди.

Примечание: Ознакомьтесь с Python's deque: Внедрение эффективных очередей и стеков для подробного изучения использования deque в вашем коде на Python.

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

Вот как вы можете эмулировать процесс, используя объект deque:

>>> from collections import deque

>>> ticket_queue = deque()
>>> ticket_queue
deque([])

>>> # People arrive to the queue
>>> ticket_queue.append("Jane")
>>> ticket_queue.append("John")
>>> ticket_queue.append("Linda")

>>> ticket_queue
deque(['Jane', 'John', 'Linda'])

>>> # People bought their tickets
>>> ticket_queue.popleft()
'Jane'
>>> ticket_queue.popleft()
'John'
>>> ticket_queue.popleft()
'Linda'

>>> # No people on the queue
>>> ticket_queue.popleft()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: pop from an empty deque


Здесь вы сначала создаете пустой объект deque для представления очереди людей. Чтобы поместить пользователя в очередь, вы можете использовать .append(),, который добавляет элементы в правый конец списка. Чтобы удалить пользователя из очереди, вы используете .popleft(),, который удаляет и возвращает элементы в левой части списка.

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

Инициализатор deque принимает два необязательных аргумента:

  1. iterable содержит итерацию, которая служит в качестве инициализатора.
  2. maxlen содержит целое число, которое определяет максимальную длину deque.

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

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

>>> from collections import deque

>>> recent_files = deque(["core.py", "README.md", "__init__.py"], maxlen=3)

>>> recent_files.appendleft("database.py")
>>> recent_files
deque(['database.py', 'core.py', 'README.md'], maxlen=3)

>>> recent_files.appendleft("requirements.txt")
>>> recent_files
deque(['requirements.txt', 'database.py', 'core.py'], maxlen=3)


Как только файл deque достигнет максимального размера (в данном случае три файла), добавление нового файла в конец файла deque автоматически приведет к удалению файла в противоположном конце. Если вы не укажете значение maxlen, то список может увеличиться до произвольного количества элементов.

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

>>> from collections import deque

>>> # Use different iterables to create deques
>>> deque((1, 2, 3, 4))
deque([1, 2, 3, 4])

>>> deque([1, 2, 3, 4])
deque([1, 2, 3, 4])

>>> deque("abcd")
deque(['a', 'b', 'c', 'd'])

>>> # Unlike lists, deque doesn't support .pop() with arbitrary indices
>>> deque("abcd").pop(2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: pop() takes no arguments (1 given)

>>> # Extend an existing deque
>>> numbers = deque([1, 2])
>>> numbers.extend([3, 4, 5])
>>> numbers
deque([1, 2, 3, 4, 5])

>>> numbers.extendleft([-1, -2, -3, -4, -5])
>>> numbers
deque([-5, -4, -3, -2, -1, 1, 2, 3, 4, 5])

>>> # Insert an item at a given position
>>> numbers.insert(5, 0)
>>> numbers
deque([-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5])


В этих примерах вы сначала создаете deques, используя различные типы iterables для их инициализации. Одно из отличий между deque и list заключается в том, что deque.pop() не поддерживает отображение элемента с заданным индексом.

Обратите внимание, что deque предоставляет родственные методы для .append(), .pop(), и .extend() с суффиксом left, указывающим на то, что они выполняют соответствующую операцию в левом конце базового deque.

Команды Deques также поддерживают операции с последовательностью:

Method Description
.clear() Remove all the elements from a deque
.copy() Create a shallow copy of a deque
.count(x) Count the number of deque elements equal to x
.remove(value) Remove the first occurrence of value

Еще одной интересной особенностью deques является возможность поворачивать их элементы с помощью .rotate():

>>> from collections import deque

>>> ordinals = deque(["first", "second", "third"])
>>> ordinals.rotate()
>>> ordinals
deque(['third', 'first', 'second'])

>>> ordinals.rotate(2)
>>> ordinals
deque(['first', 'second', 'third'])

>>> ordinals.rotate(-2)
>>> ordinals
deque(['third', 'first', 'second'])

>>> ordinals.rotate(-1)
>>> ordinals
deque(['first', 'second', 'third'])


При этом методе число шагов n сдвигается вправо. Значение n по умолчанию равно 1. Если вы укажете отрицательное значение для n, то произойдет поворот влево.

Наконец, вы можете использовать индексы для доступа к элементам в deque, но вы не можете срезать элемент deque:

>>> from collections import deque

>>> ordinals = deque(["first", "second", "third"])
>>> ordinals[1]
'second'

>>> ordinals[0:2]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: sequence index must be integer, not 'slice'


Deques поддерживают индексацию, но, что интересно, они не поддерживают разбиение на части. При попытке извлечь фрагмент из существующего deque вы получаете TypeError. Это связано с тем, что выполнение операции среза в связанном списке было бы неэффективным, поэтому операция недоступна.

Обработка недостающих ключей: defaultdict

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

>>> favorites = {"pet": "dog", "color": "blue", "language": "Python"}

>>> favorites["fruit"]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'fruit'


Существует несколько способов обойти эту проблему. Например, вы можете использовать .setdefault(). Этот метод принимает ключ в качестве аргумента. Если ключ существует в словаре, то он возвращает соответствующее значение. В противном случае метод вставляет ключ, присваивает ему значение по умолчанию и возвращает это значение:

>>> favorites = {"pet": "dog", "color": "blue", "language": "Python"}

>>> favorites.setdefault("fruit", "apple")
'apple'

>>> favorites
{'pet': 'dog', 'color': 'blue', 'language': 'Python', 'fruit': 'apple'}

>>> favorites.setdefault("pet", "cat")
'dog'

>>> favorites
{'pet': 'dog', 'color': 'blue', 'language': 'Python', 'fruit': 'apple'}


В этом примере вы используете .setdefault() для создания значения по умолчанию для fruit. Поскольку этот ключ не существует в favorites, .setdefault(), создает его и присваивает ему значение apple. Если вы вызовете .setdefault() с существующим ключом, то вызов не повлияет на словарь, и ваш ключ будет содержать исходное значение вместо значения по умолчанию.

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

>>> favorites = {"pet": "dog", "color": "blue", "language": "Python"}

>>> favorites.get("fruit", "apple")
'apple'

>>> favorites
{'pet': 'dog', 'color': 'blue', 'language': 'Python'}


Здесь .get() возвращает apple, поскольку ключ отсутствует в базовом словаре. Однако .get() не создает новый ключ для вас.

Поскольку обработка отсутствующих ключей в словарях является распространенной потребностью, collections в Python также имеется инструмент для этого. Тип defaultdict является подклассом dict, предназначенным для того, чтобы помочь вам найти недостающие ключи.

Примечание: Ознакомьтесь с Использованием типа Python defaultdict для обработки отсутствующих ключей для более глубокого понимания того, как использовать defaultdict.

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

Чтобы обеспечить его функциональность, defaultdict сохраняет входную функцию в виде .default_factory, а затем переопределяет .__missing__() для автоматического вызова функции и генерации значения по умолчанию при обращении к любым отсутствующим клавишам.

Вы можете использовать любой вызываемый объект для инициализации ваших объектов defaultdict. Например, с помощью int() вы можете создать подходящий счетчик для подсчета различных объектов:

>>> from collections import defaultdict

>>> counter = defaultdict(int)
>>> counter
defaultdict(<class 'int'>, {})
>>> counter["dogs"]
0
>>> counter
defaultdict(<class 'int'>, {'dogs': 0})

>>> counter["dogs"] += 1
>>> counter["dogs"] += 1
>>> counter["dogs"] += 1
>>> counter["cats"] += 1
>>> counter["cats"] += 1
>>> counter
defaultdict(<class 'int'>, {'dogs': 3, 'cats': 2})


В этом примере вы создаете пустой defaultdict с int() в качестве первого аргумента. Когда вы обращаетесь к несуществующему ключу, словарь автоматически вызывает int(), который возвращает 0 в качестве значения по умолчанию для имеющегося ключа. Такого рода объекты defaultdict весьма полезны, когда дело доходит до подсчета данных в Python.

Еще один распространенный вариант использования defaultdict - это группировка объектов. В этом случае удобной функцией factory является list():

>>> from collections import defaultdict

>>> pets = [
...     ("dog", "Affenpinscher"),
...     ("dog", "Terrier"),
...     ("dog", "Boxer"),
...     ("cat", "Abyssinian"),
...     ("cat", "Birman"),
... ]

>>> group_pets = defaultdict(list)

>>> for pet, breed in pets:
...     group_pets[pet].append(breed)
...

>>> for pet, breeds in group_pets.items():
...     print(pet, "->", breeds)
...
dog -> ['Affenpinscher', 'Terrier', 'Boxer']
cat -> ['Abyssinian', 'Birman']


В этом примере у вас есть необработанные данные о домашних животных и их породе, и вам нужно сгруппировать их по домашним животным. Чтобы сделать это, вы используете list() как .default_factory при создании экземпляра defaultdict. Это позволяет вашему словарю автоматически создавать пустой список ([]) в качестве значения по умолчанию для каждого пропущенного ключа, к которому вы обращаетесь. Затем вы используете этот список для хранения пород ваших домашних животных.

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

Упорядочивание ваших словарей: OrderedDict

Иногда вам нужно, чтобы ваши словари запоминали порядок, в котором вставляются пары ключ-значение. Обычные словари Python были неупорядоченными структуры данных в течение многих лет. Итак, еще в 2008 году в PEP 372 была представлена идея добавления нового класса словаря в collections.

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

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

Примечание: Ознакомьтесь с OrderedDict против dict в Python: Подходящий инструмент для работы для более глубокого погружения в Python OrderedDict и почему вам следует рассмотреть возможность его использования.

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

>>> from collections import OrderedDict

>>> life_stages = OrderedDict()

>>> life_stages["childhood"] = "0-9"
>>> life_stages["adolescence"] = "9-18"
>>> life_stages["adulthood"] = "18-65"
>>> life_stages["old"] = "+65"

>>> for stage, years in life_stages.items():
...     print(stage, "->", years)
...
childhood -> 0-9
adolescence -> 9-18
adulthood -> 18-65
old -> +65


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

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

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

Изначально эта функция считалась деталью реализации, и в документации советовали не полагаться на нее. Однако, начиная с Python 3.7, функция официально является частью спецификации языка. Итак, какой смысл использовать OrderedDict?

У OrderedDict есть некоторые особенности, которые по-прежнему делают его ценным:

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

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

Итак, теперь пришло время увидеть некоторые из этих интересных функций OrderedDict в действии:

>>> from collections import OrderedDict

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

>>> # Move b to the right end
>>> letters.move_to_end("b")
>>> letters
OrderedDict([('d', 4), ('a', 1), ('c', 3), ('b', 2)])

>>> # Move b to the left end
>>> letters.move_to_end("b", last=False)
>>> letters
OrderedDict([('b', 2), ('d', 4), ('a', 1), ('c', 3)])

>>> # Sort letters by key
>>> for key in sorted(letters):
...     letters.move_to_end(key)
...

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


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

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

>>> from collections import OrderedDict

>>> # Regular dictionaries compare the content only
>>> letters_0 = dict(a=1, b=2, c=3, d=4)
>>> letters_1 = dict(b=2, a=1, d=4, c=3)
>>> letters_0 == letters_1
True

>>> # Ordered dictionaries compare content and order
>>> letters_0 = OrderedDict(a=1, b=2, c=3, d=4)
>>> letters_1 = OrderedDict(b=2, a=1, d=4, c=3)
>>> letters_0 == letters_1
False

>>> letters_2 = OrderedDict(a=1, b=2, c=3, d=4)
>>> letters_0 == letters_2
True


Здесь letters_1 имеет порядок элементов, отличный от letters_0. При использовании обычных словарей эта разница не имеет значения, и оба словаря сравниваются одинаково. С другой стороны, когда вы используете упорядоченные словари, letters_0 и letters_1 не равны. Это связано с тем, что тесты на равенство между упорядоченными словарями учитывают содержимое, а также порядок элементов.

Подсчет объектов за один раз: Counter

Подсчет объектов - обычная операция в программировании. Допустим, вам нужно подсчитать, сколько раз данный элемент появляется в списке или повторяется. Если ваш список короткий, то подсчет его элементов может быть простым и быстрым. Если у вас длинный список, то подсчитать количество пунктов будет сложнее.

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

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

Вот пример, в котором подсчитываются буквы в слове "mississippi" с помощью обычного словаря и цикла for:

>>> word = "mississippi"
>>> counter = {}

>>> for letter in word:
...     if letter not in counter:
...         counter[letter] = 0
...     counter[letter] += 1
...

>>> counter
{'m': 1, 'i': 4, 's': 4, 'p': 2}


Цикл выполняет итерацию по буквам в word. Условный оператор проверяет, нет ли этих букв в словаре, и соответствующим образом обнуляет количество букв. Последним шагом является увеличение количества букв по мере прохождения цикла.

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

>>> from collections import defaultdict

>>> counter = defaultdict(int)

>>> for letter in "mississippi":
...     counter[letter] += 1
...

>>> counter
defaultdict(<class 'int'>, {'m': 1, 'i': 4, 's': 4, 'p': 2})


В этом примере вы создаете объект defaultdict и инициализируете его с помощью int(). Используя int() в качестве заводской функции, базовый словарь по умолчанию автоматически создает недостающие ключи и удобно инициализирует их нулем. Затем вы увеличиваете значение текущего ключа, чтобы вычислить окончательное количество букв в "mississippi".

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

Вот как вы можете написать пример "mississippi", используя Counter:

>>> from collections import Counter

>>> Counter("mississippi")
Counter({'i': 4, 's': 4, 'p': 2, 'm': 1})


Ничего себе! Это было быстро! Всего одна строка кода - и готово. В этом примере Counter повторяется по "mississippi", создавая словарь с буквами в качестве ключей и их частотностью в качестве значений.

Примечание: Ознакомьтесь с Python's Counter: Pythonic способ подсчета объектов для более глубокого погружения в Counter и как его использовать для эффективного подсчета объектов.

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

>>> from collections import Counter

>>> Counter([1, 1, 2, 3, 3, 3, 4])
Counter({3: 3, 1: 2, 2: 1, 4: 1})

>>> Counter(([1], [1]))
Traceback (most recent call last):
  ...
TypeError: unhashable type: 'list'


Целые числа могут быть хэшированы, поэтому Counter работает корректно. С другой стороны, списки не могут быть хэшированы, поэтому Counter завершается ошибкой с TypeError.

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

Примечание: В Counter высоко оптимизированная функция C обеспечивает функциональность подсчета. Если эта функция по какой-либо причине недоступна, то класс использует эквивалентную, но менее эффективную функцию Python.

Поскольку Counter является подклассом dict, их интерфейсы в основном одинаковы. Однако есть некоторые тонкие различия. Первое отличие заключается в том, что Counter не реализует .fromkeys(). Это позволяет избежать несоответствий, таких как Counter.fromkeys("abbbc", 2), в которых каждая буква будет иметь начальное значение 2, независимо от реального значения, которое она имеет во входной итеративной переменной.

Второе отличие заключается в том, что .update() не заменяет количество (значение) существующего объекта (ключа) новым количеством. Это суммирует оба значения вместе:

>>> from collections import Counter

>>> letters = Counter("mississippi")
>>> letters
Counter({'i': 4, 's': 4, 'p': 2, 'm': 1})

>>> # Update the counts of m and i
>>> letters.update(m=3, i=4)
>>> letters
Counter({'i': 8, 'm': 4, 's': 4, 'p': 2})

>>> # Add a new key-count pair
>>> letters.update({"a": 2})
>>> letters
Counter({'i': 8, 'm': 4, 's': 4, 'p': 2, 'a': 2})

>>> # Update with another counter
>>> letters.update(Counter(["s", "s", "p"]))
>>> letters
Counter({'i': 8, 's': 6, 'm': 4, 'p': 3, 'a': 2})


Здесь вы обновляете счетчик для m и i. Теперь эти буквы содержат сумму их начального количества плюс значение, которое вы передали им через .update(). Если вы используете ключ, которого нет в исходном счетчике, то .update() создает новый ключ с соответствующим значением. Наконец, .update() принимает повторяющиеся значения, сопоставления, аргументы ключевых слов, а также другие счетчики.

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

Еще одно различие между Counter и dict заключается в том, что доступ к отсутствующему ключу возвращает 0 вместо вызова KeyError:

>>> from collections import Counter

>>> letters = Counter("mississippi")
>>> letters["a"]
0


Такое поведение сигнализирует о том, что количество объектов, которые не существуют в счетчике, равно нулю. В этом примере буквы "a" нет в исходном слове, поэтому ее количество равно 0.

В Python Counter также полезно для эмуляции мультисети или пакета. Мультинаборы похожи на наборов, но они допускают несколько экземпляров данного элемента. Количество экземпляров элемента известно как его кратность. Например, у вас может быть такой мультимножество, как {1, 1, 2, 3, 3, 3, 4, 4}.

Когда вы используете Counter для эмуляции мультинаборов, ключи представляют элементы, а значения - их соответствующую кратность:

>>> from collections import Counter

>>> multiset = Counter([1, 1, 2, 3, 3, 3, 4, 4])
>>> multiset
Counter({1: 2, 2: 1, 3: 3, 4: 2})

>>> multiset.keys() == {1, 2, 3, 4}
True


Здесь ключи multiset эквивалентны набору Python. Значения содержат кратность каждого элемента в наборе.

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

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

>>> from collections import Counter

>>> inventory = Counter(dogs=23, cats=14, pythons=7)

>>> adopted = Counter(dogs=2, cats=5, pythons=1)
>>> inventory.subtract(adopted)
>>> inventory
Counter({'dogs': 21, 'cats': 9, 'pythons': 6})

>>> new_pets = {"dogs": 4, "cats": 1}
>>> inventory.update(new_pets)
>>> inventory
Counter({'dogs': 25, 'cats': 10, 'pythons': 6})

>>> inventory = inventory - Counter(dogs=2, cats=3, pythons=1)
>>> inventory
Counter({'dogs': 23, 'cats': 7, 'pythons': 5})

>>> new_pets = {"dogs": 4, "pythons": 2}
>>> inventory += new_pets
>>> inventory
Counter({'dogs': 27, 'cats': 7, 'pythons': 7})


Вот и отлично! Теперь вы можете вести учет своих питомцев, используя Counter. Обратите внимание, что вы можете использовать .subtract() и .update() для вычитания и сложения чисел или кратностей. Вы также можете использовать операторы сложения (+) и вычитания (-).

С Counter объектами в виде мультинаборов в Python можно сделать гораздо больше, так что дерзайте и попробуйте!

Объединение словарей в цепочку: ChainMap

В Python ChainMap сгруппировано несколько словарей и других сопоставлений, чтобы создать единый объект, который работает почти как обычный словарь. Другими словами, требуется несколько отображений, которые логически представляются как одно целое.

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

Примечание: Ознакомьтесь с Python's ChainMap: Эффективное управление несколькими контекстами для более глубокого изучения использования ChainMap в вашем коде на Python.

При работе с объектами ChainMap у вас может быть несколько словарей с уникальными или повторяющимися ключами.

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

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

Например, предположим, что вы работаете с приложением с интерфейсом командной строки (CLI). Приложение позволяет пользователю использовать прокси-сервис для подключения к Интернету. Приоритетами настроек являются:

  1. Параметры командной строки (--proxy, -p)
  2. Файлы локальной конфигурации в домашнем каталоге пользователя
  3. Глобальная конфигурация прокси-сервера

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

>>> from collections import ChainMap

>>> cmd_proxy = {}  # The user doesn't provide a proxy
>>> local_proxy = {"proxy": "proxy.local.com"}
>>> global_proxy = {"proxy": "proxy.global.com"}

>>> config = ChainMap(cmd_proxy, local_proxy, global_proxy)
>>> config["proxy"]
'proxy.local.com'


ChainMap позволяет вам определить соответствующий приоритет для настройки прокси-сервера приложения. При поиске ключа выполняется поиск cmd_proxy, затем local_proxy и, наконец, global_proxy, возвращая первый экземпляр ключа, который находится под рукой. В этом примере пользователь не предоставляет прокси-сервер в командной строке, поэтому ваше приложение использует прокси-сервер в local_proxy.

В целом, объекты ChainMap ведут себя аналогично обычным объектам dict. Однако у них есть некоторые дополнительные возможности. Например, у них есть атрибут .maps public, который содержит внутренний список сопоставлений:

>>> from collections import ChainMap

>>> numbers = {"one": 1, "two": 2}
>>> letters = {"a": "A", "b": "B"}

>>> alpha_nums = ChainMap(numbers, letters)
>>> alpha_nums.maps
[{'one': 1, 'two': 2}, {'a': 'A', 'b': 'B'}]


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

Кроме того, ChainMap предоставляет .new_child() метод и .parents свойство:

>>> from collections import ChainMap

>>> dad = {"name": "John", "age": 35}
>>> mom = {"name": "Jane", "age": 31}
>>> family = ChainMap(mom, dad)
>>> family
ChainMap({'name': 'Jane', 'age': 31}, {'name': 'John', 'age': 35})

>>> son = {"name": "Mike", "age": 0}
>>> family = family.new_child(son)

>>> for person in family.maps:
...     print(person)
...
{'name': 'Mike', 'age': 0}
{'name': 'Jane', 'age': 31}
{'name': 'John', 'age': 35}

>>> family.parents
ChainMap({'name': 'Jane', 'age': 31}, {'name': 'John', 'age': 35})


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

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

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

>>> from collections import ChainMap

>>> numbers = {"one": 1, "two": 2}
>>> letters = {"a": "A", "b": "B"}

>>> alpha_nums = ChainMap(numbers, letters)
>>> alpha_nums
ChainMap({'one': 1, 'two': 2}, {'a': 'A', 'b': 'B'})

>>> # Add a new key-value pair
>>> alpha_nums["c"] = "C"
>>> alpha_nums
ChainMap({'one': 1, 'two': 2, 'c': 'C'}, {'a': 'A', 'b': 'B'})

>>> # Pop a key that exists in the first dictionary
>>> alpha_nums.pop("two")
2
>>> alpha_nums
ChainMap({'one': 1, 'c': 'C'}, {'a': 'A', 'b': 'B'})

>>> # Delete keys that don't exist in the first dict but do in others
>>> del alpha_nums["a"]
Traceback (most recent call last):
  ...
KeyError: "Key not found in the first mapping: 'a'"

>>> # Clear the dictionary
>>> alpha_nums.clear()
>>> alpha_nums
ChainMap({}, {'a': 'A', 'b': 'B'})


Эти примеры показывают, что операции изменения с объектом ChainMap влияют только на первое отображение во внутреннем списке. Это важная деталь, которую следует учитывать при работе с ChainMap.

Сложность заключается в том, что на первый взгляд может показаться, что можно изменить любую существующую пару ключ-значение в заданном ChainMap. Однако вы можете изменять только пары ключ-значение в первом сопоставлении, если только вы не используете .maps для прямого доступа к другим сопоставлениям в списке и их изменения.

Настройка встроенных модулей: UserString, UserList, и UserDict

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

Python collections предоставляет три удобных класса-оболочки, которые имитируют поведение встроенных типов данных:

  1. UserString
  2. UserList
  3. UserDict

В настоящее время разработчики часто задаются вопросом, есть ли смысл использовать UserString, UserList, и UserDict, когда им нужно настроить поведение встроенных типов. Ответ - да.

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

Например, предположим, что вам нужен словарь, который автоматически записывает ключи в нижний регистр при их вставке. Вы могли бы создать подкласс dict и переопределить .__setitem__() таким образом, каждый раз, когда вы вставляете ключ, в словаре имя ключа указывается в нижнем регистре:

>>> class LowerDict(dict):
...     def __setitem__(self, key, value):
...         key = key.lower()
...         super().__setitem__(key, value)
...

>>> ordinals = LowerDict({"FIRST": 1, "SECOND": 2})
>>> ordinals["THIRD"] = 3
>>> ordinals.update({"FOURTH": 4})

>>> ordinals
{'FIRST': 1, 'SECOND': 2, 'third': 3, 'FOURTH': 4}

>>> isinstance(ordinals, dict)
True


Этот словарь работает корректно, когда вы вставляете новые ключи, используя назначение в стиле словаря с квадратными скобками ([]). Однако это не работает, когда вы передаете начальный словарь конструктору класса или когда вы используете .update(). Это означает, что вам потребуется переопределить .__init__(), .update(), и, возможно, некоторые другие методы для корректной работы вашего пользовательского словаря.

Теперь взглянем на тот же словарь, но используя UserDict в качестве базового класса:

>>> from collections import UserDict

>>> class LowerDict(UserDict):
...     def __setitem__(self, key, value):
...         key = key.lower()
...         super().__setitem__(key, value)
...

>>> ordinals = LowerDict({"FIRST": 1, "SECOND": 2})
>>> ordinals["THIRD"] = 3
>>> ordinals.update({"FOURTH": 4})

>>> ordinals
{'first': 1, 'second': 2, 'third': 3, 'fourth': 4}

>>> isinstance(ordinals, dict)
False


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

UserDict хранит обычный словарь в атрибуте экземпляра с именем .data. Затем он реализует все свои методы на основе этого словаря. UserList и UserString работают одинаково, но их атрибут .data содержит объекты list и str, соответственно.

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

В общем, вам следует использовать UserDict, UserList, и UserString, когда вам нужен класс, который действует почти идентично базовому встроенному классу в оболочке, и вы хотите настроить некоторую часть его стандартных функциональных возможностей.

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

Возможность прямого наследования от встроенных типов в значительной степени вытеснила использование UserDict, UserList, и UserString. Однако внутренняя реализация встроенных типов затрудняет безопасное наследование от них без переписывания значительного объема кода. В большинстве случаев безопаснее использовать соответствующий класс из collections. Это избавит вас от ряда проблем и странного поведения.

Заключение

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

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

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

  • Написать читаемый и явный код, используя namedtuple
  • Создавать эффективные очереди и стеки, используя deque
  • Подсчитывайте объекты эффективно, используя Counter
  • Обработать недостающие ключи словаря с помощью defaultdict
  • Запомните порядок ввода клавиш с OrderedDict
  • Объедините несколько словарей в одном представлении с помощью ChainMap

Вы также узнали о трех удобных классах-оболочках: UserDict, UserList, и UserString. Эти классы удобны, когда вам нужно создать пользовательские классы, которые имитируют поведение встроенных типов dict, list, и str.

<статус завершения article-slug="python-collections-module" class="btn-group mb-0" data-api-article-bookmark-url="/api/v1/articles/python-collections-module/bookmark/" data-api-article-завершение-статус-url="/api/версия 1/статьи/python-коллекции-модуль/завершение_статуса/"> <кнопка поделиться bluesky-text="Интересная статья на #Python от @realpython.com :" email-body="Ознакомьтесь с этой статьей о Python: коллекции%0A%0APython: Множество специализированных типов данных" email-subject="Статья о Python для вас" twitter-text="Интересная #Статья о Python от @realpython:" url="https://realpython.com/python-collections-module /" url-title="Коллекции Python: набор специализированных типов данных"> Back to Top