Anulación del método dict.update () en la subclase para evitar sobrescribir las claves dict

Más temprano hoy, leí la pregunta ” Generar error si Python dict comprensión supera una clave ” y decidí probar mi respuesta. El método que se me ocurrió de forma natural fue hacer una subclase de dict para esto. Sin embargo, me quedé atascado en mi respuesta, y ahora estoy obsesionada con hacer que esto funcione para mí.

Notas:

  • No, no planeo entregar la respuesta a esta pregunta como respuesta a la otra pregunta.
  • Esto es puramente un ejercicio intelectual para mí en este punto. Como cuestión práctica, es casi seguro que use un namedtuple o un diccionario regular siempre que tenga un requisito para algo como esto.

Mi (no funciona) Solución:

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

Mis pruebas y resultados esperados

 >>> 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 "", line 1, in  File "", 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. >>> 

Estoy seguro de que el problema está en mi método de update() , pero no puedo determinar qué es lo que estoy haciendo mal.

A continuación se muestra la versión original de mi método update() . Esta versión falla como se esperaba en los duplicados cuando se llama a my_dict.update({k: v}) para un par clave / valor que ya está en el dict, pero no falla cuando se incluye una clave duplicada al crear el dict original, debido al hecho de que convertir los argumentos a un dict da resultados en el comportamiento predeterminado para un diccionario, es decir, sobrescribir la clave duplicada.

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

Tenga en cuenta que, según la documentación:

  • dict.update toma un solo other parámetro, “ya sea otro objeto del diccionario o una iterable de pares de clave / valor” (he usado collections.Mapping dict.update para probar esto) y “Si se especifican argumentos de palabras clave, el diccionario se actualiza con esos pares clave / valor “ ; y
  • dict() toma un solo Mapping o Iterable junto con **kwargs opcionales (lo mismo que acepta la update …).

Esta no es exactamente la interfaz que ha implementado, lo que lleva a algunos problemas. Habría implementado esto de la siguiente manera:

 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 

En uso:

 >>> 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 "", line 1, in  UniqueKeyDict((k, v) for k, v in ('a1', 'b2', 'c3', 'a4')) File "", line 5, in __init__ self.update(other, **kwargs) File "", line 15, in update self[k] = v File "", line 10, in __setitem__ raise DuplicateKeyError(msg.format(key, self[key])) DuplicateKeyError: "key 'a' already exists with value '1'" 

y:

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

¡Si alguna vez terminas usando esto, me inclinaría a darle una __repr__ diferente para evitar la confusión!

Es interesante que simplemente anular __setitem__ no es suficiente para cambiar el comportamiento de la update en dict . Hubiera esperado que dict utilizara su método __setitem__ cuando se actualice mediante la update . En todos los casos, creo que es mejor implementar collections.MutableMapping para lograr el resultado deseado sin tocar la 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) 

Edición: incluye dict como clase base para satisfacer la isinstance(x, dict) .

No estoy seguro de que este sea el problema, pero me di cuenta de que está tratando sus args en el método de update como una lista de pares:

 for k, v in args[0] 

mientras que en realidad estás suministrando un diccionario:

 ukd.update({'a': 5}) 

¿Has probado esto?

 try: for k, v in args[0].iteritems(): self.__setitem__(k, v) except ValueError: pass 

EDITAR: Probablemente este error pasó desapercibido porque está except un ValueError , que es lo que trata un diccionario como una lista de pares.

Pude lograr el objective con el siguiente código:

 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 

¿Por qué no hacer algo a lo largo de las líneas inspiradas en MultiKeyDict usando setdefault? Esto deja el método de actualización como una forma de anular los valores almacenados actualmente, rompiendo, lo sé, la intención que d [k] = v == d.update ({k, v}). En mi aplicación la anulación fue útil. Entonces, antes de marcar esto como que no responde a la pregunta OP, considere que esta respuesta podría ser útil para otra persona.

 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) 

Más bien nuevo en Python, así que pruébalo, probablemente lo merezca …