Сообщить об ошибке.

Класс UserDict модуля collections в Python

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

Синтаксис:

import collections

collections.UserDict([initialdata])

Параметры:

  • initialdata - источник данных для при создании экземпляра пользовательского класса.

Описание:

Класс UserDict() модуля collections это удобная обертка для обычного объекта dict. Этот класс обеспечивает то же поведение, что и встроенный тип dict, с дополнительной возможностью предоставления доступа к базовому словарю через атрибут экземпляра UserDict.data.

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

Класс collections.UserDict был добавлен в Python еще тогда, когда было невозможно напрямую наследоваться от типа dict. Несмотря на то, что необходимость в этом классе была частично вытеснена, collections.UserDict по-прежнему доступен в стандартной библиотеке как для удобства, так и для обратной совместимости. К тому-же с этим классом может быть проще работать, т.к. базовый словарь доступен как атрибут data.

Создание пользовательского класса, наследуемого от класса UserDict.

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

>>> from collections import UserDict
>>> class UpperCaseDict(UserDict):
...     def __setitem__(self, key, value):
...         key = key.upper()
...         super().__setitem__(key, value)

>>> numbers = UpperCaseDict({"one": 1, "two": 2})
>>> numbers["three"] = 3
>>> numbers.update({"four": 4})
>>> numbers.setdefault("five", 5)
# 5
>>> numbers
# {'ONE': 1, 'TWO': 2, 'THREE': 3, 'FOUR': 4, 'FIVE': 5}

Пользовательский класс UpperCaseDict работает всегда корректно. Нет необходимости предоставлять собственные реализации методов __init__(), dict.update() или dict.setdefault(). Класс просто работает! Это связано с тем, что в UserDict все методы, обновляющие существующие ключи или добавляющие новые, последовательно полагаются на пользовательскую версию __setitem__().

Наиболее заметным отличием между UserDict и dict является атрибут UserDict.data, который содержит обернутый словарь. Использование UserDict.data напрямую может сделать код более простым, т.к. не нужно постоянно вызывать super() для обеспечения желаемой функциональности.

Создание пользовательского класса, наследуемого от типа dict.

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

Создадим подкласс dict, который переопределяет реализацию метода __setitem__():

>>> class UpperCaseDict(dict):
...     def __setitem__(self, key, value):
...         key = key.upper()
...         super().__setitem__(key, value)

>>> numbers = UpperCaseDict()
>>> numbers["one"] = 1
>>> numbers["two"] = 2
>>> numbers["three"] = 3
>>> numbers
# {'ONE': 1, 'TWO': 2, 'THREE': 3}

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

>>> numbers = UpperCaseDict({"one": 1, "two": 2, "three": 3})
>>> numbers
# {'one': 1, 'two': 2, 'three': 3}

Что произошло? Почему словарь не преобразует ключи в буквы верхнего регистра? Похоже, что инициализатор класса __init__() не вызывает неявно __setitem__() для создания словаря. Таким образом, преобразование в верхний регистр никогда не выполняется.

К сожалению, эта проблема затрагивает и другие методы словаря, такие как dict.update() и dict.setdefault(), например:

>>> numbers = UpperCaseDict()
>>> numbers["one"] = 1
>>> numbers
# {'ONE': 1}
>>> numbers.update({"two": 2})
>>> numbers
# {'ONE': 1, 'two': 2}
>>> numbers.setdefault("three", 3)
# 3
>>> numbers
{'ONE': 1, 'two': 2, 'three': 3}

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

class UpperCaseDict(dict):
    def __init__(self, mapping=None, /, **kwargs):
        if mapping is not None:
            mapping = {
                str(key).upper(): value for key, value in mapping.items()
            }
        else:
            mapping = {}
        if kwargs:
            mapping.update(
                {str(key).upper(): value for key, value in kwargs.items()}
            )
        super().__init__(mapping)

    def __setitem__(self, key, value):
        key = key.upper()
        super().__setitem__(key, value)

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

