Поиск по сайту:

Сопоставления Python: подробное руководство


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

Из этого урока вы узнаете:

  • Основные характеристики сопоставления
  • Операции, общие для большинства сопоставлений
  • Абстрактные базовые классы Mapping и MutableMapping
  • Определяемые пользователем изменяемые и неизменяемые сопоставления и способы их создания

В этом руководстве предполагается, что вы знакомы со встроенными типами данных Python, особенно со словарями, а также с основами объектно-ориентированного программирования.

Понимание основных характеристик сопоставлений Python

Сопоставление — это коллекция, которая позволяет вам искать ключ и получать его значение. Ключами в сопоставлениях могут быть объекты самых разных типов. Однако в большинстве сопоставлений существуют типы объектов, которые нельзя использовать в качестве ключей, как вы узнаете позже в этом руководстве.

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

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

>>> points = {
...     "Denise": 3,
...     "Igor": 2,
...     "Sarah": 3,
...     "Trevor": 1,
... }
>>> points["Sarah"]
3
>>> points["Matt"]
Traceback (most recent call last):
  ...
KeyError: 'Matt'

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

Вы можете использовать одно из сопоставлений в модуле collections стандартной библиотеки, чтобы назначить значение по умолчанию для ключей, которых нет в коллекции. Тип defaultdict включает вызываемый объект, который вызывается каждый раз, когда вы пытаетесь получить доступ к несуществующему ключу. Если вы хотите, чтобы значение по умолчанию было равно нулю, вы можете использовать функцию lambda, которая возвращает 0 в качестве первого аргумента в defaultdict:

>>> from collections import defaultdict
>>> points_default = defaultdict(
...     lambda: 0,
...     points,
... )

>>> points_default
defaultdict(<function <lambda> at 0x104a95da0>, {'Denise': 3,
    'Igor': 2, 'Sarah': 3, 'Trevor': 1})
>>> points_default["Sarah"]
3
>>> points_default["Matt"]
0
>>> points_default
defaultdict(<function <lambda> at 0x103e6c700>, {'Denise': 3,
    'Igor': 2, 'Sarah': 3, 'Trevor': 1, 'Matt': 0})

В этом примере конструктор defaultdict имеет два аргумента. Первый аргумент — это вызываемый объект, который используется, когда требуется значение по умолчанию. Второй аргумент — это словарь, который вы создали ранее. Вы можете использовать любой допустимый аргумент при вызове dict() в качестве второго аргумента в defaultdict() или опустить этот аргумент, чтобы создать пустой defaultdict .

Когда вы получаете доступ к ключу, которого нет в словаре, ключ добавляется, и ему присваивается значение по умолчанию. Вы также можете создать тот же объект points_default, используя вызываемый int в качестве первого аргумента, поскольку вызов int() без аргументов возвращает 0.

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

>>> from collections import Counter
>>> letters = Counter("learning python")
>>> letters
Counter({'n': 3, 'l': 1, 'e': 1, 'a': 1, 'r': 1, 'i': 1, 'g': 1,
    ' ': 1, 'p': 1, 'y': 1, 't': 1, 'h': 1, 'o': 1})

Буквы в строке "learning python" преобразуются в ключи в Counter, а количество вхождений каждой буквы используется как значение, соответствующее каждому ключу.

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

>>> for letter in letters:
...     print(letter)
...
l
e
a
r
n
i
g

p
y
t
h
o

>>> len(letters)
13

>>> "n" in letters
True
>>> "x" in letters
False

Вы можете использовать объект Counter letters в цикле for, что подтверждает его итерируемость. Все отображения являются итеративными. Однако итерация проходит по ключам, а не по значениям. Далее в этом руководстве вы увидите, как перебирать значения или одновременно ключи и значения.

Встроенная функция len() возвращает количество элементов в сопоставлении. Оно равно количеству уникальных символов в исходной строке, включая символ пробела. Размер объекта определяется, поскольку len() возвращает значение.

Вы можете использовать ключевое слово in, чтобы проверить, какие элементы находятся в сопоставлении. Одной этой проверки недостаточно, чтобы подтвердить, что отображение является контейнером. Однако вы также можете напрямую получить доступ к специальному методу объекта .__contains__():

>>> letters.__contains__("n")
True

Как видите, наличие этого специального метода подтверждает, что letters является контейнером.

Специальный метод .__getitem__() в сопоставлениях

Характеристики, о которых вы узнали в первом разделе, определяются с помощью специальных методов в определениях классов. Таким образом, отображения имеют специальный метод .__iter__(), делающий их итеративными, специальный метод .__contains__(), позволяющий определить их как контейнеры, и .__len__.() специальный метод, позволяющий задать им размер.

Сопоставления также имеют специальный метод .__getitem__(), который делает их доступными для подписки. Объект является индексируемым, если после него можно добавить квадратные скобки, например my_object[item]. При сопоставлении значение, которое вы используете в квадратных скобках, является ключом в паре ключ-значение и используется для получения значения, соответствующего ключу.

Специальный метод .__getitem__() предоставляет интерфейс для записи в квадратных скобках. В словаре Python и других сопоставлениях этот поиск данных реализован с помощью хэш-таблицы, что делает доступ к данным эффективным. Вы можете узнать больше о хеш-таблицах и о том, как они реализованы в словарях Python, в статье «Создание хеш-таблицы в Python с помощью TDD».

Если вы создаете собственное сопоставление, вам необходимо реализовать специальный метод .__getitem__() для получения значений из ключей. В большинстве случаев лучшим вариантом является использование словаря Python или других сопоставлений, реализованных в стандартной библиотеке Python, чтобы использовать эффективный доступ к данным, уже реализованный в этих структурах данных.

Вы изучите эти идеи при создании пользовательского сопоставления далее в этом руководстве.

Ключи, значения и элементы в сопоставлениях

Вернитесь к одному из сопоставлений, которые вы использовали ранее в этом руководстве, словарю points:

>>> points = {
...     "Denise": 3,
...     "Igor": 2,
...     "Sarah": 3,
...     "Trevor": 1,
... }

Словарь состоит из четырех ключей, связанных с четырьмя значениями. Каждое отображение характеризуется этими парами ключ-значение. Каждая пара ключ-значение представляет собой один элемент. Таким образом, в этом словаре четыре элемента. Вы можете подтвердить это, используя len(points), который возвращает целое число 4.

Сопоставления Python имеют три метода: .keys(), .values() и .items(). Вы можете начать с изучения первых двух из них:

>>> points.keys()
dict_keys(['Denise', 'Igor', 'Sarah', 'Trevor'])

>>> points.values()
dict_values([3, 2, 3, 1])

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

>>> points.items()
dict_items([('Denise', 3), ('Igor', 2), ('Sarah', 3), ('Trevor', 1)])

Объект, возвращаемый функцией .items(), полезен, когда вам нужно получить доступ к паре ключ-значение как к итерируемому объекту, например, если вам нужно пройти через сопоставление и получить доступ как к ключу, так и к значению для каждого элемент:

