# collections

В стандартную библиотеку Python входит модуль `collections` содержащий дополнительные структуры данных. Мы поговорим о некоторых и обсудим их пользу.

А конкретно:

* `defaultdict`
* `OrderedDict`
* `counter`
* `deque`
* `namedtuple`
* `enum.Enum` (вне модуля; Python 3.4+)

## `defaultdict`

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

```python
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)

# Вывод:
# defaultdict(<type 'list'>,
#    {'Arham': ['Green'],
#     'Yasoob': ['Yellow', 'Red'],
#     'Ahmed': ['Silver'],
#     'Ali': ['Blue', 'Black']
# })
```

Другим популярным случаем использования `defaultdict` является добавление элементов в список внутри словаря. Если ключ не существует в словаре, то вы упрётесь в `KeyError`. `defaultdict` позволяет обойти эту проблему аккуратным образом. Для начала, позвольте привести пример использования `dict` с исключением `KeyError`, а затем мы посмотрим на пример с `defaultdict`.

**Проблема:**

```python
some_dict = {}
some_dict['colours']['favourite'] = "yellow"
# Вызывает KeyError: 'colours'
```

**Решение:**

```python
import collections
tree = lambda: collections.defaultdict(tree)
some_dict = tree()
some_dict['colours']['favourite'] = "yellow"
# Работает без ошибок
```

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

```python
import json
print(json.dumps(some_dict))
# Вывод: {"colours": {"favourite": "yellow"}}
```

## `OrderedDict`

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

**Проблема:**

```python
colours = {"Red": 198, "Green": 170, "Blue": 160}
for key, value in colours.items():
    print(key, value)
# Вывод:
#   Red 198
#   Blue 160
#   Green 170
#
# Элементы выводятся в произвольном порядке
```

**Решение:**

```python
from collections import OrderedDict

colours = OrderedDict([("Red", 198), ("Green", 170), ("Blue", 160)])
for key, value in colours.items():
    print(key, value)
# Вывод:
#   Red 198
#   Green 170
#   Blue 160
#
# Порядок элементов сохранен
```

## `counter`

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

```python
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)
# Вывод: Counter({
#    'Yasoob': 2,
#    'Ali': 2,
#    'Arham': 1,
#    'Ahmed': 1
# })
```

Мы также можем посчитать частоту строк в файле. Пример:

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

## `deque`

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

```python
from collections import deque
```

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

```python
d = deque()
```

Очередь работает подобно списку в Python и имеет схожие методы. Например, вы можете:

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

print(len(d))
# Вывод: 3

print(d[0])
# Вывод: '1'

print(d[-1])
# Вывод: '3'
```

Мы можем отрезать элементы с обеих сторон очереди:

```python
d = deque(range(5))
print(len(d))
# Вывод: 5

d.popleft()
# Вывод: 0

d.pop()
# Вывод: 4

print(d)
# Вывод: deque([1, 2, 3])
```

Мы также можем ограничить число элементов, которые может хранить очередь. Таким образом при достижении максимального числа элементов очередь начнет отрезать элементы с другого конца. Это проще объяснить на примере:

```python
d = deque(maxlen=30)
```

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

```python
d = deque([1,2,3,4,5])
d.extendleft([0])
d.extend([6,7,8])
print(d)
# Вывод: deque([0, 1, 2, 3, 4, 5, 6, 7, 8])
```

## `namedtuple`

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

```python
man = ('Ali', 30)
print(man[0])
# Вывод: Ali
```

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

```python
from collections import namedtuple

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

print(perry)
# Вывод: Animal(name='perry', age=31, type='cat')

print(perry.name)
# Вывод: 'perry'
```

Теперь вы можете видеть, что мы можем обращаться к элементам именованного кортежа при помощи их имени и `.` (точки). Давайте чуть подробнее на этом остановимся. Именованный кортеж имеет два обязательных аргумента. Это имя самого кортежа и имена полей кортежа. В примере выше имя нашего кортежа `Animal`, имена полей соответственно: `name`, `age` и `type`. Именованный кортеж позволяет создавать **само-документированные** кортежи. Вы сможете легко понять код при первом же взгляде на него. И, поскольку вы не привязаны к индексам, у вас открывается больше возможностей по поддержке своего кода. Помимо этого, **именованные кортежи не создают словари для каждого экземпляра**, они легковесны и не требуют больше памяти чем обычные кортежи. Это делает их быстрее словарей. Тем не менее, помните, что как и в случае с обычными кортежами, **именованный кортеж неизменяем**. Это означает, что такой код работать не будет:

```python
from collections import namedtuple

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

# Вывод: Traceback (most recent call last):
#           File "", line 1, in
#        AttributeError: can't set attribute
```

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

```python
from collections import namedtuple

Animal = namedtuple('Animal', 'name age type')
perry = Animal(name="perry", age=31, type="cat")
print(perry[0])
# Вывод: perry
```

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

```python
from collections import namedtuple

Animal = namedtuple('Animal', 'name age type')
perry = Animal(name="Perry", age=31, type="cat")
print(perry._asdict())
# Вывод: OrderedDict([('name', 'Perry'), ('age', 31), ...
```

## `enum.Enum` (Python 3.4+)

Другой полезной структурой данных является `enum`. Он доступен в модуле `enum`, начиная с Python 3.4 (также в PyPI как бекпорт под именем `enum34`). Enums ([перечисляемый тип](https://en.wikipedia.org/wiki/Enumerated_type)) это простой способ организации разных вещей.

Давайте рассмотрим именованный кортеж `Animal` из прошлого примера. У него есть поле `type`. Проблема в том, что его тип - строка. Это создаёт нам несколько проблем. Что если пользователь ввёл `Cat`, поскольку нажал Shift? Или `CAT`? Или `kitten`?

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

```python
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
    # Список продолжается...

    # Нам безразличен возраст животного, так что мы используем синонимы
    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.cat)

# А теперь несколько тестов
>>> charlie.type == tom.type
True
>>> charlie.type
<Species.cat: 1>
```

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

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

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

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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://pavel-karateev.gitbook.io/intermediate-python/tipy-dannykh/collections.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
