Подтвердить что ты не робот

Переопределение метода 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)
4b9b3361

Ответ 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, так что пламя, вероятно, заслуживает этого...