• en
  • Language: ru
  • Documentation version: latest

12. Коллекции

Python поставляется с модулем, который содержит ряд контейнерных типов данных, называемых Collections. Мы поговорим о некоторых из них и обсудим их полезность.

О них мы и поговорим:

  • defaultdict

  • OrderedDict

  • Counter

  • deque

  • namedtuple

  • enum.Enum (вне модуля; Python 3.4+)

12.1. defaultdict

Я лично часто использую defaultdict. В отличие от dict, в defaultdict вам не нужно проверять, присутствует ли ключ или нет. Поэтому мы можем сделать следующее:

from collections import defaultdict

colours = (
    ('Yasoob', 'Yellow'),
    ('Ali', 'Blue'),
    ('Arham', 'Green'),
    ('Ali', 'Black'),
    ('Yasoob', 'Red'),
    ('Ahmed', 'Silver'),
)

favourite_colours = defaultdict(list)

for name, colour in colours:
    favourite_colours[name].append(colour)

print(favourite_colours)

# output
# defaultdict(<type 'list'>,
#    {'Arham': ['Green'],
#     'Yasoob': ['Yellow', 'Red'],
#     'Ahmed': ['Silver'],
#     'Ali': ['Blue', 'Black']
# })

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

Проблема:

some_dict = {}
some_dict['colours']['favourite'] = "yellow"
# Raises KeyError: 'colours'

Решение:

from collections import defaultdict
tree = lambda: defaultdict(tree)
some_dict = tree()
some_dict['colours']['favourite'] = "yellow"
# Works fine

Вы можете вывести some_dict, используя json.dumps. Вот пример кода:

import json
print(json.dumps(some_dict))
# Output: {"colours": {"favourite": "yellow"}}

12.2. OrderedDict

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

Проблема:

colours =  {"Red" : 198, "Green" : 170, "Blue" : 160}
for key, value in colours.items():
    print(key, value)
# Output:
#   Green 170
#   Blue 160
#   Red 198
# Entries are retrieved in an unpredictable order

Решение:

from collections import OrderedDict

colours = OrderedDict([("Red", 198), ("Green", 170), ("Blue", 160)])
for key, value in colours.items():
    print(key, value)
# Output:
#   Red 198
#   Green 170
#   Blue 160
# Insertion order is preserved

12.3. Counter

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

from collections import Counter

colours = (
    ('Yasoob', 'Yellow'),
    ('Ali', 'Blue'),
    ('Arham', 'Green'),
    ('Ali', 'Black'),
    ('Yasoob', 'Red'),
    ('Ahmed', 'Silver'),
)

favs = Counter(name for name, colour in colours)
print(favs)
# Output: Counter({
#    'Yasoob': 2,
#    'Ali': 2,
#    'Arham': 1,
#    'Ahmed': 1
# })

С его помощью можно также подсчитать наиболее часто встречающиеся строки в файле. Например:

with open('filename', 'rb') as f:
    line_count = Counter(f)
print(line_count)

12.4. deque

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

from collections import deque

Теперь мы можем создать объект deque.

d = deque()

Он работает как списки python и предоставляет вам несколько похожих методов. Например, вы можете сделать следующее:

d = deque()
d.append('1')
d.append('2')
d.append('3')

print(len(d))
# Output: 3

print(d[0])
# Output: '1'

print(d[-1])
# Output: '3'

Вы можете извлекать значения с обеих сторон deque:

d = deque(range(5))
print(len(d))
# Output: 5

d.popleft()
# Output: 0

d.pop()
# Output: 4

print(d)
# Output: deque([1, 2, 3])

Мы также можем ограничить количество элементов, которые может хранить deque. Для этого, когда мы достигнем максимального предела нашего deque, он будет просто вытаскивать элементы с противоположного конца. Лучше объяснить это на примере, так что вот, пожалуйста:

d = deque([0, 1, 2, 3, 5], maxlen=5)
print(d)
# Output: deque([0, 1, 2, 3, 5], maxlen=5)

d.extend([6])
print(d)
#Output: deque([1, 2, 3, 5, 6], maxlen=5)

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

d = deque([1,2,3,4,5])
d.extendleft([0])
d.extend([6,7,8])
print(d)
# Output: deque([0, 1, 2, 3, 4, 5, 6, 7, 8])

12.5. namedtuple

