覆盖子类中的dict.update()方法以防止覆盖dict键

时间:2015-05-14 15:50:33

标签: python dictionary

今天早些时候,我读到了问题“Raise error if python dict comprehension overwrites a key”,并决定尝试一下答案。我自然而然地想到的方法是将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()方法的原始版本。对于已经在dict中的键/值对调用my_dict.update({k: v})时,此版本在重复项上失败,但在创建原始dict时包含重复键时不会失败,因为将args转换为a dict导致字典的默认行为,即覆盖重复的密钥。

def update(self, *args, **kwargs):
    for k, v in dict(*args, **kwargs).items():
        self.__setitem__(k, v)

6 个答案:

答案 0 :(得分:7)

有趣的是,简单地覆盖__setitem__并不足以改变updatedict的行为。我希望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)检查。

答案 1 :(得分:4)

我不确定这是问题所在但我只是注意到您将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 ValueError,这就是将字典视为对的列表会引起的。

答案 2 :(得分:4)

请注意,根据文档:

  • dict.update只使用一个other参数,“另一个字典对象或一对键/值对的迭代”(我使用了collections.Mapping来对此进行测试)和“如果指定了关键字参数,则使用这些键/值对更新字典”;和
  • dict()只需一个MappingIterable以及可选的**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__来避免混淆!

答案 3 :(得分:2)

我能够通过以下代码实现目标:

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

答案 4 :(得分:1)

为什么不使用setdefault在MultiKeyDict的启发下做一些事情呢?这使得更新方法成为覆盖当前存储值的一种方式,我知道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如此火焰,可能值得...

答案 5 :(得分:1)

这个有趣的问题有点老了,已经有了一些可靠的答案(我最喜欢的是来自 sirfz 的那个)。不过,我想再提出一个建议。您可以使用 dict-wrapper UserDict。如果我没记错的话,这应该可以完成您正在寻找的工作:

from collections import UserDict

class DuplicateKeyError(KeyError):
    pass

class UniqueKeyDict(UserDict):

    def __setitem__(self, key, value):
        if key in self:
            raise DuplicateKeyError(f"Key '{key}' already exists with value '{self[key]}'")
        self.data[key] = value

collections.abc.MutableMapping 的用法一样,update 方法被隐式修改。但相比之下,您必须(重新)定义 __setitem__ 方法。由于您的修改相当小,因此使用 UserDict 对我来说似乎是一种合适的方法。

此类的实例不是dict的实例,而是collections.abc.Mapping的实例,应该用于测试dict-likeness。