>>> for name, number in points.items():
...     print(f"Number of points for {name}: {number}")
...
Number of points for Denise: 3
Number of points for Igor: 2
Number of points for Sarah: 3
Number of points for Trevor: 1

Вы также можете подтвердить, что другие сопоставления имеют эти методы:

>>> from collections import Counter
>>> letters = Counter("learning python")
>>> letters
Counter({'n': 3, 'l': 1, 'e': 1, 'a': 1, 'r': 1, 'i': 1, 'g': 1,
    ' ': 1, 'p': 1, 'y': 1, 't': 1, 'h': 1, 'o': 1})

>>> letters.keys()
dict_keys(['l', 'e', 'a', 'r', 'n', 'i', 'g', ' ', 'p', 'y', 't',
    'h', 'o'])

>>> letters.values()
dict_values([1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1])

>>> letters.items()
dict_items([('l', 1), ('e', 1), ('a', 1), ('r', 1), ('n', 3),
    ('i', 1), ('g', 1), (' ', 1), ('p', 1), ('y', 1), ('t', 1),
    ('h', 1), ('o', 1)])

Эти методы не возвращают список или кортеж. Вместо этого они возвращают объекты dict_keys, dict_values или dict_items. Даже методы, вызываемые для объекта Counter, возвращают одни и те же три типа данных, поскольку многие сопоставления основаны на реализации dict.

Объекты dict_keys, dict_values и dict_items представляют собой словарные представления. Эти объекты не содержат собственных данных, но предоставляют представление данных, хранящихся в сопоставлении. Чтобы поэкспериментировать с этой идеей, вы можете назначить одно из представлений переменной, а затем изменить данные в исходном сопоставлении:

>>> values_in_points = points.values()
>>> values_in_points
dict_values([3, 2, 3, 1])

>>> points["Igor"] += 10

>>> values_in_points
dict_values([3, 12, 3, 1])

Вы назначаете объект dict_values, возвращаемый функцией .values(), объекту values_in_points. Когда вы обновляете словарь, значения в values_in_points также изменяются.

Различие между ключами, значениями и элементами важно при работе с сопоставлениями. Вы вернетесь к методам доступа к ним позже в этом руководстве, когда будете создавать собственное сопоставление.

Сравнение отображений, последовательностей и наборов

Ранее в этом руководстве вы узнали, что сопоставление — это коллекция, в которой вы можете получить доступ к значению, используя связанный с ним ключ. Сопоставления — не единственная коллекция в Python. Последовательности и множества также являются коллекциями. Общие последовательности включают списки, кортежи и строки.

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

Отображения и последовательности подлежат подписке. Вы можете использовать квадратные скобки для доступа к значениям из сопоставлений и последовательностей. Эта характеристика определяется специальным методом .__getitem__(). С другой стороны, наборы не могут быть подписаны:

>>> # Mapping
>>> points = {
...     "Denise": 3,
...     "Igor": 2,
...     "Sarah": 3,
...     "Trevor": 1,
... }
>>> points["Igor"]
2

>>> # Sequence
>>> numbers = [4, 10, 34]
>>> numbers[1]
10

>>> # Set
>>> numbers_set = {4, 10, 34}
>>> numbers_set[1]
Traceback (most recent call last):
  ...
TypeError: 'set' object is not subscriptable

>>> numbers_set.__getitem__
Traceback (most recent call last):
  ...
AttributeError: 'set' object has no attribute '__getitem__'.
    Did you mean: '__getstate__'?

Вы можете использовать обозначение квадратных скобок для доступа к значениям из словаря и списка. То же самое относится ко всем отображениям и последовательностям. Но поскольку у наборов нет специального метода .__getitem__(), они не могут быть индексированы.

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

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

Однако для большинства сопоставлений нельзя использовать изменяемые объекты или неизменяемые структуры, содержащие изменяемые объекты. Это требование налагается хеш-таблицей, используемой для реализации словарей и других сопоставлений:

>>> {[0, 0]: None, [1, 1]: None}
Traceback (most recent call last):
  ...
TypeError: unhashable type: 'list'

>>> from collections import Counter
>>> number_groups = ([1, 2, 3], [1, 2, 3], [2, 3, 4])
>>> Counter(number_groups)
Traceback (most recent call last):
  ...
TypeError: unhashable type: 'list'

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

Отображения не являются упорядоченной структурой данных. Однако элементы в словарях сохраняют порядок, в котором они были добавлены. Эта функция присутствует начиная с Python 3.6 и была добавлена в формальное описание языка в Python 3.7. Несмотря на то, что порядок элементов словаря сохраняется, словари не являются упорядоченной структурой, подобной последовательностям. Вот демонстрация этой разницы:

>>> [1, 2] == [2, 1]
False
>>> {"one": 1, "two": 2} == {"two": 2, "one": 1}
True

Эти два списка не равны, поскольку значения находятся в разных позициях. Однако эти два словаря равны, поскольку имеют одинаковые пары ключ-значение, хотя порядок их включения различен.

В большинстве сопоставлений, основанных на словаре Python, ключи должны быть уникальными. Это еще одно требование к хэш-таблице, используемой для реализации словарей и других сопоставлений. Однако элементы в последовательностях не обязательно должны быть уникальными. Многие последовательности имеют повторяющиеся значения.

Требование использовать уникальные хешируемые объекты в качестве ключей в сопоставлениях Python связано с реализацией словарей. Это не обязательное требование для отображений. Однако большинство сопоставлений построены на основе словаря Python и, следовательно, предъявляют одни и те же требования.

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

Изучение абстрактных базовых классов Mapping и MutableMapping

Python имеет абстрактные базовые классы, которые определяют интерфейсы для категорий типов данных, таких как сопоставления. В этом разделе вы узнаете об абстрактных базовых классах Mapping и MutableMapping, которые вы найдете в модуле collections.abc.

Эти классы можно использовать для проверки того, что объект является экземпляром сопоставления:

>>> from collections import Counter
>>> from collections.abc import Mapping, MutableMapping
>>> points = {
...     "Denise": 3,
...     "Igor": 2,
...     "Sarah": 3,
...     "Trevor": 1,
... }

>>> isinstance(points, Mapping)
True
>>> isinstance(points, MutableMapping)
True

>>> letters = Counter("learning python")
>>> isinstance(letters, MutableMapping)
True

Словарь points представляет собой Mapping и MutableMapping. Все объекты MutableMapping также являются объектами Mapping. Объект Counter также возвращает True, когда вы проверяете, является ли это MutableMapping. Вместо этого вы можете проверить, является ли points объектом dict, а letters — объектом Counter.

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

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

Характеристики абстрактного базового класса Mapping

Абстрактный базовый класс Mapping определяет интерфейс для всех сопоставлений, предоставляя несколько методов и гарантируя включение необходимых специальных методов.

обязательные специальные методы, которые вам необходимо определить при создании сопоставления, следующие:

  • .__getitem__(): определяет способ доступа к значениям с использованием квадратных скобок.
  • .__iter__(): определяет способ перебора сопоставления.
  • .__len__(): определяет размер сопоставления.