>>> numbers = UpperCaseDict({"one": 1, "two": 2})
>>> numbers
# {'ONE': 1, 'TWO': 2}
>>> numbers.update({"three": 3})
>>> numbers
# {'ONE': 1, 'TWO': 2, 'three': 3}

Собственный метод UpperCaseDict.__init__() устранил проблему с инициализацией. Тем не менее, другие методы, такие как dict.update(), продолжают работать некорректно.

Почему подклассы типа dict ведут себя таким образом? Встроенные типы были разработаны и реализованы с учетом принципа "открыто-закрыто". Следовательно, они открыты для расширения, но закрыты для модификации. Разрешение модификаций основных функций этих классов потенциально может нарушить их инварианты. Поэтому разработчики ядра Python решили защитить их от модификаций.

Практические примеры создания пользовательских словарей.

При принятии решения о наследовании от dict или collections.UserDict следует учитывать несколько факторов. Эти факторы включают, но не ограничиваются следующим:

  • Объем работы.
  • Риск ошибок и багов.
  • Простота использования и кодирования.

Словарь, который обращается к ключам через значения.

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

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

# файл `value_dict.py`
class ValueDict(dict):
    def key_of(self, value):
        for k, v in self.items():
            if v == value:
                return k
        raise ValueError(value)

    def keys_of(self, value):
        for k, v in self.items():
            if v == value:
                yield k

Наследуемся от встроенного dict. Почему? В этом примере добавляется функциональность, которая не изменяет основные функции словаря. Поэтому наследование от dict более целесообразно. Это также более эффективно с точки зрения производительности.

Метод ValueDict.key_of() перебирает пары ключ-значение в базовом словаре. Условный оператор проверяет значения, соответствующие целевому значению. Блок кода if возвращает ключ первого совпадающего значения. Если целевой ключ отсутствует, то метод вызывает ошибку ValueError.

Метод ValueDict.keys_of() представляет собой генератор (выдает ключи по запросу), будет выдавать только те ключи, значение которых соответствует значению, указанному в качестве аргумента при вызове метода.

>>> vd = ValueDict()
>>> vd["apple"] = 2
>>> vd["banana"] = 3
>>> vd.update({"orange": 2})
>>> vd
# {'apple': 2, 'banana': 3, 'orange': 2}
>>> vd.key_of(2)
# 'apple'
>>> vd.key_of(3)
# 'banana'
>>> list(vd.keys_of(2))
# ['apple', 'orange']

Словарь ValueDict работает как и ожидается. Он наследует функции основного словаря от Python dict и реализует новые функции.

В общем, collections.UserDict необходимо использовать для создания класса, похожего на словарь, который действует как встроенный класс dict, но при этом настраивает некоторые специальные методы, такие как __setitem__() и __getitem__().

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

Словарь с дополнительными функциональными возможностями.

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

  • apply(action): Принимает вызываемое действие в качестве аргумента и применяет его ко всем значениям в базовом словаре.
  • remove(key): Удаляет указанный ключ из базового словаря.
  • is_empty(): Возвращает True или False в зависимости от того, пуст ли словарь или нет.

Чтобы реализовать эти три метода, не нужно изменять основное поведение встроенного типа dict.

# файл `extended_dict.py`
class ExtendedDict(dict):
    def apply(self, action):
        for key, value in self.items():
            self[key] = action(value)

    def remove(self, key):
        del self[key]

    def is_empty(self):
        return len(self) == 0

Смотрим как работает ExtendedDict:

>>> ed = ExtendedDict({"one": 1, "two": 2, "three": 3})
>>> ed
# {'one': 1, 'two': 2, 'three': 3}
>>> ed.apply(lambda x: x**2)
>>> ed
# {'one': 1, 'two': 4, 'three': 9}
>>> ed.remove("two")
>>> ed
# {'one': 1, 'three': 9}
>>> ed.is_empty()
# False