Tutorial

Использование модуля collections в Python 3

Python

Автор выбрал COVID-19 Relief Fund для получения пожертвования в рамках программы Write for DOnations.

Введение

В Python 3 имеется множество встроенных структур данных, включая кортежи, словари и списки. Структуры данных дают нам возможности организации и хранения данных. Модуль collections помогает эффективно заполнять структуры данных и управлять ими.

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

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

Предварительные требования

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

Добавление именованных полей в кортежи

Кортежи Python представляют собой неизменяемые упорядоченные последовательности элементов. Кортежи часто используются для представления табличных данных, например, строк из файла CSV или базы данных SQL. Аквариум может отслеживать список рыбок как серию кортежей.

Кортеж отдельной рыбки:

("Sammy", "shark", "tank-a")

Этот кортеж состоит из трех строковых элементов.

Хотя это полезно, данный массив четко не показывает, что означает каждое из полей. На самом деле элемент 0 — это имя, элемент 1 — это вид, а элемент 2 — резервуар.

Значение полей кортежа fish:

name species tank
Sammy shark tank-a

В этой таблице понятно, что каждый из трех элементов кортежа имеет четкое значение.

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

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

from collections import namedtuple

Fish = namedtuple("Fish", ["name", "species", "tank"])

from collections import namedtuple дает вашей программе Python доступ к функции фабрики namedtuple. Вызов функции namedtuple() возвращает класс, привязанный к имени Fish. Функция namedtuple() имеет два аргумента: желаемое имя нового класса "Fish" и список именованных элементов ["name", "species", "tank"].

Мы можем использовать класс Fish для представления описанного выше кортежа fish:

sammy = Fish("Sammy", "shark", "tank-a")

print(sammy)

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

Output
Fish(name='Sammy', species='shark', tank='tank-a')

Создается экземпляр sammy с использованием класса Fish. sammy — это кортеж из трех элементов с понятными именами.

Доступ к полям sammy осуществляется по имени или через традиционный индекс кортежа:

print(sammy.species)
print(sammy[1])

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

Output
shark shark

При доступе к.species возвращается то же значение, что и при доступе ко второму элементу sammy с использованием [1].

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

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

Используйте ._asdict() для конвертации экземпляра в словарь:

print(sammy._asdict())

Если мы выполним команду print, результат будет выглядеть так:

Output
{'name': 'Sammy', 'species': 'shark', 'tank': 'tank-a'}

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

В версиях Python ниже 3.8 эта строка может выводиться немного по-другому. Например, вместо показанного здесь простого словаря вы можете увидеть OrderedDict.

Примечание. В Python методы, начинающиеся с символа подчеркивания, обычно считаются «частными». При этом дополнительные методы, предоставляемые namedtuple (_asdict(), ._make(), ._replace() и т. д.) являются публичными.

Сбор данных в словаре

Часто бывает полезно собирать данные в словарях Python. Словарь defaultdict из модуля collections может помочь быстро и кратко собрать информацию в словарях.

defaultdict никогда не генерирует ошибку KeyError. При отсутствии ключа defaultdict просто вставляет и возвращает подстановочное значение:

from collections import defaultdict

my_defaultdict = defaultdict(list)

print(my_defaultdict["missing"])

Если запустить этот код, результат будет выглядеть следующим образом:

Output
[]

defaultdict вставляет и возвращает подстановочное значение вместо генерирования ошибки KeyError. В этом случае мы задали подстановочное значение как список.

Обычные словари в подобных случаях выдают ошибку KeyError при отсутствии ключей:

my_regular_dict = {}

my_regular_dict["missing"]

Если запустить этот код, результат будет выглядеть следующим образом:

Output
Traceback (most recent call last): File "<stdin>", line 1, in <module> KeyError: 'missing'

Обычный словарь my_regular_dict генерирует ошибку KeyError при попытке доступа к отсутствующему ключу.

Поведение defaultdict отличается от поведения обычного словаря. Вместо генерирования ошибки KeyError при отсутствии ключа defaultdict вызывает подстановочное значение без аргументов для создания нового объекта. В данном случае это list() для создания пустого списка.

Продолжим рассмотрение нашего примера вымышленного аквариума. Допустим, у нас имеется список кортежей fish, представляющий содержимое аквариума:

fish_inventory = [
    ("Sammy", "shark", "tank-a"),
    ("Jamie", "cuttlefish", "tank-b"),
    ("Mary", "squid", "tank-a"),
]

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

Наша цель — организовать инвентарный список по резервуарам и получить список рыб, присутствующих в каждом резервуаре. Другими словами, нам нужен словарь, сопоставляющий "tank-a" с ["Jamie", "Mary"] и "tank-b" с ["Jamie"].

Мы можем использовать defaultdict для группировки рыб по резервуарам:

from collections import defaultdict

fish_inventory = [
    ("Sammy", "shark", "tank-a"),
    ("Jamie", "cuttlefish", "tank-b"),
    ("Mary", "squid", "tank-a"),
]
fish_names_by_tank = defaultdict(list)
for name, species, tank in fish_inventory:
    fish_names_by_tank[tank].append(name)

print(fish_names_by_tank)

Запустив этот код, мы получим следующий результат:

Output
defaultdict(<class 'list'>, {'tank-a': ['Sammy', 'Mary'], 'tank-b': ['Jamie']})

fish_names_by_tank декларируется как словарь defaultdict, который по умолчанию вставляет list() вместо вывода ошибки KeyError. Поскольку при этом каждый ключ в fish_names_by_tank будет гарантированно указывать на список, мы можем свободно вызывать .append() для добавления имен в списки каждого резервуара.

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

Без defaultdict цикл for выглядел бы примерно так:

More Verbose Example Without defaultdict
...

fish_names_by_tank = {}
for name, species, tank in fish_inventory:
    if tank not in fish_names_by_tank:
      fish_names_by_tank[tank] = []
    fish_names_by_tank[tank].append(name)

Если используется обычный словарь (вместо defaultdict), цикл for всегда должен проверять наличие определенного резервуара в fish_names_by_tank. Имя рыбки можно добавить только после подтверждения наличия резервуара в fish_names_by_tank или его инициализации с помощью [].

defaultdict помогает сократить объем кода при заполнении словарей, поскольку никогда не генерирует ошибки KeyError.

Использование дек для эффективного добавления элементов с любой стороны набора

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

С точки зрения нотации Big O добавление в список представляет собой операцию O(1) с постоянным временем. Вставка в начало списка выполняется медленнее, с производительностью O(n).

Примечание. Инженеры-разработчики часто измеряют производительность процедур с помощью нотации «Big O». Если размер ввода не влияет на время выполнения процедуры, считается, что она выполняется за постоянное время или O(1) (“Big O of 1”). Как уже говорилось выше, Python может добавлять элементы в списки за постоянное время или O(1).

Иногда размер ввода напрямую влияет на время выполнения процедуры. Например, вставка в начало списка Python выполняется тем медленнее, чем больше элементов содержится в списке. В нотации Big O для определения размера ввода используется буква n. Это означает, что для добавления элементов в начало списка Python требуется «линейное время» или O(n) (“Big O of n”).

В целом процедуры O(1) быстрее процедур O(n).

Мы можем выполнить вставку в начало списка Python:

favorite_fish_list = ["Sammy", "Jamie", "Mary"]

# O(n) performance
favorite_fish_list.insert(0, "Alice")

print(favorite_fish_list)

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

Output
['Alice', 'Sammy', 'Jamie', 'Mary']

Метод .insert(index, object) в списке позволяет нам вставить "Alice" в начало списка favorite_fish_list. Следует отметить, что вставка в начало списка осуществляется с производительностью O(n). По мере увеличения длины списка favorite_fish_list увеличивается и время вставки имени рыбки в начало списка.

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

Вставьте элемент в начало деки:

from collections import deque

favorite_fish_deque = deque(["Sammy", "Jamie", "Mary"])

# O(1) performance
favorite_fish_deque.appendleft("Alice")

print(favorite_fish_deque)

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

Output
deque(['Alice', 'Sammy', 'Jamie', 'Mary'])

Мы можем создать экземпляр деки, используя готовый набор элементов. В данном случае это список из трех имен любимых рыбок. Вызов метода favorite_fish_deque appendleft позволяет нам вставить элемент в начало набора с производительностью O(1). Производительность O(1) означает, что время добавления элемента в начало favorite_fish_deque не увеличивается, даже если favorite_fish_deque содержит тысячи или миллионы элементов.

Примечание. Хотя дека позволяет добавлять записи в начало последовательности более эффективно, чем список, дека не выполняет все операции более эффективно, чем список. Например, доступ к случайному элементу деки осуществляется с производительностью O(n), а доступ к случайному элементу списка — с производительностью O(1). Деки используются, когда нам важно быстро вставлять и удалять элементы с любой стороны набора. Полное сравнение производительности по времени можно найти в вики-справочнике по Python.

Заключение

Модуль collections — мощный элемент стандартной библиотеки Python, делающий работу с данными более краткой и эффективной. В этом обучающем модуле мы рассказали о трех классах модуля collections: namedtuple, defaultdict и deque.

Теперь вы можете использовать документацию по модулю collection, чтобы узнать больше о других доступных классах и утилитах. Чтобы узнать больше о Python в целом, пройдите нашу серию обучающих модулей «Программирование на Python 3».

0 Comments

Creative Commons License