Абстрактный базовый класс Mapping также предоставляет следующие методы:

  • .__contains__: определяет, как определить членство в сопоставлении.
  • .__eq__(): определяет, как определить равенство двух объектов.
  • .__ne__(): определяет, как определить, что два объекта не равны.
  • .keys(): определяет способ доступа к клавишам сопоставления.
  • .values(): определяет способ доступа к значениям в сопоставлении.
  • .items(): определяет способ доступа к парам ключ-значение в сопоставлении.
  • .get(): определяет альтернативный способ доступа к значениям с помощью ключей. Этот метод позволяет вам установить значение по умолчанию, которое будет использоваться, если ключ отсутствует в сопоставлении.

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

Характеристики абстрактного базового класса MutableMapping

Абстрактный базовый класс Mapping не включает никаких методов, необходимых для внесения изменений в сопоставление. Он создает неизменяемое сопоставление. Однако существует второй абстрактный базовый класс под названием MutableMapping для создания mutable версии.

MutableMapping наследуется от Mapping. Таким образом, он включает в себя все методы, присутствующие в Mapping, но имеет два дополнительных обязательных специальных метода:

  • .__setitem__(): определяет, как установить новое значение для ключа.
  • .__delitem__(): определяет, как удалить элемент в сопоставлении.

Абстрактный базовый класс MutableMapping также добавляет следующие методы:

  • .pop(): определяет, как удалить ключ из сопоставления и вернуть его значение.
  • .popitem(): определяет, как удалить и вернуть последний добавленный элемент в сопоставлении.
  • .clear(): определяет, как удалить все элементы из сопоставления.
  • .update(): определяет, как обновить словарь, используя данные, переданные в качестве аргумента этому методу.
  • .setdefault(): определяет, как добавить ключ со значением по умолчанию, если ключ еще не указан в сопоставлении.

Возможно, вы знакомы со многими из этих методов по использованию структуры данных Python dict. Вы найдете эти методы во всех изменяемых сопоставлениях. Помимо этого набора сопоставления могут иметь и другие методы. Например, объект Counter имеет метод .most_common(), который возвращает наиболее распространенный ключ.

В оставшейся части этого руководства вы будете использовать Mapping и MutableMapping для создания собственного сопоставления и использовать многие из этих методов.

Создание пользовательского сопоставления

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

Владелец пиццерии решил включить в список только пиццы, названия которых начинаются с разных букв. Вы создадите сопоставление, чтобы ключи не начинались с одной и той же буквы. Вы также должны иметь возможность получить доступ к значению, связанному с каждым ключом, используя полный ключ или только первую букву. Следовательно, menu["Margherita"] и menu["m"] вернут одно и то же значение. Значение, связанное с каждым ключом, — это цена пиццы.

В этом разделе вы создадите класс, который наследуется от абстрактного базового класса Mapping. Это даст вам хорошее представление о том, как работают сопоставления. В последующих разделах вы поработаете над другими способами создания того же класса.

Вы можете начать с создания нового класса, который наследуется от Mapping и принимает словарь в качестве единственного аргумента:

from collections.abc import Mapping

class PizzaMenu(Mapping):
    def __init__(self, menu: dict):
        self._menu = menu

Класс PizzaMenu наследует от Mapping, а его специальный метод .__init__() принимает словарь, который присваивается классу ._menu атрибут данных. Начальное подчеркивание в ._menu указывает, что к этому атрибуту нельзя получить доступ за пределами класса.

Вы можете протестировать этот класс в сеансе REPL:

>>> from pizza_menu import PizzaMenu
>>> menu = PizzaMenu({"Margherita": 9.5, "Pepperoni": 10.5})
Traceback (most recent call last):
  ...
TypeError: Can't instantiate abstract class PizzaMenu without
    an implementation for abstract methods '__getitem__',
    '__iter__', '__len__'

Вы пытаетесь создать экземпляр PizzaMenu, используя словарь, содержащий два элемента. Но код вызывает TypeError. Поскольку PizzaMenu наследуется от абстрактного базового класса Mapping, он должен иметь три обязательных специальных метода: .__getitem__(), .__iter__( ) и .__len__().

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

from collections.abc import Mapping

class PizzaMenu(Mapping):
    def __init__(self, menu: dict):
        self._menu = menu

    def __getitem__(self, key):
        return self._menu[key]

    def __iter__(self):
        return iter(self._menu)

    def __len__(self):
        return len(self._menu)

Вы определяете .__getitem__(), чтобы при доступе к значению ключа в PizzaMenu объект возвращал значение, соответствующее этому ключу в ._menu словарь. Определение .__iter__() гарантирует, что итерация по объекту PizzaMenu эквивалентна итерации по словарю ._menu и . __len__() определяет размер объекта PizzaMenu как размер словаря ._menu.

Вы можете протестировать класс в новом сеансе REPL:

>>> from pizza_menu import PizzaMenu
>>> menu = PizzaMenu({"Margherita": 9.5, "Pepperoni": 10.5})
>>> menu
<pizza_menu.PizzaMenu object at 0x102fb6b10>

Это работает сейчас. Вы создали экземпляр PizzaMenu. Однако вывод при отображении объекта бесполезен. Хорошей практикой является определение специального метода .__repr__() для определяемого пользователем класса, который обеспечивает удобное для программиста строковое представление объекта. Вы также можете определить специальный метод .__str__(), чтобы обеспечить удобное строковое представление:

from collections.abc import Mapping

class PizzaMenu(Mapping):
    # ...

    def __repr__(self):
        return f"{self.__class__.__name__}({self._menu})"

    def __str__(self):
        return str(self._menu)

Специальный метод .__repr__() создает выходные данные, которые можно использовать для повторного создания объекта. Вы можете использовать имя класса непосредственно в строке, но вместо этого вы используете self.__class__.__name__ для динамического получения имени класса. Эта версия гарантирует, что метод .__repr__() также работает должным образом для подклассов PizzaMenu.

Вы можете подтвердить выходные данные этих методов в новом сеансе REPL:

>>> from pizza_menu import PizzaMenu
>>> menu = PizzaMenu({"Margherita": 9.5, "Pepperoni": 10.5})
>>> menu
PizzaMenu({'Margherita': 9.5, 'Pepperoni': 10.5})
>>> print(menu)
{'Margherita': 9.5, 'Pepperoni': 10.5}

Вы создаете экземпляр класса из словаря и отображаете объект. Нажмите ниже, чтобы увидеть альтернативный специальный метод .__init__() для PizzaMenu.

Метод .__init__(), созданный вами для PizzaMenu, принимает словарь в качестве аргумента. Таким образом, вы можете создать PizzaMenu только из другого словаря. Вы можете изменить метод .__init__(), чтобы он принимал те же типы аргументов, которые вы можете использовать для создания экземпляра словаря при использовании dict().

