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