Answer a question

Earlier today, I read the question "Raise error if python dict comprehension overwrites a key" and decided to try my hand at an answer. The method that naturally occurred to me was to subclass dict for this. However, I got stuck on my answer, and now I'm obsessed with getting this worked out for myself.

Notes:

  • No - I do not plan on turning in the answer to this question as an answer to the other question.
  • This is purely an intellectual exercise for me at this point. As a practical matter, I would almost certainly use a namedtuple or a regular dictionary wherever I have a requirement for something like this.

My (not quite working) Solution:

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])

My Tests and Expected Results

>>> 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.
>>>

I'm certain the issue is in my update() method, but I'm not able to determine just what I'm doing wrong.

Below is the original version of my update() method. This version fails as expected on duplicates when calling my_dict.update({k: v}) for a key/value pair already in the dict, but does not fail when including a duplicate key while creating the original dict, due to the fact that converting the args to a dict results in default behavior for a dictionary, i.e., overwriting the duplicate key.

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

Answers

Note that, per the documentation:

  • dict.update takes a single other parameter, "either another dictionary object or an iterable of key/value pairs" (I've used collections.Mapping to test for this) and "If keyword arguments are specified, the dictionary is then updated with those key/value pairs"; and
  • dict() takes a single Mapping or Iterable along with optional **kwargs (the same as update accepts...).

This is not quite the interface you have implemented, which is leading to some issues. I would have implemented this as follows:

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

In use:

>>> 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'"

and:

>>> 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'}

If you ever end up using this, I'd be inclined to give it a different __repr__ to avoid confusion!

Logo

Python社区为您提供最前沿的新闻资讯和知识内容

更多推荐