Существует четыре типа аргументов, которые вы можете использовать при создании словаря с помощью dict():

  1. Нет аргументов. Вы создаете пустой словарь при вызове dict() без аргументов.
  2. Сопоставление. Вы можете использовать любое сопоставление в качестве аргумента в dict(), который создает новый словарь на основе сопоставления.
  3. Итерируемый. Вы можете использовать итерируемый объект, который имеет пары объектов в качестве аргумента в dict(). Первый элемент в каждой паре становится ключом, а второй элемент — его значением в новом словаре.
  4. **kwargs: Вы можете использовать любое количество аргументов ключевых слов при вызове dict(). Ключевые слова становятся ключами словаря, а значения аргументов становятся значениями словаря.

Вы можете воспроизвести этот гибкий подход непосредственно в PizzaMenu:

from collections.abc import Mapping

class PizzaMenu(Mapping):
    def __init__(self, menu=None, /, **kwargs):
        self._menu = dict(menu or {}, **kwargs)

    # ...

Эта версия позволяет создавать объект PizzaMenu различными способами. Если присутствуют аргументы ключевого слова, вы вызываете dict() либо с menu, либо с пустым словарем в качестве первого аргумента. Ключевое слово or использует короткую оценку, так что menu используется, если оно истинно, и пустой словарь, если menu ложно. Если аргументы ключевого слова отсутствуют, вы вызываете dict() либо с первым аргументом, либо с пустым словарем, если menu отсутствует:

>>> from pizza_menu import PizzaMenu
>>> menu = PizzaMenu({"Margherita": 9.5, "Pepperoni": 10.5})
>>> menu
PizzaMenu({'Margherita': 9.5, 'Pepperoni': 10.5})

>>> menu = PizzaMenu(Margherita=9.5, Pepperoni=10.5)
>>> menu
PizzaMenu({'Margherita': 9.5, 'Pepperoni': 10.5})

>>> menu = PizzaMenu([("Margherita", 9.5), ("Pepperoni", 10.5)])
>>> menu
PizzaMenu({'Margherita': 9.5, 'Pepperoni': 10.5})

>>> menu = PizzaMenu()
>>> menu
PizzaMenu({})

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

В оставшейся части этого руководства вы будете использовать более простой специальный метод .__init__(), который позволит вам сосредоточиться на других аспектах сопоставления.

Ваш следующий шаг — настроить этот класс так, чтобы он соответствовал требованию, чтобы ни одна клавиша не начиналась с одной и той же буквы.

Не допускайте, чтобы названия пиццы начинались с одной и той же буквы

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

from collections.abc import Mapping

class PizzaMenu(Mapping):
    def __init__(self, menu: dict):
        self._menu = {}
        first_letters = set()
        for key, value in menu.items():
            first_letter = key[0].lower()
            if first_letter in first_letters:
                raise ValueError(
                    f"'{key}' is invalid."
                    " All pizzas must have unique first letters"
                )
            first_letters.add(first_letter)
            self._menu[key] = value

    # ...

Вы создаете набор под названием first_letters внутри .__init__(). Просматривая словарь, вы преобразуете первую букву в нижний регистр и проверяете, есть ли эта буква уже в наборе first_letters. Поскольку ни одна первая буква не может повториться, код в цикле выдает ошибку, если обнаруживает повторяющуюся букву.

Если код не выдает ошибку, вы добавляете первую букву в набор, чтобы гарантировать отсутствие недопустимых имен на последующих итерациях. Вы также добавляете значение в словарь ._menu, который инициализируете как пустой словарь в начале метода .__init__().

Вы можете проверить это поведение в новом сеансе REPL. Вы создаете словарь предлагаемых имен для использования в качестве аргумента для PizzaMenu():

>>> from pizza_menu import PizzaMenu
>>> proposed_pizzas = {
...     "Margherita": 9.50,
...     "Pepperoni": 10.50,
...     "Hawaiian": 11.50,
...     "Meat Feast": 12.50,
...     "Capricciosa": 12.50,
...     "Napoletana": 11.50,
...     "Pizza Bianca": 10.50,
... }

>>> menu = PizzaMenu(proposed_pizzas)
Traceback (most recent call last):
  ...
ValueError: 'Meat Feast' is invalid. All pizzas must have
    unique first letters

Имена в propose_pizzas содержат недопустимые записи. Есть две пиццы, названия которых начинаются с M, и две, начинающиеся с P. Вы можете переименовать пиццы и попробовать еще раз:

>>> proposed_pizzas = {
...     "Margherita": 9.50,
...     "Pepperoni": 10.50,
...     "Hawaiian": 11.50,
...     "Feast of Meat": 12.50,
...     "Capricciosa": 12.50,
...     "Napoletana": 11.50,
...     "Bianca": 10.50,
... }

>>> menu = PizzaMenu(proposed_pizzas)
>>> menu
PizzaMenu({'Margherita': 9.5, 'Pepperoni': 10.5, 'Hawaiian': 11.5,
    'Feast of Meat': 12.5, 'Capricciosa': 12.5, 'Napoletana': 11.5,
    'Bianca': 10.5})

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

Если вас обидело включение гавайской пиццы, читайте дальше. Если вас устраивает ананас в пицце, можете пропустить этот раздел!

Вы можете гарантировать, что гавайская пицца не появится в вашем меню, если добавить к .__init__():

from collections.abc import Mapping

class PizzaMenu(Mapping):
    def __init__(self, menu: dict):
        self._menu = {}
        first_letters = set()
        for key, value in menu.items():
            if key.lower() in ("hawaiian", "pineapple"):
                raise ValueError(
                    "What?! Hawaiian pizza is not allowed"
                )
            first_letter = key[0].lower()
            if first_letter in first_letters:
                raise ValueError(
                    f"'{key}' is invalid."
                    " All pizzas must have unique first letters"
                )
            first_letters.add(first_letter)
            self._menu[key] = value

    # ...

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

>>> from pizza_menu import PizzaMenu
>>> proposed_pizzas = {
...     "Margherita": 9.50,
...     "Pepperoni": 10.50,
...     "Hawaiian": 11.50,
...     "Feast of Meat": 12.50,
...     "Capricciosa": 12.50,
...     "Napoletana": 11.50,
...     "Bianca": 10.50,
... }

>>> menu = PizzaMenu(proposed_pizzas)
Traceback (most recent call last):
  ...
ValueError: What?! Hawaiian pizza is not allowed

Гавайская пицца теперь запрещена.

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

Добавьте альтернативный способ доступа к значениям в PizzaMenu

Поскольку все пиццы имеют уникальные первые буквы, вы можете изменить класс, чтобы использовать первую букву для доступа к значению из PizzaMenu. Например, предположим, что вы хотите иметь возможность использовать menu["Margherita"] или menu["m"] для доступа к цене пиццы "Маргарита".

Вы можете добавить каждую первую букву в качестве ключа в ._menu и присвоить ей то же значение, что и ключу с полным названием пиццы. Однако это дублирует данные. Вам также нужно быть осторожным при изменении цены на пиццу, чтобы убедиться, что вы изменили значение, связанное с однобуквенным ключом.

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

from collections.abc import Mapping

