Коллекции Python: Широкий выбор специализированных типов данных
Оглавление
- Начало работы с коллекциями Python
- Улучшение читаемости кода: namedtuple()
- Построение эффективных очередей и стеков: deque
- Обработка отсутствующих ключей: defaultdict
- Упорядочивание ваших словарей: OrderedDict
- Подсчет объектов за один раз: Счетчик
- Объединение словарей в цепочку: ChainMap
- Настройка встроенных модулей: UserString, UserList и UserDict
- Заключение
Модуль Python collections предоставляет богатый набор специализированных контейнерных типов данных, тщательно разработанных для решения конкретных задач программирования эффективным способом на языке Python. Модуль также предоставляет классы-оболочки, которые делают более безопасным создание пользовательских классов, которые ведут себя аналогично встроенным типам dict, list, и str.
Изучение типов данных и классов в collections позволит вам пополнить свой набор инструментов программирования ценным набором надежных и эффективных инструментов.
В этом руководстве вы узнаете, как:
- Написать читаемый и явный код с
namedtuple - Создавать эффективные очереди и стеки с помощью
deque - Быстро подсчитайте объектов с помощью
Counter - Обработать отсутствующие ключи словаря с помощью
defaultdict - Гарантировать порядок ввода ключей с
OrderedDict - Управление несколькими словарями как единым целым с
ChainMap
Чтобы лучше понять типы данных и классы в collections, вы должны знать основы работы со встроенными типами данных Python, такими как списки, кортежи и словари. Кроме того, последняя часть статьи требует некоторых базовых знаний о объектно-ориентированном программировании на Python.
Скачать бесплатно: Ознакомьтесь с примером главы из книги "Приемы работы с Python: Книга", в которой показаны лучшие практики Python на простых примерах, которые вы можете подайте заявку немедленно, чтобы написать более красивый + Pythonic код.
Начало работы с Python collections
Еще в Python 2.4, Раймонд Хеттингер добавил новый модуль под названием collections в стандартную библиотеку. Цель состояла в том, чтобы предоставить различные специализированные типы данных для сбора данных для решения конкретных задач программирования.
В то время collections включал только одну структуру данных, deque, которая была специально разработана как двойная-завершенная очередь, которая поддерживает эффективные операции добавления и извлечения на обоих концах последовательности. С этого момента несколько модулей стандартной библиотеки использовали преимущества deque для повышения производительности своих классов и структур. Вот несколько выдающихся примеров queue и threading.
Со временем модуль пополнился несколькими специализированными типами контейнерных данных:
| Data type | Python version | Description |
|---|---|---|
deque |
2.4 | A sequence-like collection that supports efficient addition and removal of items from either end of the sequence |
defaultdict |
2.5 | A dictionary subclass for constructing default values for missing keys and automatically adding them to the dictionary |
namedtuple() |
2.6 | A factory function for creating subclasses of tuple that provides named fields that allow accessing items by name while keeping the ability to access items by index |
OrderedDict |
2.7, 3.1 | A dictionary subclass that keeps the key-value pairs ordered according to when the keys are inserted |
Counter |
2.7, 3.1 | A dictionary subclass that supports convenient counting of unique items in a sequence or iterable |
ChainMap |
3.3 | A dictionary-like class that allows treating a number of mappings as a single dictionary object |
Помимо этих специализированных типов данных, collections также предоставляет три базовых класса, которые облегчают создание пользовательских списков, словарей и строк:
| Class | Description |
|---|---|
UserDict |
A wrapper class around a dictionary object that facilitates subclassing dict |
UserList |
A wrapper class around a list object that facilitates subclassing list |
UserString |
A wrapper class around a string object that facilitates subclassing string |
Необходимость в этих классах-оболочках была частично устранена возможностью создания подклассов соответствующих стандартных встроенных типов данных. Однако иногда использование этих классов более безопасно и менее подвержено ошибкам, чем использование стандартных типов данных.
После этого краткого ознакомления с collections и конкретными вариантами использования, которые могут быть решены структурами данных и классами в этом модуле, пришло время рассмотреть их поближе. Перед этим важно отметить, что это руководство представляет собой введение в collections в целом. В большинстве следующих разделов вы найдете синее окно с предупреждением, которое приведет вас к специальной статье о данном классе или функции.
Улучшение читаемости кода: namedtuple()
namedtuple() в Python - это заводская функция, которая позволяет создавать tuple подклассов с именованными полями. Эти поля предоставляют вам прямой доступ к значениям в заданном именованном кортеже, используя точечное обозначение, как в obj.attr.
Необходимость в этой функции возникла из-за того, что использование индексов для доступа к значениям в обычном кортеже раздражает, его трудно читать и оно чревато ошибками. Это особенно актуально, если кортеж, с которым вы работаете, состоит из нескольких элементов и создан далеко от того места, где вы его используете.
Примечание: Ознакомьтесь с Написанием кода на Python и его очисткой с помощью namedtuple для более глубокого понимания того, как использовать namedtuple в Python.
Подкласс кортежей с именованными полями, к которым разработчики могут обращаться с помощью точечной записи, казался желательной функцией еще в Python 2.6. Это источник namedtuple(). Подклассы кортежей, которые вы можете создать с помощью этой функции, значительно улучшают читаемость кода, если сравнить их с обычными кортежами.
Чтобы рассмотреть проблему удобочитаемости кода в перспективе, рассмотрим divmod(). Эта встроенная функция принимает два (несложных) числа и возвращает кортеж с частным и остатком, которые получаются в результате целочисленного деления входных значений:
>>> divmod(12, 5) (2, 2)предварительно> кодовый блок>Это прекрасно работает. Однако читабелен ли этот результат? Можете ли вы сказать, что означает каждое число в выходных данных? К счастью, Python предлагает способ улучшить это. Вы можете закодировать пользовательскую версию
divmod()с явным результатом, используяnamedtuple:>>> from collections import namedtuple >>> def custom_divmod(x, y): ... DivMod = namedtuple("DivMod", "quotient remainder") ... return DivMod(*divmod(x, y)) ... >>> result = custom_divmod(12, 5) >>> result DivMod(quotient=2, remainder=2) >>> result.quotient 2 >>> result.remainder 2предварительно> кодовый блок>Теперь вы знаете значение каждого значения в результате. Вы также можете получить доступ к каждому независимому значению, используя точечное обозначение и описательное название поля.
Чтобы создать новый подкласс кортежей с помощью
namedtuple(), вам нужны два обязательных аргумента:
typenameэто имя класса, который вы создаете. Это должна быть строка с допустимым идентификатором Python.field_namesэто список имен полей, которые вы будете использовать для доступа к элементам в результирующем кортеже. Это может быть:
- Набор повторяемых строк, таких как
["field1", "field2", ..., "fieldN"]- Строка с именами полей, разделенными пробелами, например
"field1 field2 ... fieldN"- Строка с именами полей, разделенными запятыми, например
"field1, field2, ..., fieldN"Например, вот различные способы создания образца 2D
Pointс двумя координатами (xиy) с помощьюnamedtuple():>>> from collections import namedtuple >>> # Use a list of strings as field names >>> Point = namedtuple("Point", ["x", "y"]) >>> point = Point(2, 4) >>> point Point(x=2, y=4) >>> # Access the coordinates >>> point.x 2 >>> point.y 4 >>> point[0] 2 >>> # Use a generator expression as field names >>> Point = namedtuple("Point", (field for field in "xy")) >>> Point(2, 4) Point(x=2, y=4) >>> # Use a string with comma-separated field names >>> Point = namedtuple("Point", "x, y") >>> Point(2, 4) Point(x=2, y=4) >>> # Use a string with space-separated field names >>> Point = namedtuple("Point", "x y") >>> Point(2, 4) Point(x=2, y=4)предварительно> кодовый блок>В этих примерах вы сначала создаете
Point, используяlistимен полей. Затем вы создаете экземплярPoint, чтобы создать объектpoint. Обратите внимание, что вы можете получить доступ кxиyпо имени поля, а также по индексу.В остальных примерах показано, как создать эквивалентный именованный кортеж из строки имен полей, разделенных запятыми, генерирующего выражения и строки имен полей, разделенных пробелами.
Именованные кортежи также предоставляют множество интересных функций, которые позволяют вам определять значения по умолчанию для ваших полей, создавать словарь из заданного именованного кортежа, заменять значение данного поля и многое другое:
>>> from collections import namedtuple >>> # Define default values for fields >>> Person = namedtuple("Person", "name job", defaults=["Python Developer"]) >>> person = Person("Jane") >>> person Person(name='Jane', job='Python Developer') >>> # Create a dictionary from a named tuple >>> person._asdict() {'name': 'Jane', 'job': 'Python Developer'} >>> # Replace the value of a field >>> person = person._replace(job="Web Developer") >>> person Person(name='Jane', job='Web Developer')предварительно> кодовый блок>Здесь вы сначала создаете класс
Person, используяnamedtuple(). На этот раз вы используете необязательный аргументdefaults, который принимает последовательность значений по умолчанию для полей кортежа. Обратите внимание, чтоnamedtuple()применяет значения по умолчанию к крайним правым полям.Во втором примере вы создаете словарь из существующего именованного кортежа, используя
._asdict(). Этот метод возвращает новый словарь, который использует имена полей в качестве ключей.Наконец, вы используете
._replace()для замены исходного значенияjob. Этот метод не обновляет кортеж вместо , а возвращает новый именованный кортеж с новым значением, сохраненным в соответствующем поле. У вас есть представление о том, почему._replace()возвращает новый именованный кортеж?Создание эффективных очередей и стеков:
dequePython
dequeбыла первой структурой данных вcollections. Этот тип данных, похожий на последовательность, является обобщением стеков и очередей, предназначенных для поддержки экономичного и быстрого добавления и выполняют операций на обоих концах структуры данных.Примечание: Слово
dequeпроизносится как “палуба” и означает double-eнайдено что-тонапример.В Python операции добавления и извлечения в начале или слева от объектов
listнеэффективны, поскольку O(n) требуют времени. Эти операции особенно дороги, если вы работаете с большими списками, потому что Python должен перемещать все элементы вправо, чтобы вставлять новые элементы в начало списка.С другой стороны, операции добавления и извлечения в правой части списка обычно эффективны (O(1)) за исключением тех случаев, когда Python необходимо перераспределить память для увеличения объема базовый список для приема новых товаров.
Программа Python
dequeбыла создана для решения этой проблемы. Операции добавления и выгрузки с обеих сторон объектаdequeстабильны и одинаково эффективны, поскольку определения реализованы в виде двусвязного списка. Вот почему deques особенно полезны для создания стеков и очередей.Возьмем в качестве примера очередь. Он управляет элементами в режиме "Первый вход/первый выход". ( FIFO) мода. Это работает как конвейер, в который вы помещаете новые элементы на одном конце конвейера и извлекаете старые элементы с другого конца. Добавление элемента в конец очереди называется операцией постановка в очередь. Удаление элемента из начала очереди называется Удалением из очереди.
Примечание: Ознакомьтесь с Python's deque: Внедрение эффективных очередей и стеков для подробного изучения использования
dequeв вашем коде на Python.Теперь предположим, что вы моделируете очередь людей, ожидающих покупки билетов в кино. Вы можете сделать это с помощью
deque. Каждый раз, когда приходит новый человек, вы ставите его в очередь. Когда человек, стоящий в начале очереди, получит свои билеты, вы выводите его из очереди.Вот как вы можете эмулировать процесс, используя объект
deque:>>> from collections import deque >>> ticket_queue = deque() >>> ticket_queue deque([]) >>> # People arrive to the queue >>> ticket_queue.append("Jane") >>> ticket_queue.append("John") >>> ticket_queue.append("Linda") >>> ticket_queue deque(['Jane', 'John', 'Linda']) >>> # People bought their tickets >>> ticket_queue.popleft() 'Jane' >>> ticket_queue.popleft() 'John' >>> ticket_queue.popleft() 'Linda' >>> # No people on the queue >>> ticket_queue.popleft() Traceback (most recent call last): File "<stdin>", line 1, in <module> IndexError: pop from an empty dequeпредварительно> кодовый блок>Здесь вы сначала создаете пустой объект
dequeдля представления очереди людей. Чтобы поместить пользователя в очередь, вы можете использовать.append(),, который добавляет элементы в правый конец списка. Чтобы удалить пользователя из очереди, вы используете.popleft(),, который удаляет и возвращает элементы в левой части списка.Примечание: В стандартной библиотеке Python вы найдете
queue. Этот модуль реализует очереди с несколькими производителями и несколькими потребителями, что полезно для безопасного обмена информацией между несколькими потоками.Инициализатор
dequeпринимает два необязательных аргумента:
iterableсодержит итерацию, которая служит в качестве инициализатора.maxlenсодержит целое число, которое определяет максимальную длинуdeque.Если вы не укажете
iterable, то получите пустой запрос. Если вы укажете значение вmaxlen,, то в вашем deque будет храниться толькоmaxlenэлементов.Наличие
maxlen- удобная функция. Например, предположим, что вам нужно создать список последних файлов в одном из ваших приложений. В этом случае вы можете сделать следующее:>>> from collections import deque >>> recent_files = deque(["core.py", "README.md", "__init__.py"], maxlen=3) >>> recent_files.appendleft("database.py") >>> recent_files deque(['database.py', 'core.py', 'README.md'], maxlen=3) >>> recent_files.appendleft("requirements.txt") >>> recent_files deque(['requirements.txt', 'database.py', 'core.py'], maxlen=3)предварительно> кодовый блок>Как только файл deque достигнет максимального размера (в данном случае три файла), добавление нового файла в конец файла deque автоматически приведет к удалению файла в противоположном конце. Если вы не укажете значение
maxlen, то список может увеличиться до произвольного количества элементов.На данный момент вы ознакомились с основами списков, в том числе с тем, как их создавать, а также как добавлять и извлекать элементы с обоих концов данного списка. Списки предоставляют некоторые дополнительные возможности в виде интерфейса, похожего на список. Вот некоторые из них:
>>> from collections import deque >>> # Use different iterables to create deques >>> deque((1, 2, 3, 4)) deque([1, 2, 3, 4]) >>> deque([1, 2, 3, 4]) deque([1, 2, 3, 4]) >>> deque("abcd") deque(['a', 'b', 'c', 'd']) >>> # Unlike lists, deque doesn't support .pop() with arbitrary indices >>> deque("abcd").pop(2) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: pop() takes no arguments (1 given) >>> # Extend an existing deque >>> numbers = deque([1, 2]) >>> numbers.extend([3, 4, 5]) >>> numbers deque([1, 2, 3, 4, 5]) >>> numbers.extendleft([-1, -2, -3, -4, -5]) >>> numbers deque([-5, -4, -3, -2, -1, 1, 2, 3, 4, 5]) >>> # Insert an item at a given position >>> numbers.insert(5, 0) >>> numbers deque([-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5])предварительно> кодовый блок>В этих примерах вы сначала создаете deques, используя различные типы iterables для их инициализации. Одно из отличий между
dequeиlistзаключается в том, чтоdeque.pop()не поддерживает отображение элемента с заданным индексом.Обратите внимание, что
dequeпредоставляет родственные методы для.append(),.pop(), и.extend()с суффиксомleft, указывающим на то, что они выполняют соответствующую операцию в левом конце базового deque.Команды Deques также поддерживают операции с последовательностью:
Method Description .clear()Remove all the elements from a deque .copy()Create a shallow copy of a deque .count(x)Count the number of deque elements equal to x.remove(value)Remove the first occurrence of valueЕще одной интересной особенностью deques является возможность поворачивать их элементы с помощью
.rotate():>>> from collections import deque >>> ordinals = deque(["first", "second", "third"]) >>> ordinals.rotate() >>> ordinals deque(['third', 'first', 'second']) >>> ordinals.rotate(2) >>> ordinals deque(['first', 'second', 'third']) >>> ordinals.rotate(-2) >>> ordinals deque(['third', 'first', 'second']) >>> ordinals.rotate(-1) >>> ordinals deque(['first', 'second', 'third'])предварительно> кодовый блок>При этом методе число шагов
nсдвигается вправо. Значениеnпо умолчанию равно1. Если вы укажете отрицательное значение дляn, то произойдет поворот влево.Наконец, вы можете использовать индексы для доступа к элементам в deque, но вы не можете срезать элемент deque:
>>> from collections import deque >>> ordinals = deque(["first", "second", "third"]) >>> ordinals[1] 'second' >>> ordinals[0:2] Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: sequence index must be integer, not 'slice'предварительно> кодовый блок>Deques поддерживают индексацию, но, что интересно, они не поддерживают разбиение на части. При попытке извлечь фрагмент из существующего deque вы получаете
TypeError. Это связано с тем, что выполнение операции среза в связанном списке было бы неэффективным, поэтому операция недоступна.Обработка недостающих ключей:
defaultdictРаспространенная проблема, с которой вы столкнетесь при работе с словарями в Python, заключается в том, как обрабатывать отсутствующие ключи. Если вы попытаетесь получить доступ к ключу, которого нет в данном словаре, то получите
KeyError:>>> favorites = {"pet": "dog", "color": "blue", "language": "Python"} >>> favorites["fruit"] Traceback (most recent call last): File "<stdin>", line 1, in <module> KeyError: 'fruit'предварительно> кодовый блок>Существует несколько способов обойти эту проблему. Например, вы можете использовать
.setdefault(). Этот метод принимает ключ в качестве аргумента. Если ключ существует в словаре, то он возвращает соответствующее значение. В противном случае метод вставляет ключ, присваивает ему значение по умолчанию и возвращает это значение:>>> favorites = {"pet": "dog", "color": "blue", "language": "Python"} >>> favorites.setdefault("fruit", "apple") 'apple' >>> favorites {'pet': 'dog', 'color': 'blue', 'language': 'Python', 'fruit': 'apple'} >>> favorites.setdefault("pet", "cat") 'dog' >>> favorites {'pet': 'dog', 'color': 'blue', 'language': 'Python', 'fruit': 'apple'}предварительно> кодовый блок>В этом примере вы используете
.setdefault()для создания значения по умолчанию дляfruit. Поскольку этот ключ не существует вfavorites,.setdefault(), создает его и присваивает ему значениеapple. Если вы вызовете.setdefault()с существующим ключом, то вызов не повлияет на словарь, и ваш ключ будет содержать исходное значение вместо значения по умолчанию.Вы также можете использовать
.get(), чтобы вернуть подходящее значение по умолчанию, если заданный ключ отсутствует:>>> favorites = {"pet": "dog", "color": "blue", "language": "Python"} >>> favorites.get("fruit", "apple") 'apple' >>> favorites {'pet': 'dog', 'color': 'blue', 'language': 'Python'}предварительно> кодовый блок>Здесь
.get()возвращаетapple, поскольку ключ отсутствует в базовом словаре. Однако.get()не создает новый ключ для вас.Поскольку обработка отсутствующих ключей в словарях является распространенной потребностью,
collectionsв Python также имеется инструмент для этого. Типdefaultdictявляется подклассомdict, предназначенным для того, чтобы помочь вам найти недостающие ключи.Примечание: Ознакомьтесь с Использованием типа Python defaultdict для обработки отсутствующих ключей для более глубокого понимания того, как использовать
defaultdict.Конструктор
defaultdictпринимает объект функции в качестве своего первого аргумента. Когда вы обращаетесь к несуществующему ключу,defaultdictавтоматически вызывает эту функцию без аргументов, чтобы создать подходящее значение по умолчанию для имеющегося ключа.Чтобы обеспечить его функциональность,
defaultdictсохраняет входную функцию в виде.default_factory, а затем переопределяет.__missing__()для автоматического вызова функции и генерации значения по умолчанию при обращении к любым отсутствующим клавишам.Вы можете использовать любой вызываемый объект для инициализации ваших объектов
defaultdict. Например, с помощьюint()вы можете создать подходящий счетчик для подсчета различных объектов:>>> from collections import defaultdict >>> counter = defaultdict(int) >>> counter defaultdict(<class 'int'>, {}) >>> counter["dogs"] 0 >>> counter defaultdict(<class 'int'>, {'dogs': 0}) >>> counter["dogs"] += 1 >>> counter["dogs"] += 1 >>> counter["dogs"] += 1 >>> counter["cats"] += 1 >>> counter["cats"] += 1 >>> counter defaultdict(<class 'int'>, {'dogs': 3, 'cats': 2})предварительно> кодовый блок>В этом примере вы создаете пустой
defaultdictсint()в качестве первого аргумента. Когда вы обращаетесь к несуществующему ключу, словарь автоматически вызываетint(), который возвращает0в качестве значения по умолчанию для имеющегося ключа. Такого рода объектыdefaultdictвесьма полезны, когда дело доходит до подсчета данных в Python.Еще один распространенный вариант использования
defaultdict- это группировка объектов. В этом случае удобной функцией factory являетсяlist():>>> from collections import defaultdict >>> pets = [ ... ("dog", "Affenpinscher"), ... ("dog", "Terrier"), ... ("dog", "Boxer"), ... ("cat", "Abyssinian"), ... ("cat", "Birman"), ... ] >>> group_pets = defaultdict(list) >>> for pet, breed in pets: ... group_pets[pet].append(breed) ... >>> for pet, breeds in group_pets.items(): ... print(pet, "->", breeds) ... dog -> ['Affenpinscher', 'Terrier', 'Boxer'] cat -> ['Abyssinian', 'Birman']предварительно> кодовый блок>В этом примере у вас есть необработанные данные о домашних животных и их породе, и вам нужно сгруппировать их по домашним животным. Чтобы сделать это, вы используете
list()как.default_factoryпри создании экземпляраdefaultdict. Это позволяет вашему словарю автоматически создавать пустой список ([]) в качестве значения по умолчанию для каждого пропущенного ключа, к которому вы обращаетесь. Затем вы используете этот список для хранения пород ваших домашних животных.Наконец, вы должны отметить, что, поскольку
defaultdictявляется подклассомdict, он предоставляет тот же интерфейс. Это означает, что вы можете использовать своиdefaultdictобъекты так, как если бы вы использовали обычный словарь.Упорядочивание ваших словарей:
OrderedDictИногда вам нужно, чтобы ваши словари запоминали порядок, в котором вставляются пары ключ-значение. Обычные словари Python были неупорядоченными структуры данных в течение многих лет. Итак, еще в 2008 году в PEP 372 была представлена идея добавления нового класса словаря в
collections.Новый класс будет запоминать порядок элементов в зависимости от момента, в который были вставлены ключи. Это было началом
OrderedDict.
OrderedDictбыл представлен в Python 3.1. Его интерфейс прикладного программирования (API) практически такой же, как и вdict. ОднакоOrderedDictвыполняет итерацию по ключам и значениям в том же порядке, в каком ключи были впервые вставлены в словарь. Если вы присваиваете новое значение существующему ключу, порядок следования пары ключ-значение остается неизменным. Если запись удалена и вставлена повторно, то она будет перемещена в конец словаря.Примечание: Ознакомьтесь с OrderedDict против dict в Python: Подходящий инструмент для работы для более глубокого погружения в Python
OrderedDictи почему вам следует рассмотреть возможность его использования.Существует несколько способов создания объектов
OrderedDict. Большинство из них идентичны способам создания обычного словаря. Например, вы можете создать пустой упорядоченный словарь, создав экземпляр класса без аргументов, а затем вставить пары ключ-значение по мере необходимости:>>> from collections import OrderedDict >>> life_stages = OrderedDict() >>> life_stages["childhood"] = "0-9" >>> life_stages["adolescence"] = "9-18" >>> life_stages["adulthood"] = "18-65" >>> life_stages["old"] = "+65" >>> for stage, years in life_stages.items(): ... print(stage, "->", years) ... childhood -> 0-9 adolescence -> 9-18 adulthood -> 18-65 old -> +65предварительно> кодовый блок>В этом примере вы создаете пустой упорядоченный словарь, создавая экземпляр
OrderedDictбез аргументов. Затем вы добавляете пары ключ-значение в словарь, как если бы вы использовали обычный словарь.Когда вы выполняете итерацию по словарю,
life_stages,, вы получаете пары ключ-значение в том же порядке, в каком вы вставляли их в словарь. Гарантированный порядок элементов - это основная проблема, которую решаетOrderedDict.В Python 3.6 появилась новая реализация
dict. Эта реализация предоставляет неожиданную новую функцию: теперь обычные словари сохраняют свои элементы в том же порядке, в каком они были вставлены впервые.Изначально эта функция считалась деталью реализации, и в документации советовали не полагаться на нее. Однако, начиная с Python 3.7, функция официально является частью спецификации языка. Итак, какой смысл использовать
OrderedDict?У
OrderedDictесть некоторые особенности, которые по-прежнему делают его ценным:
- Сообщение о намерениях: С помощью
OrderedDictВ вашем коде будет ясно указано, что порядок элементов в словаре важен. Вы ясно даете понять, что ваш код нуждается в порядке элементов в базовом словаре или полагается на него.- Управление порядком элементов: С помощью
OrderedDictу вас есть доступ к.move_to_end(), который является методом, позволяющим вам изменять порядок элементов в вашем словаре. У вас также будет расширенный вариант.popitem(), который позволяет удалять элементы из любого конца базового словаря.- Поведение при проверке на равенство: С
OrderedDictПри проверке на равенство между словарями учитывается порядок элементов. Итак, если у вас есть два упорядоченных словаря с одной и той же группой элементов, но в разном порядке, то ваши словари будут считаться неравнозначными.Есть, по крайней мере, еще одна причина для использования
OrderedDict: обратная совместимость. Использование обычных объектовdictдля сохранения порядка элементов приведет к нарушению работы вашего кода в средах, использующих версии Python старше 3.6.Итак, теперь пришло время увидеть некоторые из этих интересных функций
OrderedDictв действии:>>> from collections import OrderedDict >>> letters = OrderedDict(b=2, d=4, a=1, c=3) >>> letters OrderedDict([('b', 2), ('d', 4), ('a', 1), ('c', 3)]) >>> # Move b to the right end >>> letters.move_to_end("b") >>> letters OrderedDict([('d', 4), ('a', 1), ('c', 3), ('b', 2)]) >>> # Move b to the left end >>> letters.move_to_end("b", last=False) >>> letters OrderedDict([('b', 2), ('d', 4), ('a', 1), ('c', 3)]) >>> # Sort letters by key >>> for key in sorted(letters): ... letters.move_to_end(key) ... >>> letters OrderedDict([('a', 1), ('b', 2), ('c', 3), ('d', 4)])предварительно> кодовый блок>В этих примерах вы используете
.move_to_end()для перемещения элементов и изменения порядкаletters. Обратите внимание, что.move_to_end()принимает необязательный аргументlast, который позволяет вам управлять тем, в какой конец словаря вы хотите переместить элементы. Этот метод очень удобен, когда вам нужно отсортировать элементы в ваших словарях или когда вам нужно каким-либо образом изменить их порядок.Еще одно важное различие между
OrderedDictи обычным словарем заключается в том, как они сравниваются на предмет равенства:>>> from collections import OrderedDict >>> # Regular dictionaries compare the content only >>> letters_0 = dict(a=1, b=2, c=3, d=4) >>> letters_1 = dict(b=2, a=1, d=4, c=3) >>> letters_0 == letters_1 True >>> # Ordered dictionaries compare content and order >>> letters_0 = OrderedDict(a=1, b=2, c=3, d=4) >>> letters_1 = OrderedDict(b=2, a=1, d=4, c=3) >>> letters_0 == letters_1 False >>> letters_2 = OrderedDict(a=1, b=2, c=3, d=4) >>> letters_0 == letters_2 Trueпредварительно> кодовый блок>Здесь
letters_1имеет порядок элементов, отличный отletters_0. При использовании обычных словарей эта разница не имеет значения, и оба словаря сравниваются одинаково. С другой стороны, когда вы используете упорядоченные словари,letters_0иletters_1не равны. Это связано с тем, что тесты на равенство между упорядоченными словарями учитывают содержимое, а также порядок элементов.Подсчет объектов за один раз:
CounterПодсчет объектов - обычная операция в программировании. Допустим, вам нужно подсчитать, сколько раз данный элемент появляется в списке или повторяется. Если ваш список короткий, то подсчет его элементов может быть простым и быстрым. Если у вас длинный список, то подсчитать количество пунктов будет сложнее.
Для подсчета объектов обычно используется счетчик или целая переменная с начальным значением, равным нулю. Затем вы увеличиваете счетчик, чтобы отразить количество раз, когда встречается данный объект.
В Python вы можете использовать словарь для одновременного подсчета нескольких различных объектов. В этом случае ключи будут хранить отдельные объекты, а значения будут содержать количество повторений данного объекта или количество объектов .
Вот пример, в котором подсчитываются буквы в слове
"mississippi"с помощью обычного словаря и циклаfor:>>> word = "mississippi" >>> counter = {} >>> for letter in word: ... if letter not in counter: ... counter[letter] = 0 ... counter[letter] += 1 ... >>> counter {'m': 1, 'i': 4, 's': 4, 'p': 2}предварительно> кодовый блок>Цикл выполняет итерацию по буквам в
word. Условный оператор проверяет, нет ли этих букв в словаре, и соответствующим образом обнуляет количество букв. Последним шагом является увеличение количества букв по мере прохождения цикла.Как вы уже знаете,
defaultdictобъекты удобны, когда дело доходит до подсчета, потому что вам не нужно проверять, существует ли ключ. Словарь гарантирует соответствующие значения по умолчанию для любых отсутствующих ключей:>>> from collections import defaultdict >>> counter = defaultdict(int) >>> for letter in "mississippi": ... counter[letter] += 1 ... >>> counter defaultdict(<class 'int'>, {'m': 1, 'i': 4, 's': 4, 'p': 2})предварительно> кодовый блок>В этом примере вы создаете объект
defaultdictи инициализируете его с помощьюint(). Используяint()в качестве заводской функции, базовый словарь по умолчанию автоматически создает недостающие ключи и удобно инициализирует их нулем. Затем вы увеличиваете значение текущего ключа, чтобы вычислить окончательное количество букв в"mississippi".Как и в случае с другими распространенными проблемами программирования, в Python также есть эффективный инструмент для решения проблемы подсчета. В
collectionsвы найдетеCounter, который является подклассомdict, специально разработанным для подсчета объектов.Вот как вы можете написать пример
"mississippi", используяCounter:>>> from collections import Counter >>> Counter("mississippi") Counter({'i': 4, 's': 4, 'p': 2, 'm': 1})предварительно> кодовый блок>Ничего себе! Это было быстро! Всего одна строка кода - и готово. В этом примере
Counterповторяется по"mississippi", создавая словарь с буквами в качестве ключей и их частотностью в качестве значений.Примечание: Ознакомьтесь с Python's Counter: Pythonic способ подсчета объектов для более глубокого погружения в
Counterи как его использовать для эффективного подсчета объектов.Существует несколько различных способов создания экземпляра
Counter. Вы можете использовать списки, кортежи или любые повторяющиеся объекты с повторяющимися объектами. Единственное ограничение заключается в том, что ваши объекты должны быть хэшируемыми:>>> from collections import Counter >>> Counter([1, 1, 2, 3, 3, 3, 4]) Counter({3: 3, 1: 2, 2: 1, 4: 1}) >>> Counter(([1], [1])) Traceback (most recent call last): ... TypeError: unhashable type: 'list'предварительно> кодовый блок>Целые числа могут быть хэшированы, поэтому
Counterработает корректно. С другой стороны, списки не могут быть хэшированы, поэтомуCounterзавершается ошибкой сTypeError.Быть хэшируемым означает, что ваши объекты должны иметь хэш-значение, которое никогда не меняется в течение их жизненного цикла. Это обязательное условие, поскольку эти объекты будут работать как ключи словаря. В Python неизменяемые объекты также доступны для хэширования.
Примечание: В
Counterвысоко оптимизированная функция C обеспечивает функциональность подсчета. Если эта функция по какой-либо причине недоступна, то класс использует эквивалентную, но менее эффективную функцию Python.Поскольку
Counterявляется подклассомdict, их интерфейсы в основном одинаковы. Однако есть некоторые тонкие различия. Первое отличие заключается в том, чтоCounterне реализует.fromkeys(). Это позволяет избежать несоответствий, таких какCounter.fromkeys("abbbc", 2), в которых каждая буква будет иметь начальное значение2, независимо от реального значения, которое она имеет во входной итеративной переменной.Второе отличие заключается в том, что
.update()не заменяет количество (значение) существующего объекта (ключа) новым количеством. Это суммирует оба значения вместе:>>> from collections import Counter >>> letters = Counter("mississippi") >>> letters Counter({'i': 4, 's': 4, 'p': 2, 'm': 1}) >>> # Update the counts of m and i >>> letters.update(m=3, i=4) >>> letters Counter({'i': 8, 'm': 4, 's': 4, 'p': 2}) >>> # Add a new key-count pair >>> letters.update({"a": 2}) >>> letters Counter({'i': 8, 'm': 4, 's': 4, 'p': 2, 'a': 2}) >>> # Update with another counter >>> letters.update(Counter(["s", "s", "p"])) >>> letters Counter({'i': 8, 's': 6, 'm': 4, 'p': 3, 'a': 2})предварительно> кодовый блок>Здесь вы обновляете счетчик для
mиi. Теперь эти буквы содержат сумму их начального количества плюс значение, которое вы передали им через.update(). Если вы используете ключ, которого нет в исходном счетчике, то.update()создает новый ключ с соответствующим значением. Наконец,.update()принимает повторяющиеся значения, сопоставления, аргументы ключевых слов, а также другие счетчики.Примечание: Поскольку
Counterявляется подклассомdict, нет никаких ограничений на объекты, которые вы можете хранить в ключах и значениях ваших счетчиков . Ключи могут хранить любые хэшируемые объекты, в то время как значения могут хранить любые объекты. Однако для логической работы в качестве счетчиков значения должны быть целыми числами, представляющими счетчики.Еще одно различие между
Counterиdictзаключается в том, что доступ к отсутствующему ключу возвращает0вместо вызоваKeyError:>>> from collections import Counter >>> letters = Counter("mississippi") >>> letters["a"] 0предварительно> кодовый блок>Такое поведение сигнализирует о том, что количество объектов, которые не существуют в счетчике, равно нулю. В этом примере буквы
"a"нет в исходном слове, поэтому ее количество равно0.В Python
Counterтакже полезно для эмуляции мультисети или пакета. Мультинаборы похожи на наборов, но они допускают несколько экземпляров данного элемента. Количество экземпляров элемента известно как его кратность. Например, у вас может быть такой мультимножество, как {1, 1, 2, 3, 3, 3, 4, 4}.Когда вы используете
Counterдля эмуляции мультинаборов, ключи представляют элементы, а значения - их соответствующую кратность:>>> from collections import Counter >>> multiset = Counter([1, 1, 2, 3, 3, 3, 4, 4]) >>> multiset Counter({1: 2, 2: 1, 3: 3, 4: 2}) >>> multiset.keys() == {1, 2, 3, 4} Trueпредварительно> кодовый блок>Здесь ключи
multisetэквивалентны набору Python. Значения содержат кратность каждого элемента в наборе.Python’
Counterпредоставляет несколько дополнительных функций, которые помогут вам работать с ними как с мультинаборами. Например, вы можете инициализировать свои счетчики с помощью сопоставления элементов и их кратности. Вы также можете выполнять математические операции с кратностью элементов и многое другое.Допустим, вы работаете в местном приюте для домашних животных. У вас есть определенное количество домашних животных, и вам нужно вести учет того, сколько питомцев усыновляется каждый день и сколько питомцев входят в приют и покидают его. В этом случае вы можете использовать
Counter:>>> from collections import Counter >>> inventory = Counter(dogs=23, cats=14, pythons=7) >>> adopted = Counter(dogs=2, cats=5, pythons=1) >>> inventory.subtract(adopted) >>> inventory Counter({'dogs': 21, 'cats': 9, 'pythons': 6}) >>> new_pets = {"dogs": 4, "cats": 1} >>> inventory.update(new_pets) >>> inventory Counter({'dogs': 25, 'cats': 10, 'pythons': 6}) >>> inventory = inventory - Counter(dogs=2, cats=3, pythons=1) >>> inventory Counter({'dogs': 23, 'cats': 7, 'pythons': 5}) >>> new_pets = {"dogs": 4, "pythons": 2} >>> inventory += new_pets >>> inventory Counter({'dogs': 27, 'cats': 7, 'pythons': 7})предварительно> кодовый блок>Вот и отлично! Теперь вы можете вести учет своих питомцев, используя
Counter. Обратите внимание, что вы можете использовать.subtract()и.update()для вычитания и сложения чисел или кратностей. Вы также можете использовать операторы сложения (+) и вычитания (-).С
Counterобъектами в виде мультинаборов в Python можно сделать гораздо больше, так что дерзайте и попробуйте!Объединение словарей в цепочку:
ChainMapВ Python
ChainMapсгруппировано несколько словарей и других сопоставлений, чтобы создать единый объект, который работает почти как обычный словарь. Другими словами, требуется несколько отображений, которые логически представляются как одно целое.
ChainMapобъекты являются обновляемыми представлениями, что означает, что изменения в любом из связанных отображений влияют наChainMapобъект в целом. Это связано с тем, чтоChainMapне объединяет входные сопоставления вместе. Он хранит список сопоставлений и переопределяет общие операции со словарем поверх этого списка. Например, поиск по ключу выполняет последовательный поиск по списку сопоставлений, пока не найдет нужный ключ.Примечание: Ознакомьтесь с Python's ChainMap: Эффективное управление несколькими контекстами для более глубокого изучения использования
ChainMapв вашем коде на Python.При работе с объектами
ChainMapу вас может быть несколько словарей с уникальными или повторяющимися ключами.В любом случае,
ChainMapпозволяет обрабатывать все ваши словари как один. Если у вас есть уникальные ключи для всех словарей, вы можете получать доступ к ключам и обновлять их, как если бы вы работали с одним словарем.Если в ваших словарях есть повторяющиеся ключи, вы можете не только управлять своими словарями как единым целым, но и воспользоваться внутренним списком сопоставлений, чтобы определить какой-то приоритет доступа. Благодаря этой функции
ChainMapобъекты отлично подходят для обработки нескольких контекстов.Например, предположим, что вы работаете с приложением с интерфейсом командной строки (CLI). Приложение позволяет пользователю использовать прокси-сервис для подключения к Интернету. Приоритетами настроек являются:
- Параметры командной строки (
--proxy,-p)- Файлы локальной конфигурации в домашнем каталоге пользователя
- Глобальная конфигурация прокси-сервера
Если пользователь указывает прокси-сервер в командной строке, приложение должно использовать этот прокси-сервер. В противном случае приложение должно использовать прокси-сервер, указанный в следующем объекте конфигурации, и так далее. Это один из наиболее распространенных вариантов использования
ChainMap. В этой ситуации вы можете сделать следующее:>>> from collections import ChainMap >>> cmd_proxy = {} # The user doesn't provide a proxy >>> local_proxy = {"proxy": "proxy.local.com"} >>> global_proxy = {"proxy": "proxy.global.com"} >>> config = ChainMap(cmd_proxy, local_proxy, global_proxy) >>> config["proxy"] 'proxy.local.com'предварительно> кодовый блок>
ChainMapпозволяет вам определить соответствующий приоритет для настройки прокси-сервера приложения. При поиске ключа выполняется поискcmd_proxy, затемlocal_proxyи, наконец,global_proxy, возвращая первый экземпляр ключа, который находится под рукой. В этом примере пользователь не предоставляет прокси-сервер в командной строке, поэтому ваше приложение использует прокси-сервер вlocal_proxy.В целом, объекты
ChainMapведут себя аналогично обычным объектамdict. Однако у них есть некоторые дополнительные возможности. Например, у них есть атрибут.mapspublic, который содержит внутренний список сопоставлений:>>> from collections import ChainMap >>> numbers = {"one": 1, "two": 2} >>> letters = {"a": "A", "b": "B"} >>> alpha_nums = ChainMap(numbers, letters) >>> alpha_nums.maps [{'one': 1, 'two': 2}, {'a': 'A', 'b': 'B'}]предварительно> кодовый блок>Атрибут экземпляра
.mapsпредоставляет вам доступ к внутреннему списку сопоставлений. Этот список можно обновлять. Вы можете добавлять и удалять сопоставления вручную, выполнять итерацию по списку и многое другое.Кроме того,
ChainMapпредоставляет.new_child()метод и.parentsсвойство:>>> from collections import ChainMap >>> dad = {"name": "John", "age": 35} >>> mom = {"name": "Jane", "age": 31} >>> family = ChainMap(mom, dad) >>> family ChainMap({'name': 'Jane', 'age': 31}, {'name': 'John', 'age': 35}) >>> son = {"name": "Mike", "age": 0} >>> family = family.new_child(son) >>> for person in family.maps: ... print(person) ... {'name': 'Mike', 'age': 0} {'name': 'Jane', 'age': 31} {'name': 'John', 'age': 35} >>> family.parents ChainMap({'name': 'Jane', 'age': 31}, {'name': 'John', 'age': 35})предварительно> кодовый блок>С помощью
.new_child()вы создаете новый объектChainMap, содержащий новую карту (son), за которой следуют все карты текущего экземпляра. Карта, переданная в качестве первого аргумента, становится первой картой в списке карт. Если вы не передаете карту, то метод использует пустой словарь.Свойство
parentsвозвращает новыеChainMapобъекты, содержащие все карты в текущем экземпляре, за исключением первой. Это полезно, когда вам нужно пропустить первую карту при поиске по ключу.Последняя особенность, на которую следует обратить внимание в
ChainMap, заключается в том, что изменяющие операции, такие как обновление ключей, добавление новых ключей, удаление существующих ключей, вставка ключей и очистка словаря, воздействуют на первое отображение во внутреннем списке отображений:>>> from collections import ChainMap >>> numbers = {"one": 1, "two": 2} >>> letters = {"a": "A", "b": "B"} >>> alpha_nums = ChainMap(numbers, letters) >>> alpha_nums ChainMap({'one': 1, 'two': 2}, {'a': 'A', 'b': 'B'}) >>> # Add a new key-value pair >>> alpha_nums["c"] = "C" >>> alpha_nums ChainMap({'one': 1, 'two': 2, 'c': 'C'}, {'a': 'A', 'b': 'B'}) >>> # Pop a key that exists in the first dictionary >>> alpha_nums.pop("two") 2 >>> alpha_nums ChainMap({'one': 1, 'c': 'C'}, {'a': 'A', 'b': 'B'}) >>> # Delete keys that don't exist in the first dict but do in others >>> del alpha_nums["a"] Traceback (most recent call last): ... KeyError: "Key not found in the first mapping: 'a'" >>> # Clear the dictionary >>> alpha_nums.clear() >>> alpha_nums ChainMap({}, {'a': 'A', 'b': 'B'})предварительно> кодовый блок>Эти примеры показывают, что операции изменения с объектом
ChainMapвлияют только на первое отображение во внутреннем списке. Это важная деталь, которую следует учитывать при работе сChainMap.Сложность заключается в том, что на первый взгляд может показаться, что можно изменить любую существующую пару ключ-значение в заданном
ChainMap. Однако вы можете изменять только пары ключ-значение в первом сопоставлении, если только вы не используете.mapsдля прямого доступа к другим сопоставлениям в списке и их изменения.Настройка встроенных модулей:
UserString,UserList, иUserDictИногда вам нужно настроить встроенные типы, такие как строки, списки и словари, чтобы добавить или изменить определенное поведение. Начиная с версии Python 2.2, вы можете сделать это, создав подклассы этих типов напрямую. Однако, как вы увидите через минуту, при таком подходе могут возникнуть некоторые проблемы.
Python
collectionsпредоставляет три удобных класса-оболочки, которые имитируют поведение встроенных типов данных:
UserStringUserListUserDictВ настоящее время разработчики часто задаются вопросом, есть ли смысл использовать
UserString,UserList, иUserDict, когда им нужно настроить поведение встроенных типов. Ответ - да.Встроенные типы были разработаны и внедрены с учетом принципа "открыто-закрыто". Это означает, что они открыты для расширения, но закрыты для модификации. Разрешение вносить изменения в основные функции этих классов потенциально может нарушить их инварианты. Поэтому разработчики ядра Python решили защитить их от изменений.
Например, предположим, что вам нужен словарь, который автоматически записывает ключи в нижний регистр при их вставке. Вы могли бы создать подкласс
dictи переопределить.__setitem__()таким образом, каждый раз, когда вы вставляете ключ, в словаре имя ключа указывается в нижнем регистре:>>> class LowerDict(dict): ... def __setitem__(self, key, value): ... key = key.lower() ... super().__setitem__(key, value) ... >>> ordinals = LowerDict({"FIRST": 1, "SECOND": 2}) >>> ordinals["THIRD"] = 3 >>> ordinals.update({"FOURTH": 4}) >>> ordinals {'FIRST': 1, 'SECOND': 2, 'third': 3, 'FOURTH': 4} >>> isinstance(ordinals, dict) Trueпредварительно> кодовый блок>Этот словарь работает корректно, когда вы вставляете новые ключи, используя назначение в стиле словаря с квадратными скобками (
[]). Однако это не работает, когда вы передаете начальный словарь конструктору класса или когда вы используете.update(). Это означает, что вам потребуется переопределить.__init__(),.update(), и, возможно, некоторые другие методы для корректной работы вашего пользовательского словаря.Теперь взглянем на тот же словарь, но используя
UserDictв качестве базового класса:>>> from collections import UserDict >>> class LowerDict(UserDict): ... def __setitem__(self, key, value): ... key = key.lower() ... super().__setitem__(key, value) ... >>> ordinals = LowerDict({"FIRST": 1, "SECOND": 2}) >>> ordinals["THIRD"] = 3 >>> ordinals.update({"FOURTH": 4}) >>> ordinals {'first': 1, 'second': 2, 'third': 3, 'fourth': 4} >>> isinstance(ordinals, dict) Falseпредварительно> кодовый блок>Это работает! Теперь ваш пользовательский словарь преобразует все новые ключи в строчные буквы перед их вставкой в словарь. Обратите внимание, что, поскольку вы не наследуете от
dictнапрямую, ваш класс не возвращает экземплярыdict, как в примере выше.
UserDictхранит обычный словарь в атрибуте экземпляра с именем.data. Затем он реализует все свои методы на основе этого словаря.UserListиUserStringработают одинаково, но их атрибут.dataсодержит объектыlistиstr, соответственно.Если вам нужно настроить любой из этих классов, то вам просто нужно переопределить соответствующие методы и изменить то, что они делают, по мере необходимости.
В общем, вам следует использовать
UserDict,UserList, иUserString, когда вам нужен класс, который действует почти идентично базовому встроенному классу в оболочке, и вы хотите настроить некоторую часть его стандартных функциональных возможностей.Еще одной причиной использования этих классов, а не встроенных эквивалентных классов, является доступ к базовому атрибуту
.dataдля непосредственного управления им.Возможность прямого наследования от встроенных типов в значительной степени вытеснила использование
UserDict,UserList, иUserString. Однако внутренняя реализация встроенных типов затрудняет безопасное наследование от них без переписывания значительного объема кода. В большинстве случаев безопаснее использовать соответствующий класс изcollections. Это избавит вас от ряда проблем и странного поведения.Заключение
В модуле Python
collectionsу вас есть несколько специализированных контейнерных типов данных, которые вы можете использовать для решения общих задач программирования, таких как подсчет объектов, создание очередей и стеков, обработка пропущенных ключей в словарях и многое другое.Типы данных и классы в
collectionsбыли разработаны таким образом, чтобы они были эффективными и основывались на языке Python. Они могут быть чрезвычайно полезны в процессе программирования на Python, поэтому изучение их стоит потраченного времени и усилий.В этом руководстве вы узнали, как:
- Написать читаемый и явный код, используя
namedtuple- Создавать эффективные очереди и стеки, используя
deque- Подсчитывайте объекты эффективно, используя
Counter- Обработать недостающие ключи словаря с помощью
defaultdict- Запомните порядок ввода клавиш с
OrderedDict- Объедините несколько словарей в одном представлении с помощью
ChainMapВы также узнали о трех удобных классах-оболочках:
<статус завершения article-slug="python-collections-module" class="btn-group mb-0" data-api-article-bookmark-url="/api/v1/articles/python-collections-module/bookmark/" data-api-article-завершение-статус-url="/api/версия 1/статьи/python-коллекции-модуль/завершение_статуса/"> статус завершения> <кнопка поделиться bluesky-text="Интересная статья на #Python от @realpython.com :" email-body="Ознакомьтесь с этой статьей о Python: коллекции%0A%0APython: Множество специализированных типов данных" email-subject="Статья о Python для вас" twitter-text="Интересная #Статья о Python от @realpython:" url="https://realpython.com/python-collections-module /" url-title="Коллекции Python: набор специализированных типов данных"> кнопка поделиться> Back to TopUserDict,UserList, иUserString. Эти классы удобны, когда вам нужно создать пользовательские классы, которые имитируют поведение встроенных типовdict,list, иstr.