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

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

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

Для примера мы будем работать с инвентарем fish, который мы будем изменять по мере добавления (и удаления) рыб в условный аквариум.

Требования

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

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

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

Вот персональный кортеж одной из рыб:

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

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

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

имя вид резервуар
Sammy shark tank-a

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

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

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

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

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

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

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

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

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

sammy создается с помощью класса Fish. sammy – это кортеж с тремя четко названными элементами.

Доступ к полям sammy можно получить по их именам или с помощью традиционного индекса:

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

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

shark
shark

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

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

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

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

print(sammy._asdict())

Если мы введем print, мы получим такой вывод:

{'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"])

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

[]

defaultdict вставляет и выдает заполнитель вместо KeyError. В этом случае мы указали в качестве заполнителя список.

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

my_regular_dict = {}
my_regular_dict["missing"]

Если мы запустим этот код, мы увидим:

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

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

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

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

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

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

Наша цель – организовать инвентарь по резервуарам, то есть нам нужно знать список рыб, присутствующих в каждом резервуаре. Другими словами, нам нужен словарь, который сопоставляет «tank-a» и [«Sammy», «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)

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

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

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

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

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

...
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 всегда должно проверять наличие данного резервуара (tank) в fish_names_by_tank. Только после того, как он убедится, что аквариум уже присутствует в fish_names_by_tank или был инициализирован с помощью [], он может добавить имя рыбы.

defaultdict помогает сократить традиционный код при заполнении словарей, потому что он никогда не вызывает KeyError.

Эффективное добавление элементов в коллекцию с помощью deque

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

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

Примечание: Инженеры-программисты часто измеряют производительность процедур, используя так называемую Big O нотацию. Когда размер ввода не влияет на время, необходимое для выполнения процедуры, говорят, что она выполняется за постоянное или константное время, или O(1). Как мы уже говорили, Python может добавлять данные в списки с постоянной производительностью, иначе известной как O(1).

Иногда размер ввода напрямую влияет на количество времени, необходимое для выполнения процедуры. Например, вставка в начало списка Python выполняется тем медленнее, чем больше элементов в списке. В Big O нотации n обозначает размер входных данных. Это означает, что добавление элементов в начало списка Python выполняется за линейное время, или за O(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)

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

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

Метод .insert(index, object) в списке позволяет нам вставить значение «Alice» в начало списка favorite_fish_list. Примечательно, что вставка в начало списка имеет производительность O(n). По мере увеличения длины списка favourite_fish_list продолжительность вставки данных в начало будет пропорционально увеличиваться и занимать все больше и больше времени.

deque (произносится как «deck») из модуля collections – это списко-подобный объект, который позволяет нам вставлять элементы в начало или в конец последовательности за постоянное время, O(1).

Вставьте элемент в начало deque:

from collections import deque
favorite_fish_deque = deque(["Sammy", "Jamie", "Mary"])
# O(1) performance
favorite_fish_deque.appendleft("Alice")
print(favorite_fish_deque)

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

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

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

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

Заключение

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

Чтобы узнать больше о других доступных классах и утилитах модуля collections, обратитесь к его документации. Чтобы узнать больше о Python в целом, обратитесь к нашему Информаторию.

Tags: ,

Добавить комментарий