class PizzaMenu(Mapping):
    def __init__(self, menu: dict):
        self._menu = {}
        self._first_letters = {}
        for key, value in menu.items():
            first_letter = key[0].lower()
            if first_letter in self._first_letters:
                raise ValueError(
                    f"'{key}' is invalid."
                    " All pizzas must have unique first letters"
                )
            self._first_letters[first_letter] = key
            self._menu[key] = value

    # ...

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

Вам также необходимо изменить .__getitem__(), чтобы разрешить использование одной буквы в квадратных скобках при доступе к значению из объекта PizzaMenu:

from collections.abc import Mapping

class PizzaMenu(Mapping):
    # ...

    def __getitem__(self, key):
        if key not in self._menu and len(key) > 1:
            raise KeyError(key)
        key = self._first_letters.get(key[0].lower(), key)
        return self._menu[key]

    # ...

Специальный метод .__getitem__() теперь может принимать однобуквенный аргумент. Если аргумент, присвоенный параметру key, отсутствует в ._menu и не является одним символом, вы вызываете исключение, поскольку ключ недействителен.

Затем вы вызываете .get() для self._first_letters, который является словарем. В этом вызове вы включаете параметр key в качестве значения по умолчанию. Если key представляет собой одну букву, присутствующую в ._first_letters, .get() возвращает ее значение в этом словаре. Это значение переназначается key. Однако если аргумент .__getitem__() не является элементом ._first_letters, параметр key не изменяется, поскольку это значение по умолчанию. в .get().

Вы можете подтвердить это изменение в новом сеансе REPL:

>>> from pizza_menu import PizzaMenu
>>> proposed_pizzas = {
...     "Margherita": 9.50,
...     "Pepperoni": 10.50,
...     "Hawaiian": 11.50,
...     "Feast of Meat": 12.50,
...     "Capricciosa": 12.50,
...     "Napoletana": 11.50,
...     "Bianca": 10.50,
... }

>>> menu = PizzaMenu(proposed_pizzas)

>>> menu["Margherita"]
9.5
>>> menu["m"]
9.5

Класс стал более гибким. Вы можете узнать цену пиццы, указав ее полное название или только первую букву.

В следующем разделе вы изучите другие методы, которые вы ожидаете использовать в сопоставлении.

Переопределить другие методы, необходимые для PizzaMenu

Ранее в этом руководстве вы узнали о характеристиках, общих для всех сопоставлений. Поскольку PizzaMenu является подклассом Mapping, он наследует методы, которые есть у всех сопоставлений.

Вы можете проверить, что объект PizzaMenu ведет себя должным образом при выполнении распространенных операций:

>>> from pizza_menu import PizzaMenu
>>> proposed_pizzas = {
...     "Margherita": 9.50,
...     "Pepperoni": 10.50,
...     "Hawaiian": 11.50,
...     "Feast of Meat": 12.50,
...     "Capricciosa": 12.50,
...     "Napoletana": 11.50,
...     "Bianca": 10.50,
... }

>>> menu = PizzaMenu(proposed_pizzas)
>>> another_menu = PizzaMenu(proposed_pizzas)

>>> menu is another_menu
False

>>> menu == another_menu
True

>>> for item in menu:
...     print(item)
...
Margherita
Pepperoni
Hawaiian
Feast of Meat
Capricciosa
Napoletana
Bianca

>>> "Margherita" in menu
True

>>> "m" in menu
True

Вы создаете два объекта PizzaMenu из одного словаря. Объекты разные, поэтому ключевое слово is возвращает False при сравнении двух объектов. Однако оператор равенства == возвращает True. Итак, объекты равны, если все элементы в ._menu равны. Поскольку вы не определили .__eq__(), Python использует .__iter__() для перебора обоих объектов и сравнения их значений.

В сеансе REPL вы также подтверждаете, что итерация по PizzaMenu выполняется по клавишам, как и в случае с другими сопоставлениями.

Наконец, вы подтверждаете, что можете проверить, является ли название пиццы членом объекта PizzaMenu, используя ключевое слово in. Поскольку вы не определили .__contains__(), Python использует специальный метод .__getitem__() для поиска названия пиццы.

Однако это также показывает, что буква m является членом меню, поскольку вы изменили .__getitem__(), чтобы гарантировать возможность использования одной буквы в квадратных скобках. Если вы предпочитаете не включать отдельные буквы в качестве членов объекта PizzaMenu, вы можете определить специальный метод .__contains__():

from collections.abc import Mapping

class PizzaMenu(Mapping):
    # ...

    def __contains__(self, key):
        return key in self._menu

Когда присутствует метод .__contains__(), Python использует его для проверки членства. Это обходит .__getitem__() и проверяет, является ли ключ членом словаря, хранящегося в ._menu. Вы можете подтвердить, что отдельные буквы больше не считаются членами объекта в новом сеансе REPL:

>>> from pizza_menu import PizzaMenu
>>> proposed_pizzas = {
...     "Margherita": 9.50,
...     "Pepperoni": 10.50,
...     "Hawaiian": 11.50,
...     "Feast of Meat": 12.50,
...     "Capricciosa": 12.50,
...     "Napoletana": 11.50,
...     "Bianca": 10.50,
... }

>>> menu = PizzaMenu(proposed_pizzas)

>>> "Margherita" in menu
True

>>> "m" in menu
False

"Маргарита" по-прежнему является членом объекта PizzaMenu, но "m" больше не является его членом.

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

>>> menu.keys()
KeysView(PizzaMenu({'Margherita': 9.5, 'Pepperoni': 10.5,
    'Hawaiian': 11.5, 'Feast of Meat': 12.5, 'Capricciosa': 12.5,
    'Napoletana': 11.5, 'Bianca': 10.5}))

>>> menu.values()
ValuesView(PizzaMenu({'Margherita': 9.5, 'Pepperoni': 10.5,
    'Hawaiian': 11.5, 'Feast of Meat': 12.5, 'Capricciosa': 12.5,
    'Napoletana': 11.5, 'Bianca': 10.5}))

>>> menu.items()
ItemsView(PizzaMenu({'Margherita': 9.5, 'Pepperoni': 10.5,
    'Hawaiian': 11.5, 'Feast of Meat': 12.5, 'Capricciosa': 12.5,
    'Napoletana': 11.5, 'Bianca': 10.5}))

Методы .keys(), .values() и .items() существуют, поскольку они унаследованы от абстрактного базового класса, но они не отображают ожидаемые значения. Вместо этого они показывают весь объект, который представляет собой строковое представление, возвращаемое функцией .__repr__().

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

>>> for key in menu.keys():
...    print(key)
...
Margherita
Pepperoni
Hawaiian
Feast of Meat
Capricciosa
Napoletana
Bianca

>>> for value in menu.values():
...     print(value)
...
9.5
10.5
11.5
12.5
12.5
11.5
10.5

>>> for item in menu.items():
...     print(item)
...
('Margherita', 9.5)
('Pepperoni', 10.5)
('Hawaiian', 11.5)
('Feast of Meat', 12.5)
('Capricciosa', 12.5)
('Napoletana', 11.5)
('Bianca', 10.5)

