Конспект посвящён модулю collections и его составляющим, а именно именованному кортежу, специфическим типам словарей и т.п. Вторая часть конспектов про типы данных в Python
Модуль collections
Python содержит встроенный модуль collections, который содержит специализированные типы коллекций, альтернативных традиционным list, tuple, dict:
namedtuple
defaultdict
OrderedDict
Counter
ChainMap
и прочие
Начнём с разбора именованных кортежей.
Namedtuple
Для использования: from collections import namedtuple
Именованные кортежи (тип namedtuple) — это подтип обычных кортежей в Python. У них те же функции, что и у обычных, но их значения можно получать как с помощью индекса (например, [0]), так и с помощью имени через точку (например, .name). Их основным назначением является улучшение читаемости кода.
Опишем точку на плоскости, имеющую две координаты x и y с помощью именованного кортежа:
1
2
3
4
5
6
7
8
9
10
fromcollectionsimportnamedtuplePoint=namedtuple('Point',['x','y'])# создаём подкласс кортежа Point (именованный кортеж)point=Point(3,7)# создаём экземпляр Pointprint(point)print(point.x,point.y)print(point[0],point[1])print(type(point))
Параметр typename отвечает за имя создаваемого класса namedtuple (который и возвращает функция namedtuple()), а параметр fieldnames за название полей, которые мы будем использовать, чтобы получить доступ к значениям определённого экземпляра именованного кортежа. В качестве параметра field_names можно использовать:
Множество. Можно создать именованный кортеж с помощью множества, но делать это не рекомендуется, так как множество — неупорядоченный набор данных, поэтому поля могут перемешаться.
1
2
3
4
5
fromcollectionsimportnamedtuplePoint=namedtuple('Point',{'x','y'})# в качестве второго параметра передаем множествоpoint=Point(2,4)print(point)# выводит Point(x=2, y=4) или Point(y=2, x=4)
Совет
В качестве параметра field_names можно передавать любой итерируемый объект, например, результат вызова функций map() и filter()
В качестве названия полей для именованных кортежей мы можем использовать любое корректное название имени переменной, за исключением:
имён, начинающихся с символа _;
ключевых слов языка Python (if, with, else, class, …).
rename
При rename=True названия полей, переданных в field_names, которые содержат ключевые слова Python, переименовываются в соответствии с их порядковыми номерами (начиная с нуля), перед которыми ставится символ _. Посмотрим на пример:
Параметр defaults (работает в Python 3.7+) используется для того, чтобы установить значения по умолчанию для полей именованного кортежа. Можно указать значение по умолчанию только для некоторых полей, при этом defaults присваивает значения по умолчанию с хвоста.
1
2
3
4
5
6
7
8
fromcollectionsimportnamedtuplePoint=namedtuple('Point',['x','y'],defaults=(0,0))point=Point()# используем значения по умолчаниюprint(point)# Вывод: Point(x=0, y=0)
Если мы укажем допустимое имя модуля для этого аргумента, тогда атрибуту .__ module__ результирующего именованного кортежа будет присвоено это значение:
Параметр module был добавлен в Python 3.6 для того, чтобы появилась возможность сериализовать/десериализовать именованные кортежи с помощью модуля pickle в разных реализациях Python (IronPython, Jython и т.д.)
Распаковка именованного кортежа
Именованный кортеж распаковывается также, как и обычный:
1
2
3
4
5
6
7
8
9
fromcollectionsimportnamedtuplePerson=namedtuple('Person',['name','age','height'])ivan=Person('Иван',19,179)print(*ivan)# Вывод: Иван 19 179
Атрибуты _fields и _field_defaults
Именованные кортежи имеют два дополнительных атрибута: _fields и _field_defaults. Первый содержит кортеж строк, в котором перечислены имена полей. Второй атрибут содержит словарь, который сопоставляет имена полей с соответствующими значениями по умолчанию, если таковые имеются.
Как видно из примера выше, можно обращаться к атрибуту _fields как через переменную (ivan), так и через сам тип именованного кортежа (Person).
С помощью атрибута _fields можно создавать новые именованные кортежи на основании уже существующих. В следующем примере создаётся новый именованный кортеж с именем ExtendedPerson, который расширяет старый Person новым полем weight:
1
2
3
4
5
6
7
8
9
10
fromcollectionsimportnamedtuplePerson=namedtuple('Person',['name','age','height'])ExtendedPerson=namedtuple('ExtendedPerson',[*Person._fields,'weight'])# распаковка полей старого кортежаivan=ExtendedPerson('Иван',19,179,63)print(ivan)print(ExtendedPerson._fields)
_asdict() преобразует именованные кортежи в словари, в которых имена полей используются в качестве ключей. Ключи результирующего словаря находятся в том же порядке, что и поля в исходном именованном кортеже.
_replace() создаёт новый именованный кортеж на основе уже существующего с заменой некоторых значений. Потребность в данном методе вызвана тем, что именованные кортежи являются неизменяемыми.
Функционал именованных кортежей можно полностью заменить функционалом словарей, тогда зачем вообще нужен этот namedtuple? Если коротко, то он более быстрый и занимает меньше места в памяти. Сравним эти показатели с помощью нехитрых программ.
Для использования: from collections import defaultdict
Основная проблема при работе с обычными словарями — попытка получить доступ к несуществующему ключу, которая вызывает ошибку KeyError. С этим можно справиться через setdefault(), get() или проверкой наличия ключа в словаря (try/except или оператор in), а можно воспользоваться типом данных defaultdict.
Тип defaultdict ведет себя почти так же, как обычный словарь dict, но если мы попытаемся получить доступ (или изменить значение) по несуществующему ключу, то defaultdict автоматически создаст ключ и сгенерирует для него значение по умолчанию. Такое поведение делает этот тип данных удобным вариантом обработки недостающих ключей в словарях.
defaultdict является является наследником класса dict, а значит обладает всеми его методами.
Функция defaultdict() принимает в качестве аргумента тип элемента по умолчанию. Таким образом, для ключей, к которым происходит обращение, словарь defaultdict поставит в соответствие дефолтный элемент данного типа:
int – число 0;
float – число 0.0;
bool – значение False;
str – пустая строка '';
list – пустой список [];
tuple – пустой кортеж ();
set – пустое множество set();
dict – пустой словарь {}.
1
2
3
4
5
6
7
8
9
10
fromcollectionsimportdefaultdictinfo=defaultdict(int)# создаем словарь со значением по умолчанию 0info['name']='Ivan'info['age']=19info['job']='Darkstore'print(info['salary'])print(info)
Также допустимы все способы, которые мы используем при создании обычных словарей, а именно передача именованных аргументов или итерируемого объекта, содержащего пары ключ-значение (например, список кортежей). Правда, следующий код приведёт к ошибке, так как в качестве первого аргумента должен быть указан тип элемента по умолчанию, а не итерируемый объект с парами ключ-значение:
Если же создать экземпляр defaultdict словаря без указания default_factory (значения по умолчанию для отсутствующих ключей), то поведение defaultdict будет таким же, как и у обычного словаря (тип dict). То же самое произойдёт, если передать значение None (является значением по умолчанию для default_factory) Следующий код вызовет ошибку KeyError:
1
2
3
4
5
fromcollectionsimportdefaultdictdata=defaultdict()# или data = defaultdict(None)print(data['salary'])
Если в ходе написания кода потребуется изменить значение по умолчанию, это можно осуществить через атрибут default_factory:
При создании defaultdict словаря можно указывать не только тип данных для значений по умолчанию, но и любую функцию, не принимающую аргументов и возвращающую некоторое дефолтное значение.
Рассмотрим задачу и решим её с помощью defaultdict: пусть задан список чисел numbers, в котором некоторые числа встречаются несколько раз. Нужно узнать, сколько именно раз встречается каждое из чисел.
Таким образом, при использовании defaultdict нет необходимости ни проверять наличие соответствующих ключей в словаре, ни создавать предварительно пустые списки.
Если ваш код в значительной степени основан на словарях и вы все время имеете дело с отсутствующими ключами, вам следует подумать об использовании defaultdict, а не обычного dict;
Если элементы вашего словаря необходимо инициализировать некоторым значением по умолчанию, вам следует подумать об использовании defaultdict, вместо dict;
Если ваш код использует словари для агрегирования, накопления, подсчета или группировки значений, вам следует подумать об использовании defaultdict, вместо dict.
К слову, тип defaultdict работает быстрее чем использование методов setdefault() и get() обычного словаря.
OrderedDict
Для использования: from collections import OrderedDict
Для использования: from collections import OrderedDict
В Python 3.6 словари были переработаны так, чтобы повысилась их производительность (и понизилось потребление памяти на 20-25%). Следствием такой переработки явился один очень интересный побочный эффект — словари стали упорядоченными, то есть стали сохранять порядок вставки элементов, хотя на тот момент официально этот порядок не гарантировался. «Официально не гарантируется» означает, что это была просто деталь реализации, которая могла быть изменена в будущих версиях Python. Но начиная с Python 3.7, в спецификации языка гарантируется сохранение порядка вставки элементов в словарь.
Задолго до переработки устройства словарей в рамках релиза Python 3.1 в стандартную библиотеку был добавлен тип OrderedDict, который на тот момент решал проблему неупорядоченности обычных словарей.
Стоит учесть, что в OrderedDict стоит использовать только для обратной совместимости со старыми программами на версиях Python, когда dict ещё не был упорядочен, ведь обычный словарь быстрее на 40% и занимает на 50% меньше памяти.
Таблица различий и особенностей классов dict и OrderedDict:
Функционал
`OrderedDict
dict
Сохранность порядка вставки ключей
Да (начиная с Python 3.1)
Да (начиная с Python 3.6)
Удобочитаемость и сигнализация о намерениях
Высокая
Низкая
Возможность менять порядок элементов
Да (метод move_to_end())
Нет
Производительность операций
Низкая
Высокая
Потребление памяти
Высокое
Низкое
Учет порядка элементов при сравнении на равенство
Да
Нет
Перебор ключей в обратном порядке
Да (начиная с Python 3.5)
Да (начиная с Python 3.8)
Возможность добавления пользовательских атрибутов
Да (атрибут .__dict__)
Нет
Возможность использовать операторы | и |=
Да (начиная с Python 3.9)
Да (начиная с Python 3.9)
OrderedDict является подклассом dict, а значит обладает всеми его методами, также имея собственные методы.
Как и defaultdict, эти словари можно создавать любым из доступных способов, как и обычные словари:
В большинстве случаев OrderedDict ведёт себя как и обычный словарь: программист может добавлять, обновлять, удалять, перебирать (по keys(), values() и items()) элементы, объединять словари с помощью операторов | и |= (конкатенация словарей). Словарь (обычный или OrderedDict) можно “развернуть” функцией reversed(). Обновляя значение ключа, позиция сохраняется, удаляя ключ и снова добавляя его, он помещается в конец словаря.
Инфо
Значение по ключу можно обновить как через квадратные скобки, так и через словарный метод update()
Метод move_to_end()
Метод move_to_end() позволяет переместить существующий элемент либо в конец, либо в начало словаря.
Ему можно передать два аргумента:
key (обязательный) – ключ, который идентифицирует перемещаемый элемент
last=True (необязательный) – логическое значение (тип bool), которое определяет, в какой конец словаря мы перемещаем элемент, значение True (по умолчанию) перемещает элемент в конец, значение False – в начало
Метод popitem(last=True) по умолчанию удаляет и возвращает элемент в порядке LIFO (Last-In/First-Out, последний пришел/первый ушел), то есть удаляет элементы с конца словаря.
OrderedDict словари содержат дополнительный атрибут __dict__, которого нет у обычного словаря. Данный атрибут используется для динамического наделения объектов дополнительным функционалом. Этот атрибут позволяет динамически добавлять пользовательские атрибуты в OrderedDict. Использовать его можно двумя способами:
В стиле словаря: ordered_dict.__dict__['attr'] = value;
Через точечную нотацию: ordered_dict.attr = value.
# Вывод:
OrderedDict({('b', 2), ('d', 4), ('a', 1), ('c', 3)})
['a', 'b', 'c', 'd']
OrderedDict({('b', 2), ('d', 4), ('a', 1), ('c', 3), ('e', 5)})
['a', 'b', 'c', 'd', 'e']
a -> 1
b -> 2
c -> 3
d -> 4
e -> 5
Если же методу передать необязательный аргумент last=False, то он начнет удалять и возвращать элементы в порядке FIFO (First-In/First-Out, первый пришел/первый ушел).
Сравнение словарей
Стоит просто запомнить три истины:
При сравнении на равенство обычных словарей порядок расположения их элементов неважен.
При сравнении на равенство dict и OrderedDict словарей порядок расположения их элементов неважен.
При сравнение на равенство OrderedDict словарей порядок расположения их элементов важен.
Counter
Для использования: from collections import Counter
Counter является подклассом dict, специально разработанным для подсчёта хэшируемых объектов в Python. Он хранит объекты в качестве ключей, а их количество — в качестве значений. Для подсчёта класс Counter использует высокооптимизированную функцию, написанную на языке C.
Инфо
С помощью типа Counter можно реализовать концепцию мультимножества
Есть несколько способов создать объект Counter. Например, можно передать коллекцию или итератор в конструктор:
Класс Counter, будучи подклассом типа dict, наследует все методы, предоставляемые обычным словарем. Но вызов метода fromkeys() всегда будет приводить к возникновению ошибки. Такое поведение не случайно, оно позволяет избежать ошибок неоднозначности при создании объектов типа Counter, например следующий код: counter = Counter.fromkeys('mississippi', 2) мог бы создать объект типа Counter на основе строки mississippi со значением по умолчанию равным 2 для всех символов строки, несмотря на реальное количество вхождений символов в строке mississippi.
Как и в обычных словарях, ключи в объектах Counter должны быть хэшируемы. Ограничений на тип значений нет, но для нормальной работы подсчёта объектов в качестве значений должны быть целые неотрицательные числа.
Доступ к элементам и итерирование по Counter словарям работает так же, как и у обычных словарей. Мы можем перебирать ключи напрямую или можем использовать словарные методы items(), keys() и values(). При этом, если обратиться по ключу, которого нет в Counter словаре, то ошибка KeyError возникать не будет. Будет возвращено нулевое значение (ключ создан не будет).
Объекты типа Counter можно сравнивать между собой. Очевидно, что одинаковыми будут те, что имеют одинаковые элементы (ключ: значение). Также до версии Python 3.10 словари Counter(i=4) и Counter(i=4, s=0) считались разными, но, начиная с Python 3.10 сравнение рассматривает отсутствующие элементы как имеющие нулевое значение, поэтому следующий код вернёт True:
Объекты класса Counter, аналогично объектам OrderedDict, содержат дополнительный атрибут __dict__, который используется для динамического наделения объектов дополнительным функционалом:
Для изменения объектов типа Counter рекомендуется использовать метод update(). Он не заменяет значения как у обычных словарей, а суммирует существующие. При этом для новых объектов update() создаёт новые пары ключ: количество.
Метод update() принимает любой итерируемый объект: список, строку, кортеж и т.д., другой объект типа Counter, либо обычный словарь. Также его можно использовать с именованными аргументами, например следующие две строки кода равнозначны: sales.update(apple=3, orange=12, banana=7) и sales.update(monday_sales).
Из предыдущего кода также можем заметить, что под ключом 's' содержится не '45', а '54'. Так устроен метод update() — он складывает значение переданного объекта с изменяемым, но не наоборот, то есть '5' + '4'.
Метод most_common()
most_common() возвращает список наиболее повторяемых элементов и количество каждого из них в виде кортежей (ключ, число повторений).
Для поиска самых редких элементов, можно использовать срезы с отрицательным шагом.
Метод elements()
elements() возвращает итератор по элементам, в котором каждый элемент повторяется столько раз, во сколько установлено его значение. Элементы возвращаются в порядке их появления. При этом, если количество элементов по некоторому ключу меньше единицы, то метод elements() просто проигнорирует его.
subtract() вычитает из значений элементов одного словаря Counter значения элементов другого словаря. Этот метод подобен update(), но вычитает количества, а не складывает их. При этом у результирующего словаря значения ключей могут быть нулевыми или отрицательными.
Помимо словарей, метод subtract() может принимать любой итерируемый объект: список, строку, кортеж и т.д., а также его можно использовать с именованными аргументами.
1
2
3
4
5
6
7
8
9
10
fromcollectionsimportCountercounter=Counter(i=4,s=40,a=1,p=20,b=98,z=69)letters='iisssssapppz'counter.subtract(letters)# обновляем значения в counterprint(counter)# Вывод: Counter({'b': 98, 'z': 68, 's': 35, 'p': 17, 'i': 2, 'a': 0})
Операторы +, -, &, |
Как мы уже знаем, методы update() и subtract() объединяют Counter словари путем сложения и вычитания количества соответствующих элементов. Python предоставляет удобные операторы сложения (+) и вычитания (-), которые могут заменить вызовы данных методов. При использовании этих операторов из результирующего словаря исключаются элементы с нулевыми и отрицательными значениями.
Операторы + и - работают только с Counter словарями, в то время как методы update() и subtract() — с любым итерируемым объектом
Counter позволяет также использовать унарные операторы сложения и вычитания. В первом случае мы получаем новый Counter словарь, который содержит только элементы с положительными значениями, во втором — элементы с отрицательными значениями. Другими словами, операторы унарного сложения и вычитания прибавляют пустой Counter словарь или вычитают исходный из пустого. Следующие два блока кодов равнозначны:
Помимо указанных выше операторов, Python также предоставляет операторы пересечения (&) и объединения (|), которые возвращают минимум и максимум из соответствующих значений.
Для использования: from collections import ChainMap
ChainMap представляет из себя объединение нескольких словарей. Этот объект группирует словари вместе, что позволяет рассматривать их как единое целое.
ChainMap был добавлен в модуль collections в версии Python 3.3. Этот класс не создаёт новый словарь, вместо этого он хранит ссылки на исходные словари в списке, что, грубо говоря, позволяет программисту иметь несколько одноимённых ключей в одном словаре.
Раз ChainMap содержит ссылки на объекты, то изменение содержания любого словаря, на основании которого создан ChainMap, изменяет и сам ChainMap объект. Аналогично, изменение ChainMap объекта приводит к изменению словаря, на основании которого он создан.
Чтобы создать объект ChainMap, можно, например, передать в конструктор словари:
В ChainMap можно также передать любой из уже изученных словарей: defaultdict, OrderedDict, Counter. При этом нужно понимать, что поиск по ChainMap объекту будет учитывать особенность поиска по соответствующим словарям. Для defaultdict, в случае если ключ отсутствует, вернётся значение по умолчанию, для Counter — нулевое значение.
Для получения значений по ключу в ChainMap объектах используется такой же механизм, как и в обычных словарях. Либо мы используем квадратные скобки, либо метод get(). Рассмотрим объект, в котором ключи повторяются:
Как видно, в ситуации, когда у объединяемых словарей есть повторяющиеся ключи, возвращается только первое вхождение этого ключа. Таким образом, поиск по ChainMap объекту всегда осуществляется в том же порядке, в котором словари были указаны при создании этого объекта, при этом поиск останавливается, как только значение по нужному ключу найдено.
Встроенная функция len() вернёт количество уникальных ключей ChainMap объекта.
При этом, если присутствуют дубликаты ключей, возвращаться будет последнее значение (имеется ввиду последнее при итерировании по объекту ChainMap, то есть первое значение, если идти сверху вниз).
Для изменения объектов типа ChainMap можно использовать те же способы, что и для изменения обычного словаря. Позволяется обновлять, добавлять, удалять и извлекать элементы. При этом нужно знать, что все эти операции действуют только на первый из объединяемых словарей.
При попытке удаления значения по ключу, которого нет в первом словаре, возникает ошибка KeyError
Указывая в качестве первого аргумента для ChainMap пустой словарь, получается поведение, при котором все изменения ChainMap объекта не затрагивают объединяемые (исходные) словари.
Сравнение ChainMap
Два объекта типа ChainMap (к примеру, chainmap1 и chainmap2) считаются равными, если значение следующего выражения равно True: dict(chainmap1.items()) == dict(chainmap2.items()).
Учитывая специфику работы метода items(), равенство двух объектов типа ChainMap не гарантирует того, что эти объекты в точности совпадают:
Как уже было сказано, Объект ChainMap хранит ссылки на все объединяемые словари во внутреннем списке, который доступен через атрибут maps и может быть изменён. Порядок словарей в списке maps соответствует порядку, в котором словари были указаны при создании объекта ChainMap.
При создании пустого ChainMap объекта его maps будет содержать пустой словарь.
Атрибут maps является обычным списком, поэтому он поддерживает все основные операции со списками. Мы можем добавлять в него новые словари, удалять уже добавленные, а также изменять их порядок.
Изменяя порядок словарей в списке атрибута maps, мы также меняем сами объединяемые словари, а также порядок поиска в объекте ChainMap
Атрибут maps можно использовать для обработки абсолютно всех значений во всех словарях. С помощью этого атрибута мы можем обойти поведение по умолчанию, заключающееся в получении (изменении) первого значения из первого словаря.
new_child() возвращает новый объектChainMap(), содержащий новый переданный словарь в качестве первого элемента, за которым следуют все исходные словари объекта, к которому этот метод применялся. Вызов этого метода (например, d.new_child()) эквивалентен вызову ChainMap({}, *d.maps).
parents возвращает новый объектChainMap, содержащий все словари, кроме первого. Может пригодиться в случае, когда нужно пропустить первый словарь при поиске ключей. Обращение к этому атрибуту (например, d.parents) эквивалентно вызову ChainMap(*d.maps[1:]).
Основным вариантом использования ChainMap является эффективное управление несколькими областями видимости и определение приоритетов доступа дубликатов ключей. Например, в документации по ChainMap можно найти, как Python обращается к именами переменных в разных пространствах имён. Когда интерпретатор ищет имя (переменную), он последовательно обращается к локальной, глобальной и встроенной (print, list, input и т.д.) областям видимости, которые представляют из себя словари, отображающие имена на объекты.