Переопределение метода dict.update() в подклассе для предотвращения перезаписи ключей dict
Ранее сегодня я прочитал вопрос "Повысить ошибку, если понимание python dict перезаписывает ключ" и решил попробовать свои силы при ответе. Метод, который, естественно, приходил ко мне, заключался в подклассе dict
для этого. Тем не менее, я застрял на своем ответе, и теперь я одержим тем, что все это получилось для меня.
Примечания:
- Нет - я не собираюсь входить в ответ на этот вопрос как ответ на другой вопрос.
- Это чисто интеллектуальное упражнение для меня в этот момент. В практическом плане я почти наверняка использовал бы
namedtuple
или обычный словарь, где бы я ни требовал чего-то подобного.
Мой (не совсем рабочий) Решение:
class DuplicateKeyError(KeyError):
pass
class UniqueKeyDict(dict):
def __init__(self, *args, **kwargs):
self.update(*args, **kwargs)
def __setitem__(self, key, value):
if key in self: # Validate key doesn't already exist.
raise DuplicateKeyError('Key \'{}\' already exists with value \'{}\'.'.format(key, self[key]))
super().__setitem__(key, value)
def update(self, *args, **kwargs):
if args:
if len(args) > 1:
raise TypeError('Update expected at most 1 arg. Got {}.'.format(len(args)))
else:
try:
for k, v in args[0]:
self.__setitem__(k, v)
except ValueError:
pass
for k in kwargs:
self.__setitem__(k, kwargs[k])
Мои тесты и ожидаемые результаты
>>> ukd = UniqueKeyDict((k, int(v)) for k, v in ('a1', 'b2', 'c3', 'd4')) # Should succeed.
>>> ukd['e'] = 5 # Should succeed.
>>> print(ukd)
{'a': 1, 'b': 2, 'c': 3, d: 4, 'e': 5}
>>> ukd['a'] = 5 # Should fail.
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 8, in __setitem__
__main__.DuplicateKeyError: Key 'a' already exists with value '1'.
>>> ukd.update({'a': 5}) # Should fail.
>>> ukd = UniqueKeyDict((k, v) for k, v in ('a1', 'b2', 'c3', 'd4', 'a5')) # Should fail.
>>>
Я уверен, проблема в моем методе update()
, но я не могу определить, что я делаю неправильно.
Ниже приведена исходная версия моего метода update()
. Эта версия терпит неудачу, как ожидалось, при дублировании при вызове my_dict.update({k: v})
для пары ключ/значение, уже находящейся в dict, но не сбой при включении дублирующего ключа при создании оригинального dict из-за того, что преобразование args в dict
приводит к поведению по умолчанию для словаря, то есть перезаписыванию дубликата ключа.
def update(self, *args, **kwargs):
for k, v in dict(*args, **kwargs).items():
self.__setitem__(k, v)
Ответы
Ответ 1
Обратите внимание, что в документации:
-
dict.update
принимает один параметр other
, "либо другой объект словаря, либо итерабельность пар ключ/значение" (I ' вы использовали collections.Mapping
, чтобы проверить это) и "Если указаны аргументы ключевого слова, словарь затем обновляется этими парами ключ/значение"; и
-
dict()
принимает один Mapping
или Iterable
вместе с необязательным **kwargs
(то же, что и update
принимает...).
Это не совсем тот интерфейс, который вы реализовали, что приводит к некоторым проблемам. Я бы выполнил это следующим образом:
from collections import Mapping
class DuplicateKeyError(KeyError):
pass
class UniqueKeyDict(dict):
def __init__(self, other=None, **kwargs):
super().__init__()
self.update(other, **kwargs)
def __setitem__(self, key, value):
if key in self:
msg = 'key {!r} already exists with value {!r}'
raise DuplicateKeyError(msg.format(key, self[key]))
super().__setitem__(key, value)
def update(self, other=None, **kwargs):
if other is not None:
for k, v in other.items() if isinstance(other, Mapping) else other:
self[k] = v
for k, v in kwargs.items():
self[k] = v
При использовании:
>>> UniqueKeyDict((k, v) for k, v in ('a1', 'b2', 'c3', 'd4'))
{'c': '3', 'd': '4', 'a': '1', 'b': '2'}
>>> UniqueKeyDict((k, v) for k, v in ('a1', 'b2', 'c3', 'a4'))
Traceback (most recent call last):
File "<pyshell#8>", line 1, in <module>
UniqueKeyDict((k, v) for k, v in ('a1', 'b2', 'c3', 'a4'))
File "<pyshell#7>", line 5, in __init__
self.update(other, **kwargs)
File "<pyshell#7>", line 15, in update
self[k] = v
File "<pyshell#7>", line 10, in __setitem__
raise DuplicateKeyError(msg.format(key, self[key]))
DuplicateKeyError: "key 'a' already exists with value '1'"
и
>>> ukd = UniqueKeyDict((k, v) for k, v in ('a1', 'b2', 'c3', 'd4'))
>>> ukd.update((k, v) for k, v in ('e5', 'f6')) # single Iterable
>>> ukd.update({'h': 8}, g='7') # single Mapping plus keyword args
>>> ukd
{'e': '5', 'f': '6', 'a': '1', 'd': '4', 'c': '3', 'h': 8, 'b': '2', 'g': '7'}
Если вы когда-нибудь закончите использовать это, я бы склонен дать ему другой __repr__
, чтобы избежать путаницы!
Ответ 2
Я не уверен, что это проблема, но я только заметил, что вы рассматриваете свой args
в методе update
как список пар:
for k, v in args[0]
пока вы действительно поставляете словарь:
ukd.update({'a': 5})
Вы пробовали это:
try:
for k, v in args[0].iteritems():
self.__setitem__(k, v)
except ValueError:
pass
РЕДАКТИРОВАТЬ: Вероятно, эта ошибка осталась незамеченной, потому что вы except
ing a ValueError
, что и объясняет словарь как список пар.
Ответ 3
Интересно, что просто переопределить __setitem__
недостаточно, чтобы изменить поведение update
в dict
. Я бы предположил, что dict
будет использовать свой метод __setitem__
при его обновлении с помощью update
. Во всех случаях я думаю, что лучше реализовать collections.MutableMapping
для достижения желаемого результата, не касаясь update
:
import collections
class UniqueKeyDict(collections.MutableMapping, dict):
def __init__(self, *args, **kwargs):
self._dict = dict(*args, **kwargs)
def __getitem__(self, key):
return self._dict[key]
def __setitem__(self, key, value):
if key in self:
raise DuplicateKeyError("Key '{}' already exists with value '{}'.".format(key, self[key]))
self._dict[key] = value
def __delitem__(self, key):
del self._dict[key]
def __iter__(self):
return iter(self._dict)
def __len__(self):
return len(self._dict)
Изменить: включить dict
в качестве базового класса для проверки isinstance(x, dict)
.
Ответ 4
Я смог достичь цели с помощью следующего кода:
class UniqueKeyDict(dict):
def __init__(self, *args, **kwargs):
self.update(*args, **kwargs)
def __setitem__(self, key, value):
if self.has_key(key):
raise DuplicateKeyError("%s is already in dict" % key)
dict.__setitem__(self, key, value)
def update(self, *args, **kwargs):
for d in list(args) + [kwargs]:
for k,v in d.iteritems():
self[k]=v
Ответ 5
Почему бы не сделать что-то по строкам, вдохновленным MultiKeyDict, используя setdefault? Это оставляет метод обновления как способ переопределить сохраненные в настоящее время значения, и я знаю, что намерение d [k] = v == d.update({k, v}). В моем приложении переопределение было полезным. Поэтому, прежде чем отмечать это как не отвечающий на вопрос OP, рассмотрите этот ответ, возможно, полезный для кого-то другого.
class DuplicateKeyError(KeyError):
"""File exception rasised by UniqueKeyDict"""
def __init__(self, key, value):
msg = 'key {!r} already exists with value {!r}'.format(key, value)
super(DuplicateKeyError, self).__init__(msg)
class UniqueKeyDict(dict):
"""Subclass of dict that raises a DuplicateKeyError exception"""
def __setitem__(self, key, value):
if key in self:
raise DuplicateKeyError(key, self[key])
self.setdefault(key, value)
class MultiKeyDict(dict):
"""Subclass of dict that supports multiple values per key"""
def __setitem__(self, key, value):
self.setdefault(key, []).append(value)
Скорее новичок в python, так что пламя, вероятно, заслуживает этого...