Эти методы работают по назначению, но их строковые представления не отображают ожидаемые данные. Вы можете переопределить методы .keys(), .values() и .items() в PizzaMenu. > определение класса, если вы хотите изменить это отображение, но это не обязательно.

Ни один из методов абстрактного базового класса Mapping не позволяет изменять содержимое сопоставления. Это неизменяемое сопоставление. В следующем разделе вы измените PizzaMenu на mutable сопоставление.

Создание пользовательского изменяемого сопоставления

Ранее в этом руководстве вы узнали о дополнительных методах, включенных в интерфейс MutableMapping. Вы можете начать преобразование класса PizzaMenu, созданного в предыдущем разделе, в изменяемое сопоставление, унаследовав от абстрактного базового класса MutableMapping, не внося пока никаких дополнительных изменений:

from collections.abc import MutableMapping

class PizzaMenu(MutableMapping):
    # ...

Вы можете попытаться создать экземпляр этого класса в новом сеансе REPL:

>>> from pizza_menu import PizzaMenu
>>> proposed_pizzas = {
...     "Margherita": 9.50,
...     "Pepperoni": 10.50,
...     "Hawaiian": 11.50,
...     "Feast of Meat": 12.50,
...     "Capricciosa": 12.50,
...     "Napoletana": 11.50,
...     "Bianca": 10.50,
... }

>>> menu = PizzaMenu(proposed_pizzas)
Traceback (most recent call last):
  ...
TypeError: Can't instantiate abstract class PizzaMenu without an
    implementation for abstract methods '__delitem__', '__setitem__'

Неизменяемое сопоставление, созданное вами в предыдущем разделе, имело три обязательных специальных метода. Изменяемые сопоставления имеют еще два: .__delitem__() и .__setitem__(). Поэтому вы должны включить их при создании подкласса MutableMapping.

Изменение, добавление и удаление элементов из меню пиццы

Вы можете начать с добавления .__delitem__() в определение класса:

from collections.abc import MutableMapping

class PizzaMenu(MutableMapping):
    # ...

    def __delitem__(self, key):
        if key not in self._menu and len(key) > 1:
            raise KeyError(key)
        key = self._first_letters.pop(key[0].lower(), key)
        del self._menu[key]

Специальный метод .__delitem__() аналогичен шаблону .__getitem__(). Если ключ не состоит из одной буквы и не является членом ._menu, метод вызывает KeyError. Затем вы вызываете .first_letters.pop() и включаете key в качестве значения по умолчанию. Метод .pop() удаляет и возвращает элемент, но возвращает значение по умолчанию, если элемента нет в словаре.

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

Метод .__setitem__() требует более подробного обсуждения, поскольку существует несколько вариантов, которые необходимо учитывать:

  • Если при установке нового значения вы используете полное название существующей пиццы, .__setitem__() должен изменить значение существующего элемента в ._menu.
  • Если вы используете одну букву, соответствующую существующей пицце, .__setitem__() также должен изменить значение существующего элемента в ._menu.
  • Если вы используете название пиццы, которого еще нет в ._menu, код должен проверить уникальность первой буквы перед добавлением нового элемента в ._menu.

Вы можете включить эти точки в определение .__setitem__():

from collections.abc import MutableMapping

class PizzaMenu(MutableMapping):
    # ...

    def __setitem__(self, key, value):
        first_letter = key[0].lower()
        if len(key) == 1:
            key = self._first_letters.get(first_letter, key)
        if key in self._menu:
            self._menu[key] = value
        elif first_letter in self._first_letters:
            raise ValueError(
                f"'{key}' is invalid."
                " All pizzas must have unique first letters"
            )
        else:
            self._first_letters[first_letter] = key
            self._menu[key] = value

Этот метод выполняет следующие действия:

  • Он присваивает первую букву ключа first_letter.
  • Если ключ представляет собой одну букву, он извлекает полное имя пиццы и переназначает его key. Этот ключ используется в остальной части этого метода. Если подходящей пиццы нет, key не меняется, что позволяет добавить новую пиццу с однобуквенным названием.
  • Если ключ представляет собой полное название пиццы из ._menu, этому ключу присваивается новое значение.
  • Если ключа нет в ._menu, но его первая буква находится в ._first_letters, то это название пиццы недействительно, поскольку оно начинается с уже использованной буквы. Метод вызывает ValueError.
  • Наконец, оставшийся вариант — для нового и действительного ключа. Первая буква добавляется в ._first_letters, а название и цена пиццы добавляются как пара ключ-значение в ._menu.

Обратите внимание, как вы повторяете код, который дважды вызывает ошибку ValueError. Вы можете избежать этого повторения, добавив новый метод:

from collections.abc import MutableMapping

class PizzaMenu(MutableMapping):
    def __init__(self, menu: dict):
        self._menu = {}
        self._first_letters = {}
        for key, value in menu.items():
            first_letter = key[0].lower()
            if first_letter in self._first_letters:
                self._raise_duplicate_key_error(key)
            self._first_letters[first_letter] = key
            self._menu[key] = value

    def _raise_duplicate_key_error(self, key):
        raise ValueError(
            f"'{key}' is invalid."
            " All pizzas must have unique first letters"
        )

    # ...

    def __setitem__(self, key, value):
        first_letter = key[0].lower()
        if len(key) == 1:
            key = self._first_letters.get(first_letter, key)
        if key in self._menu:
            self._menu[key] = value
        elif first_letter in self._first_letters:
            self._raise_duplicate_key_error(key)
        else:
            self._first_letters[first_letter] = key
            self._menu[key] = value

Новый метод ._raise_duplicate_key_error() можно вызывать всякий раз, когда имеется недопустимое имя. Вы используете это в .__init__() и .__setitem__().

Теперь вы можете попытаться изменить объект PizzaMenu в новом сеансе REPL:

>>> from pizza_menu import PizzaMenu
>>> proposed_pizzas = {
...     "Margherita": 9.50,
...     "Pepperoni": 10.50,
...     "Hawaiian": 11.50,
...     "Feast of Meat": 12.50,
...     "Capricciosa": 12.50,
...     "Napoletana": 11.50,
...     "Bianca": 10.50,
... }

>>> menu = PizzaMenu(proposed_pizzas)
>>> menu
PizzaMenu({'Margherita': 9.5, 'Pepperoni': 10.5, 'Hawaiian': 11.5,
    'Feast of Meat': 12.5, 'Capricciosa': 12.5, 'Napoletana': 11.5,
    'Bianca': 10.5})

>>> menu["m"] = 10.25
>>> menu
PizzaMenu({'Margherita': 10.25, 'Pepperoni': 10.5, 'Hawaiian': 11.5,
'Feast of Meat': 12.5, 'Capricciosa': 12.5, 'Napoletana': 11.5,
'Bianca': 10.5})

>>> menu["Pepperoni"] += 1.25
>>> menu
PizzaMenu({'Margherita': 10.25, 'Pepperoni': 11.75, 'Hawaiian': 11.5,
    'Feast of Meat': 12.5, 'Capricciosa': 12.5, 'Napoletana': 11.5,
    'Bianca': 10.5})

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