Возможно, вы уже знакомы с кортежами. Кортеж - это неизменяемый список, который позволяет хранить последовательность значений, разделенных запятыми. Они похожи на списки, но имеют несколько ключевых отличий. Главное из них заключается в том, что в отличие от списков, вы не можете переназначить элемент в кортеже. Для доступа к значению в кортеже используются целочисленные индексы, такие как:

man = ('Ali', 30)
print(man[0])
# Output: Ali

Итак, что же такое namedtuples? Они превращают кортежи в удобные контейнеры для простых задач. С именованными кортежами вам не нужно использовать целочисленные индексы для доступа к членам кортежа. Именованные кортежи можно рассматривать как словари, но в отличие от словарей они неизменяемы.

from collections import namedtuple

Animal = namedtuple('Animal', 'name age type')
perry = Animal(name="perry", age=31, type="cat")

print(perry)
# Output: Animal(name='perry', age=31, type='cat')

print(perry.name)
# Output: 'perry'

Теперь вы видите, что мы можем получить доступ к членам кортежа просто по их имени с помощью .. Давайте разберем это немного подробнее. Именованный кортеж имеет два обязательных аргумента. Это имя кортежа и поле_имени кортежа. В приведенном выше примере имя нашего кортежа было „Animal“, а поле_имени кортежа было „name“, „age“ и „type“. Namedtuple делает ваши кортежи самодокументирующимися. Вы можете легко понять, что происходит, бросив беглый взгляд на свой код. А поскольку вы не обязаны использовать целочисленные индексы для доступа к членам кортежа, это упрощает сопровождение вашего кода. Более того, поскольку экземпляры `namedtuple` не имеют словарей для каждого экземпляра, они легковесны и требуют не больше памяти, чем обычные кортежи. Это делает их быстрее, чем словари. Однако помните, что, как и в случае с кортежами, атрибуты в namedtuples неизменяемы. Это означает, что данный метод не будет работать:

from collections import namedtuple

Animal = namedtuple('Animal', 'name age type')
perry = Animal(name="perry", age=31, type="cat")
perry.age = 42

# Output: Traceback (most recent call last):
#            File "", line 1, in
#         AttributeError: can't set attribute

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

from collections import namedtuple

Animal = namedtuple('Animal', 'name age type')
perry = Animal(name="perry", age=31, type="cat")
print(perry[0])
# Output: perry

И последнее, но не менее важное: вы можете преобразовать именованный кортеж в словарь. Например:

from collections import namedtuple

Animal = namedtuple('Animal', 'name age type')
perry = Animal(name="Perry", age=31, type="cat")
print(perry._asdict())
# Output: OrderedDict([('name', 'Perry'), ('age', 31), ...

12.6. enum.Enum (Python 3.4+)

Другой полезной коллекцией является объект enum. Он доступен в модуле enum, в Python 3.4 и выше (также доступен как backport в PyPI под названием enum34.) Перечисления (enumerated type) - это способ организации различных вещей.

Рассмотрим кортеж Animal namedtuple из предыдущего примера. В нем было поле type. Проблема в том, что его тип был строкой. Это создает для нас некоторые проблемы. Что если пользователь введет Cat, потому что удерживал клавишу Shift? Или CAT? Или kitten?

Перечисления могут помочь нам избежать этой проблемы, не используя строки. Рассмотрим этот пример:

from collections import namedtuple
from enum import Enum

class Species(Enum):
    cat = 1
    dog = 2
    horse = 3
    aardvark = 4
    butterfly = 5
    owl = 6
    platypus = 7
    dragon = 8
    unicorn = 9
    # The list goes on and on...

    # But we don't really care about age, so we can use an alias.
    kitten = 1
    puppy = 2

Animal = namedtuple('Animal', 'name age type')
perry = Animal(name="Perry", age=31, type=Species.cat)
drogon = Animal(name="Drogon", age=4, type=Species.dragon)
tom = Animal(name="Tom", age=75, type=Species.cat)
charlie = Animal(name="Charlie", age=2, type=Species.kitten)

# And now, some tests.
>>> charlie.type == tom.type
True
>>> charlie.type
<Species.cat: 1>

Это гораздо менее подвержено ошибкам. Мы должны быть конкретными и использовать только перечисление для наименования типов.

Существует три способа доступа к членам перечисления. Например, все три метода дадут вам значение для cat:

Species(1)
Species['cat']
Species.cat

Это был просто быстрый просмотр модуля collections. Убедитесь, что вы прочитали официальную документацию после прочтения этой статьи.