<jupyter_start><jupyter_text>Модуль ```collections``` Именованные кортежи (```namedtuple```)У классических кортежей есть один недостаток. Для обращения к значению, находящемуся внутри него, нужно использовать индекс. Это не всегда удобно. Например, когда в кортеже хранится дата в формате ```date = (year, month, day)```, для получения значения года нужно обратиться по индексу ```date[0]```. Читаемость этого выражения не слишком высокая. Для его понимания нужно держать в голове порядок элементов кортежа и помнить, что по нулевому индексу находится год. Как развитие обычных кортежей были предложены именованные кортежи (см. [документацию](https://docs.python.org/3/library/collections.htmlcollections.namedtuple)) или тип ```namedtuple```. Они находятся в модуле ```collections``` стандартной библиотеки. Для начала их использования необходимо их импортировать. ```pythonform collections import namedtuple``` Подробнее операция импорта будет рассмотрена в следующих разделах. Функция ```namedtuple``` принимает ряд аргументов. Ее сигнатура выглядит как ```namedtuple(typename, field_names, *, rename=False, defaults=None, module=None)```. Параметр ```typename``` должен быть строкой и определяет название типа создаваемого именованного кортежа. Набор полей задается с помощью параметра ```field_names```. Имена полей могут передаваться в качестве последовательности сток, например, ```['a', 'b']``` или ```('a', 'b')```, а также в одной строке, разделенные пробелом или запятой, например, ```'a b'``` или ```'a, b'```. ```defaults``` задает значение по умолчанию для всех полей кортежа.<jupyter_code>from collections import namedtuple

# создание типа именованного кортежа. Желательно, 
# чтобы имя переменной и имя в строке совпадали.
Point_2d = namedtuple('Point_2d', ('x', 'y'), defaults=(0, 0))

# объект со значениями по умолчанию
point_0 = Point_2d()
# значения можно передавать явно указывая именя
point_1 = Point_2d(x=1, y=2)
# можно передавать как позиционные, тогда первое 
# значение попадет в первый параметр и т.д.
point_2 = Point_2d(1, 2)

print(f'Точка 0: {point_0 = }')
print(f'Точка 1: {point_1 = }')
print(f'Точка 2: {point_2 = }')
# тип объектов namedtuple будет совпадать со значением 
# параметра typename, в данном случае Point_2d
print(f'Тип объекта 1: {type(point_1) = }')
print(f'Тип объекта 2: {type(point_2) = }')
# доуступ к атрибутам namedtuple можно осуществлять через точку и имя атрибута
print(f'{point_1.x = }, {point_1.y = }')
# или через индекс
print(f'{point_1[0] = }, {point_1[1] = }')

x, y = point_1
print(f'{x = }, {y = }')
# namedtuple это кортеж, а значит неизменяемая коллекция
point_1.x = 0
# для изменения одного или нескольких атрибутов у namedtuple есть 
# метод _replace. На самом деле он не изменяет значение атрибута, 
# а создает новый объект на основе текущего
point_3 = point_1._replace(y=0)
print(f'{point_1 = }, {point_3 = }')
# создавать именованные кортежи можно с помощью метода _make. 
# Он принимает коллекцию. 
# Эквивалентно Point_2d(1, 2)
p_1 = Point_2d._make([1, 2])
# Эквивалентно Point_2d(x=1, y=2)
p_2 = Point_2d._make({'x': 1, 'y': 2})

print(f'{p_1 = }, {p_2 = }')
# Именованные кортежи легко можно преобразовать в словарь с 
# помощью метода _asdict
print(f'Преобразование namedtyple в словарь: {point_1._asdict() = }')
print(f'Преобразование namedtyple в кортеж: {tuple(point_1) = }')
# namedtuple это коллекция, поэтому справедливы все операции, 
# характерные для коллекций.
print(f'{len(point_1) = }')
for value in point_1:
    print(f'{value = }')<jupyter_output>len(point_1) = 2
value = 1
value = 2<jupyter_text>Бывают ситуации когда необходимо создать новый тип именованного кортежа на основе уже существующего. Например, логично создать точку в трехмерном пространстве ```Point_3d``` на основе точки на двумерной плоскости ```Point_2d```. У этих двух типов есть одинаковый атрибуты ```x``` и ```y```. В этом случае писать два раза почти одну и ту же коллекцию атрибутов не хочется. Используя специальный атрибут ```_fields``` можно получить коллекцию атрибутов именованного кортежа. ```_fields``` рекомендуется использовать для создания нового типа именованного кортежа на основе существующего, если у них есть одинаковые атрибуты. С помощью этого атрибута можно создавать именованные кортежи на основе сразу нескольких других кортежей.<jupyter_code>print(f'{point_1._fields = }')
Point_3d = namedtuple('Point_3d', Point_2d._fields + ('z', ), defaults=(0, 0, 0))

p_1 = Point_3d(1, 2, 3)

print(f'{Point_3d = }')
print(f'{Point_3d._fields = }')
print(f'{p_1 = }')
print(f'{type(p_1) = }')<jupyter_output>Point_3d = <class '__main__.Point_3d'>
Point_3d._fields = ('x', 'y', 'z')
p_1 = Point_3d(x=1, y=2, z=3)
type(p_1) = <class '__main__.Point_3d'><jupyter_text>```Counter```Для подсчета любых повторяющихся элементов в какой-либо коллекции существует класс ```Couter``` из модуля ```collections``` (см. [документацию](https://docs.python.org/3/library/collections.htmlcollections.Counter)). Итоговая коллекция очень похожа на словарь и доступ к элементам осуществляется по ключу. Ключом являются сами элементы коллекции, а значениями количество элементов в коллекции. Приятным бонусом его использования будет то, что итоговая коллекция будет отсортирована в порядке убывания их количества в строке. Ниже приведены далеко не все возможности ```Couter```, подробнее см. в [документации](https://docs.python.org/3/library/collections.htmlcollections.Counter).<jupyter_code>from collections import Counter

s = 'Never Gonna Give You Up'

count = Counter(s)
print(f'{count = }')

print(f'{count["e"] = }')
print(f'{count[" "] = }')
print(f'{count["p"] = }')

print(f'{count.most_common(4) = }')
print(f'{list(count.elements()) = }')<jupyter_output>count = Counter({' ': 4, 'e': 3, 'v': 2, 'G': 2, 'o': 2, 'n': 2, 'N': 1, 'r': 1, 'a': 1, 'i': 1, 'Y': 1, 'u': 1, 'U': 1, 'p': 1})
count["e"] = 3
count[" "] = 4
count["p"] = 1
count.most_common(4) = [(' ', 4), ('e', 3), ('v', 2), ('G', 2)]
list(count.elements()) = ['N', 'e', 'e', 'e', 'v', 'v', 'r', ' ', ' ', ' ', ' ', 'G', 'G', 'o', 'o', 'n', 'n', 'a', 'i', 'Y', 'u', 'U', 'p']<jupyter_text>```deque``````deque``` это реализация такой структуры данных как [двусторонняя очередь](https://en.wikipedia.org/wiki/Queue_(abstract_data_type)). Функция ```deque``` принимает коллекцию в качестве аргумента и один необязательный параметр ```maxlen```, который ограничивает длину очереди.Она позволяет реализовать:- ограниченную очередь (за счет дополнительного аргумента ```maxlen```);- очередь по принципу "первый пришёл — первый вышел" или FIFO (за счет методов ```popleft``` для удаления элемента с левой части очереди и ```append``` для добавления элемента в правую часть);- очередь по принципу "последний пришёл — первый вышел" или LIFO (за счет методов ```pop``` для удаления элемента с правой части очереди и ```append```).Реализация очереди с помощью ```deque``` гораздо эффективнее с точки зрения производительности нежели реализация очереди на основе списков. Поэтому если существует необходимость в этой структуре данных нужно использовать ```deque```, а не списки.Подробнее о доступных методах и подробных примерах см. в [документации](https://docs.python.org/3/library/collections.htmlcollections.deque).<jupyter_code>from collections import deque

d = deque([1, 2, 3, 4])
print(f'{d = }')
print(f'{type(d) = }')

# Операции pop и popleft удаляют элемент из очереди и возвращают, 
# это позволяет работать с ним после удаления
left_item = d.popleft()
right_item = d.pop()
print(f'Элемент с левого конца: {left_item = }')
print(f'Элемент с правого конца: {right_item = }')

# Можно добавлять элементы в любой конец
d.append(42)
d.appendleft(196)
print(f'Очередь после добавления элементов: {d = }')<jupyter_output>d = deque([1, 2, 3, 4])
type(d) = <class 'collections.deque'>
Элемент с левого конца: left_item = 1
Элемент с правого конца: right_item = 4
Очередь после добавления элементов: d = deque([196, 2, 3, 42])<jupyter_text>```ChainMap```Очень часто встречаются ситуации, когда нужно объединить несколько словарей. Это можно сделать несколькими разными способами:- для Python < 3.9 ```{**a, **b}```;- для Python >= 3.9 ```a | b```;- использовать ```ChainMap```.Первые два способа создадут новый словарь в памяти на основе двух других. Для больших исходных словарей накладно создавать еще больший словарь с аналогичным содержимым. Для решения этой проблемы используется ```ChainMap```. Он позволяет объединить несколько словарей, не создавая новый. По факту он хранит только ссылки на исходные словари. Однако, его повеление полностью аналогично тому, как если бы мы произвели объединение словарей.Подробнее о возможностях ```ChainMap``` см. в [документации](https://docs.python.org/3/library/collections.htmlcollections.ChainMap).<jupyter_code>from collections import ChainMap

# словари, которые будем объединять
# в некоторых из них есть одинаковые ключи
d_1 = {
    'a': [1, 2, 3],
    'b': [4, 5, 6],
}
d_2 = {
    'b': 42,
    'c': 196,
}
d_3 = {
    'd': ['dot', 'dump'],
    'f': ['foo', 'fail'],
}

# Два разных ChainMap
# Создавая ChainMap важен порядок словарей, как при их 
# объединении. Это повлияет на обновление повторяющихся ключей.
cm_1 = ChainMap(d_1, d_2, d_2)
cm_2 = ChainMap(d_3, d_2, d_1)
print(f'{cm_1 = }')
print(f'{type(cm_1) = }')

print('ChainMap(d_1, d_2, d_3):')
print(f'{cm_1["a"] = }')
print(f'{cm_1["b"] = }')
print(f'{cm_1["c"] = }')
print('-' * 25)
print('ChainMap(d_3, d_2, d_1):')
print(f'{cm_2["a"] = }')
print(f'{cm_2["b"] = }')
print(f'{cm_2["c"] = }')<jupyter_output>cm_1 = ChainMap({'a': [1, 2, 3], 'b': [4, 5, 6]}, {'b': 42, 'c': 196}, {'b': 42, 'c': 196})
type(cm_1) = <class 'collections.ChainMap'>
ChainMap(d_1, d_2, d_3):
cm_1["a"] = [1, 2, 3]
cm_1["b"] = [4, 5, 6]
cm_1["c"] = 196
-------------------------
ChainMap(d_3, d_2, d_1):
cm_2["a"] = [1, 2, 3]
cm_2["b"] = 42
cm_2["c"] = 196<jupyter_text>Изменение исходных списков отражается на ```ChainMap``` и наоборот.<jupyter_code>d_1['a'].append(4)

print(f'{cm_1 = }')
print(f'{cm_1["a"] = }')

# добавление нового ключа и значения в ChainMap 
# привело к добавлению этого ключа в первый словарь
cm_1['g'] = 0
print(f'{cm_1 = }')
print(f'{d_1 = }')
print(f'Объединение всех списков: {(d_1 | d_2 | d_2).__sizeof__() = }')
print(f'ChainMap: {cm_1.__sizeof__() = }')<jupyter_output>Объединение всех списков: (d_1 | d_2 | d_2).__sizeof__() = 216
ChainMap: cm_1.__sizeof__() = 32<jupyter_text>```defaultdict```Стандартные словари ```dict``` при обращении к несуществующему ключу возвращают исключение ```KeyError```. Это приводит к тому, что нужно постоянно проверять его наличие. В ситуациях, когда предполагается добавление отсутствующему ключу добавлять некоторое значение по умолчанию постоянное выполнение проверок не слишком продуктивно. ```python проверка наличия ключаif key in d:    value = d[key]else:     добавление ключа со значением по умолчанию    d[key] = 0```Для таких ситуаций предусмотрена коллекция ```defaultdict```. Это словарь, который умеет автоматически задавать значение по умолчанию для несуществующих ключей. Функция ```defaultdict()``` принимает один необязательный аргумент - функцию, которая возвращает значение по умолчанию. Если вызывать ```defaultdict``` без аргмуентов, то его поведение будет аналогичным обычному ```dict```.Подробнее о возможностях ```defaultdict``` см. в [документации](https://docs.python.org/3/library/collections.htmlcollections.defaultdict).<jupyter_code>from collections import defaultdict

d = defaultdict()
print(f'{d = }')
print(f'{type(d) = }')

d['a'] = 5
print(f'{d["a"] = }')
print(f'{d["b"] = }')  # KeyError
# вызов функции int без аргументов возвращает ноль
print(f'{int() = }')

# словарь со значением ноль по умолчанию
d = defaultdict(int)
print(f'{d = }')  # изначально словарь пуст
print(f'{type(d) = }')

d['a'] = 5
print(f'{d["a"] = }')
print(f'{d["b"] = }')  # 0
print(f'{d["с"] = }')  # 0
# каждое новое обращение к несуществующему ключу добавляет его в словарь
print(f'{d = }')

# проверка на наличие ключа работает как обычно
print(f'{"g" in d = }')
print(f'{"c" in d = }')

# можно сразу выполнять in-place операции даже с несуществующими ключами
d['e'] += 42
print(f'{d["e"] = }')<jupyter_output>int() = 0
d = defaultdict(<class 'int'>, {})
type(d) = <class 'collections.defaultdict'>
d["a"] = 5
d["b"] = 0
d["с"] = 0
d = defaultdict(<class 'int'>, {'a': 5, 'b': 0, 'с': 0})
"g" in d = False
"c" in d = False
d["e"] = 42<jupyter_text>```defaultdict``` можно использовать для разных задач. Например, в ситуации, когда нужно создать дерево на основе словарей. В этом случае значением по умолчанию будет тот же ```defaultdict```.<jupyter_code># фуекция для создания значения по умолчанию
def tree():
    # defaultdict с этой же функцией
    return defaultdict(tree)

# создание корневого узла
df = defaultdict(tree)
print(df[1])
print(df[1][2][3])
print(df[1][4])

# в качестве листов дерева будут пустые словари
print(f'{df = }')<jupyter_output>defaultdict(<function tree at 0x0000021A9127DAF0>, {})
defaultdict(<function tree at 0x0000021A9127DAF0>, {})
defaultdict(<function tree at 0x0000021A9127DAF0>, {})
df = defaultdict(<function tree at 0x0000021A9127DAF0>, {1: defaultdict(<function tree at 0x0000021A9127DAF0>, {2: defaultdict(<function tree at 0x0000021A9127DAF0>, {3: defaultdict(<function tree at 0x0000021A9127DAF0>, {})}), 4: defaultdict(<function tree at 0x0000021A9127DAF0>, {})})})
{'fraction_non_alphanumeric': 0.15470684768014323, 'fraction_numerical': 0.02856929732955393, 'mean_word_length': 5.72704465629704, 'pattern_counts': {'":': 0, '<': 41, '<?xml version=': 0, '>': 41, 'https://': 7, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '8', 'dir': 'jupyter-structured-clean-dedup', 'id': '654939', 'license': 'MIT', 'n_tokens_mistral': 5795, 'n_tokens_neox': 6379, 'n_words': 1436, 'path': 'python_pd/03_collections/03_collections.ipynb', 'repo_name': 'Alexey-smurf/python-bp', 'size': '13406'}