>>> menu["Regina"] = 13
>>> menu
PizzaMenu({'Margherita': 10.25, 'Pepperoni': 11.75, 'Hawaiian': 11.5,
    'Feast of Meat': 12.5, 'Capricciosa': 12.5, 'Napoletana': 11.5,
    'Bianca': 10.5, 'Regina': 13})

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

>>> menu["Plain Pizza"] = 10
Traceback (most recent call last):
  ...
ValueError: 'Plain Pizza' is an invalid name. All pizzas must
    have unique first letters

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

>>> del menu["Hawaiian"]
>>> menu
PizzaMenu({'Margherita': 10.25, 'Pepperoni': 11.75, 'Feast of Meat': 12.5,
    'Capricciosa': 12.5, 'Napoletana': 11.5, 'Bianca': 10.5})

>>> menu["h"]
Traceback (most recent call last):
  ...
KeyError: 'h'

>>> menu["Hawaiian"]
Traceback (most recent call last):
  ...
KeyError: 'Hawaiian'

Как только вы удалите гавайскую пиццу, вы получите KeyError при попытке доступа к ней, используя либо одну букву, либо полное имя.

Используйте другие методы, изменяющие сопоставления

Абстрактный базовый класс MutableMapping также добавляет в класс дополнительные методы, такие как .pop() и .update(). Вы можете проверить, работают ли они должным образом:

>>> menu.pop("n")
11.5
>>> menu
PizzaMenu({'Margherita': 10.25, 'Pepperoni': 11.75,
    'Feast of Meat': 12.5, 'Capricciosa': 12.5, 'Bianca': 10.5})

>>> menu.update({"Margherita": 11.5, "c": 14})
>>> menu
PizzaMenu({'Margherita': 11.5, 'Pepperoni': 11.75,
    'Feast of Meat': 12.5, 'Capricciosa': 14, 'Bianca': 10.5})

>>> menu.update({"Festive Pizza": 16})
Traceback (most recent call last):
  ...
ValueError: 'Festive Pizza' is an invalid name. All pizzas must
    have unique first letters

Вы можете использовать одну букву в .pop(), которая удаляет пиццу «Наполетана». Метод .update() также работает с полными названиями пицц или отдельными буквами. Цена пиццы Каприччиоза обновляется, поскольку вы включаете ключ "c" при вызове .update().

Вы также не можете использовать .update() для добавления неверного названия пиццы. Праздничная пицца была отклонена, поскольку уже существует другая пицца с названием, начинающимся с F.

Это показывает, что вам не нужно определять все эти методы, поскольку может быть достаточно уже определенных вами специальных методов. В качестве упражнения вы можете убедиться, что методы, добавленные с помощью MutableMapping, не требуют переопределения в этом примере.

Вот окончательная версия класса PizzaMenu, который наследуется от MutableMapping:

from collections.abc import MutableMapping

class PizzaMenu(MutableMapping):
    def __init__(self, menu: dict):
        self._menu = {}
        self._first_letters = {}
        for key, value in menu.items():
            first_letter = key[0].lower()
            if first_letter in self._first_letters:
                self._raise_duplicate_key_error(key)
            self._first_letters[first_letter] = key
            self._menu[key] = value

    def _raise_duplicate_key_error(self, key):
        raise ValueError(
            f"'{key}' is invalid."
            " All pizzas must have unique first letters"
        )

    def __getitem__(self, key):
        if key not in self._menu and len(key) > 1:
            raise KeyError(key)
        key = self._first_letters.get(key[0].lower(), key)
        return self._menu[key]

    def __setitem__(self, key, value):
        first_letter = key[0].lower()
        if len(key) == 1:
            key = self._first_letters.get(first_letter, key)
        if key in self._menu:
            self._menu[key] = value
        elif first_letter in self._first_letters:
            self._raise_duplicate_key_error(key)
        else:
            self._first_letters[first_letter] = key
            self._menu[key] = value

    def __delitem__(self, key):
        if key not in self._menu and len(key) > 1:
            raise KeyError(key)
        key = self._first_letters.pop(key[0].lower(), key)
        del self._menu[key]

    def __iter__(self):
        return iter(self._menu)

    def __len__(self):
        return len(self._menu)

    def __repr__(self):
        return f"{self.__class__.__name__}({self._menu})"

    def __str__(self):
        return str(self._menu)

    def __contains__(self, key):
        return key in self._menu

Эта версия содержит все методы, обсуждаемые в этом разделе руководства.

Вы напишете еще одну версию этого класса в следующем разделе этого руководства.

Наследование от dict и collections.UserDict

При наследовании от Mapping или MutableMapping вам необходимо определить все обязательные методы. Для Сопоставление необходимо определить как минимум .__getitem__(), .__iter__() и .__len__() , а для MutableMapping также требуются .__setitem__() и .__delitem__(). У вас есть полный контроль над определением сопоставления.

В предыдущих разделах вы создали класс на основе этих абстрактных базовых классов. Это полезно, чтобы помочь понять, что происходит при сопоставлении.

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

Класс, наследуемый от dict

В более ранних версиях Python было невозможно создавать подклассы для встроенных типов, таких как dict. Однако это уже не так. Тем не менее, при наследовании от dict возникают проблемы.

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

class PizzaMenuDict(dict):
    def __init__(self, menu: dict):
        _menu = {}
        self._first_letters = {}
        for key, value in menu.items():
            first_letter = key[0].lower()
            if first_letter in self._first_letters:
                self._raise_duplicate_key_error(key)
            self._first_letters[first_letter] = key
            _menu[key] = value
        super().__init__(_menu)

    def _raise_duplicate_key_error(self, key):
        raise ValueError(
            f"'{key}' is invalid."
            " All pizzas must have unique first letters"
        )

Класс теперь наследуется от dict. ._raise_duulate_key_error() идентичен версии в PizzaMenu, которую вы написали ранее. В метод .__init__() внесены некоторые изменения:

  • Внутренний словарь больше не является атрибутом данных self._menu, а является локальной переменной _menu, поскольку он больше не нужен где-либо в классе.
  • Эта локальная переменная ._menu передается инициализатору dict с помощью super() в последней строке.

Поскольку объект PizzaMenuDict является словарем, вы можете получить доступ к данным словаря непосредственно через объект, используя self внутри методов. Любые операции над self будут использовать методы, определенные в PizzaMenuDict. Однако если методы не определены в PizzaMenuDict, используются методы dict.

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

>>> from pizza_menu import PizzaMenuDict
>>> menu = PizzaMenuDict({"Margherita": 9.5, "Pepperoni": 10.5})
>>> menu
{'Margherita': 9.5, 'Pepperoni': 10.5}

>>> menu = PizzaMenuDict({"Margherita": 9.5, "Meat Feast": 10.5})
Traceback (most recent call last):
  ...
ValueError: 'Meat Feast' is an invalid name.
    All pizzas must have unique first letters

Вы получаете сообщение об ошибке при попытке создать объект PizzaMenuDict с двумя пиццами, начинающимися с M. Однако ни один из других специальных методов не определен. Таким образом, этот класс еще не имеет всех необходимых функций:

>>> menu["m"]
Traceback (most recent call last):
  ...
KeyError: 'm'

Вы не можете получить доступ к значению, используя одну букву. Но вы можете реализовать метод .__getitem__(), который похож, но не идентичен методу, который вы определили в предыдущем разделе в PizzaMenu:

class PizzaMenuDict(dict):
    # ...

    def __getitem__(self, key):
        if key not in self and len(key) > 1:
            raise KeyError(key)
        key = self._first_letters.get(key[0].lower(), key)
        return super().__getitem__(key)

Есть два отличия от .__getitem__() в PizzaMenu из предыдущего раздела, поскольку атрибут данных ._menu больше не присутствует в этой версии. :

  1. Оператор if в PizzaMenu.__getitem__() проверяет, является ли key членом self._menu. Однако эквивалентный условный оператор в PizzaMenuDict.__getitem__() проверяет членство непосредственно в self.
  2. Оператор return в PizzaMenu.__getitem__() возвращает self._menu[key]. Однако последняя строка в PizzaMenuDict.__getitem__() вызывает и возвращает специальный метод .__getitem__() суперкласса с использованием измененного key. Суперкласс — dict.

Итак, метод .__getitem__() в PizzaMenuDict обрабатывает регистр одной буквы, а затем вызывает .__getitem__() в dict класс.

Вы заметите тот же шаблон в .__setitem__():

class PizzaMenuDict(dict):
    # ...

    def __setitem__(self, key, value):
        first_letter = key[0].lower()
        if len(key) == 1:
            key = self._first_letters.get(first_letter, key)
        if key in self:
            super().__setitem__(key, value)
        elif first_letter in self._first_letters:
            self._raise_duplicate_key_error(key)
        else:
            self._first_letters[first_letter] = key
            super().__setitem__(key, value)

Всякий раз, когда вам нужно обновить данные в сопоставлении, вы вызываете dict.__setitem__() вместо установки значений в атрибуте данных ._menu, как вы это делали в Меню Пиццы.

Вам также необходимо определить .__delitem__() аналогичным образом:

class PizzaMenuDict(dict):
    # ...

    def __delitem__(self, key):
        if key not in self and len(key) > 1:
            raise KeyError(key)
        key = self._first_letters.pop(key[0].lower(), key)
        super().__delitem__(key)

Последняя строка метода вызывает метод .__delitem__() в dict.

Обратите внимание, что вам не нужно определять специальные методы, такие как .__repr__(), .__str__(), .__iter__() или .__len__(), как это нужно было делать при наследовании от абстрактных базовых классов Mapping и MutableMapping. Поскольку PizzaMenuDict является подклассом dict, вы можете положиться на методы словаря, если вам не требуется другое поведение. Вам нужно будет начать новый сеанс REPL, поскольку вы внесли изменения в определение класса:

>>> from pizza_menu import PizzaMenuDict
>>> menu = PizzaMenuDict({"Margherita": 9.5, "Pepperoni": 10.5})

>>> for pizza in menu:
...     print(pizza)
...
Margherita
Pepperoni

>>> menu
{'Margherita': 9.5, 'Pepperoni': 10.5}

>>> len(menu)
2

Итерация использует метод .__iter__() в dict. Представление строк и определение длины объекта также работают должным образом, поскольку специальных методов .__repr__() и .__len__() в dict достаточно.

Методы, требующие обновления

Кажется, что для наследования от dict требуется меньше работы. Однако есть и другие методы, на которые следует обратить внимание. Например, вы можете изучить .pop() с помощью PizzaMenuDict:

>>> menu.pop("m")
Traceback (most recent call last):
  ...
KeyError: 'm'

Поскольку вы не определили .pop() в PizzaMenuDict, вместо этого класс использует метод dict. Однако dict.pop() использует dict.__getitem__(), поэтому он обходит метод .__getitem__(), который вы определили специально для ПиццаМенюДикт. Вам необходимо переопределить .pop() в PizzaMenuDict:

class PizzaMenuDict(dict):
    # ...

    def pop(self, key):
        key = self._first_letters.pop(key[0].lower(), key)
        return super().pop(key)

Прежде чем вызывать и возвращать метод .pop() суперкласса, убедитесь, что key всегда является полным именем пиццы. Вы можете убедиться, что это работает в новом сеансе REPL:

>>> from pizza_menu import PizzaMenuDict
>>> menu = PizzaMenuDict({"Margherita": 9.5, "Pepperoni": 10.5})

>>> menu.pop("m")
9.5

>>> menu
{'Pepperoni': 10.5}

Теперь вы можете использовать однобуквенный аргумент в .pop().

Вам нужно будет просмотреть все методы dict, чтобы определить, какие из них необходимо определить в PizzaMenuDict. Вы можете попробовать выполнить это занятие в качестве упражнения. Вы заметите, что этот процесс делает этот подход более длительным и более подверженным ошибкам. Поэтому в этом примере с пиццерией лучшим вариантом может быть наследование непосредственно от MutableMapping.

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

Другая альтернатива: collections.UserDict

В модуле collections вы найдете еще один класс, от которого можно наследовать, чтобы создать объект, подобный словарю. Вы можете наследовать от UserDict вместо MutableMapping или dict. UserDict был включен в Python, когда было невозможно наследовать непосредственно от dict. Однако UserDict не полностью устарел теперь, когда стало возможным создание подкласса dict.

UserDict создает обертку вокруг словаря, а не создает подкласс dict. Объект UserDict включает атрибут .data, который представляет собой словарь, содержащий данные. Этот атрибут аналогичен атрибуту ._menu, который вы добавили в PizzaMenu при наследовании от Mapping и MutableMapping.

Однако UserDict — это конкретный класс, а не абстрактный базовый класс. Таким образом, вам не нужно определять необходимые специальные методы, если вам не требуется другое поведение.

Вы уже написали две версии класса для создания меню для пиццерии, поэтому в этом уроке вы не будете писать третью. При этом больше нечего узнать о сопоставлениях. Однако, если вы хотите узнать больше о сходствах и различиях между наследованием от dict или UserDict, вы можете прочитать «Пользовательские словари Python: наследование от dict против UserDict».

Заключение

Словарь Python является наиболее часто используемым сопоставлением, и он подойдет в большинстве случаев, когда вам нужно сопоставление. Однако в стандартной библиотеке и сторонних библиотеках есть и другие сопоставления. У вас также могут быть приложения, в которых вам необходимо создать собственное сопоставление.

Из этого урока вы узнали:

  • Основные характеристики сопоставления
  • Операции, общие для большинства сопоставлений
  • Абстрактные базовые классы Mapping и MutableMapping
  • Определяемые пользователем изменяемые и неизменяемые сопоставления и способы их создания

Понимание общих черт всех сопоставлений и того, что происходит за кулисами при создании и использовании объектов сопоставления, поможет вам более эффективно использовать словари и другие сопоставления в ваших программах Python.