¿Cómo “perfectamente” anular un dict?

¿Cómo puedo hacer como “perfecto” una subclase de dict como sea posible? El objective final es tener un dict simple en el que las teclas estén en minúscula.

Parece que debería haber algún pequeño conjunto de primitivas que pueda anular para hacer que esto funcione, pero de acuerdo con todas mis investigaciones e bashs, parece que este no es el caso:

Aquí está mi primer bash, get() no funciona y sin duda hay muchos otros problemas menores:

 class arbitrary_dict(dict): """A dictionary that applies an arbitrary key-altering function before accessing the keys.""" def __keytransform__(self, key): return key # Overridden methods. List from # https://stackoverflow.com/questions/2390827/how-to-properly-subclass-dict def __init__(self, *args, **kwargs): self.update(*args, **kwargs) # Note: I'm using dict directly, since super(dict, self) doesn't work. # I'm not sure why, perhaps dict is not a new-style class. def __getitem__(self, key): return dict.__getitem__(self, self.__keytransform__(key)) def __setitem__(self, key, value): return dict.__setitem__(self, self.__keytransform__(key), value) def __delitem__(self, key): return dict.__delitem__(self, self.__keytransform__(key)) def __contains__(self, key): return dict.__contains__(self, self.__keytransform__(key)) class lcdict(arbitrary_dict): def __keytransform__(self, key): return str(key).lower() 

Puede escribir un objeto que se comporte como un dict bastante fácilmente con ABC s (clases base abstractas) desde el módulo de colecciones . Incluso le dice que si perdió un método, a continuación se muestra la versión mínima que cierra el ABC.

 import collections class TransformedDict(collections.MutableMapping): """A dictionary that applies an arbitrary key-altering function before accessing the keys""" def __init__(self, *args, **kwargs): self.store = dict() self.update(dict(*args, **kwargs)) # use the free update to set keys def __getitem__(self, key): return self.store[self.__keytransform__(key)] def __setitem__(self, key, value): self.store[self.__keytransform__(key)] = value def __delitem__(self, key): del self.store[self.__keytransform__(key)] def __iter__(self): return iter(self.store) def __len__(self): return len(self.store) def __keytransform__(self, key): return key 

Obtienes algunos métodos gratuitos del ABC:

 class MyTransformedDict(TransformedDict): def __keytransform__(self, key): return key.lower() s = MyTransformedDict([('Test', 'test')]) assert s.get('TEST') is s['test'] # free get assert 'TeSt' in s # free __contains__ # free setdefault, __eq__, and so on import pickle assert pickle.loads(pickle.dumps(s)) == s # works too since we just use a normal dict 

Yo no haría una subclase de dict (u otros elementos) directamente. A menudo no tiene sentido, porque lo que realmente quiere hacer es implementar la interfaz de un dictado . Y eso es exactamente para lo que son los ABC.

¿Cómo puedo hacer como “perfecto” una subclase de dict como sea posible?

El objective final es tener un dict simple en el que las teclas estén en minúscula.

  • Si __setitem__ __getitem__ / __setitem__ , entonces get / set no funciona. ¿Cómo los hago funcionar? ¿Seguro que no necesito implementarlas individualmente?

  • ¿Estoy evitando que el decapado funcione, y necesito implementar __setstate__ etc.?

  • ¿Necesito repr, update y __init__ ?

  • ¿Debo usar mutablemapping (parece que no se debe usar UserDict o DictMixin )? ¿Si es así, cómo? Los documentos no son exactamente esclarecedores.

La respuesta aceptada sería mi primer enfoque, pero como tiene algunos problemas, y dado que nadie ha abordado la alternativa, en realidad subclasificar un dict , lo haré aquí.

¿Qué hay de malo con la respuesta aceptada?

Esto me parece una petición bastante simple:

¿Cómo puedo hacer como “perfecto” una subclase de dict como sea posible? El objective final es tener un dict simple en el que las teclas estén en minúscula.

La respuesta aceptada no es realmente una subclase de dict , y una prueba para esto falla:

 >>> isinstance(MyTransformedDict([('Test', 'test')]), dict) False 

Idealmente, cualquier código de verificación de tipo estaría probando la interfaz que esperamos, o una clase base abstracta, pero si nuestros objetos de datos se pasan a funciones que están probando dict , y no podemos “arreglar” esas funciones, esto el código fallará

Otras cuestiones que uno podría hacer:

  • A la respuesta aceptada también le falta el método de clase: fromkeys .
  • La respuesta aceptada también tiene un __dict__ redundante, por lo que __dict__ más espacio en la memoria:

     >>> s.foo = 'bar' >>> s.__dict__ {'foo': 'bar', 'store': {'test': 'test'}} 

En realidad subclasificando dict

Podemos reutilizar los métodos dict a través de la herencia. Todo lo que necesitamos hacer es crear una capa de interfaz que garantice que las claves se pasen al dictado en minúsculas si son cadenas.

Si __setitem__ __getitem__ / __setitem__ , entonces get / set no funciona. ¿Cómo los hago funcionar? ¿Seguro que no necesito implementarlas individualmente?

Bueno, implementarlos cada uno individualmente es la desventaja de este enfoque y la ventaja de usar MutableMapping (ver la respuesta aceptada), pero realmente no es mucho más trabajo.

Primero, _RaiseKeyError la diferencia entre Python 2 y 3, creamos un singleton ( _RaiseKeyError ) para asegurarnos de que sabemos si realmente obtenemos un argumento para dict.pop , y creamos una función para asegurar que nuestras claves de cadena estén en minúsculas:

 from itertools import chain try: # Python 2 str_base = basestring items = 'iteritems' except NameError: # Python 3 str_base = str, bytes, bytearray items = 'items' _RaiseKeyError = object() # singleton for no-default behavior def ensure_lower(maybe_str): """dict keys can be any hashable object - only call lower if str""" return maybe_str.lower() if isinstance(maybe_str, str_base) else maybe_str 

Ahora implementamos: estoy usando super con los argumentos completos para que este código funcione para Python 2 y 3:

 class LowerDict(dict): # dicts take a mapping or iterable as their optional first argument __slots__ = () # no __dict__ - that would be redundant @staticmethod # because this doesn't make sense as a global function. def _process_args(mapping=(), **kwargs): if hasattr(mapping, items): mapping = getattr(mapping, items)() return ((ensure_lower(k), v) for k, v in chain(mapping, getattr(kwargs, items)())) def __init__(self, mapping=(), **kwargs): super(LowerDict, self).__init__(self._process_args(mapping, **kwargs)) def __getitem__(self, k): return super(LowerDict, self).__getitem__(ensure_lower(k)) def __setitem__(self, k, v): return super(LowerDict, self).__setitem__(ensure_lower(k), v) def __delitem__(self, k): return super(LowerDict, self).__delitem__(ensure_lower(k)) def get(self, k, default=None): return super(LowerDict, self).get(ensure_lower(k), default) def setdefault(self, k, default=None): return super(LowerDict, self).setdefault(ensure_lower(k), default) def pop(self, k, v=_RaiseKeyError): if v is _RaiseKeyError: return super(LowerDict, self).pop(ensure_lower(k)) return super(LowerDict, self).pop(ensure_lower(k), v) def update(self, mapping=(), **kwargs): super(LowerDict, self).update(self._process_args(mapping, **kwargs)) def __contains__(self, k): return super(LowerDict, self).__contains__(ensure_lower(k)) def copy(self): # don't delegate w/ super - dict.copy() -> dict :( return type(self)(self) @classmethod def fromkeys(cls, keys, v=None): return super(LowerDict, cls).fromkeys((ensure_lower(k) for k in keys), v) def __repr__(self): return '{0}({1})'.format(type(self).__name__, super(LowerDict, self).__repr__()) 

Utilizamos un enfoque de placa de caldera para cualquier método o método especial que haga referencia a una clave, pero de otra manera, por herencia, obtenemos métodos: len , clear , items , keys , popitem y values de forma gratuita. Si bien esto requiere un pensamiento cuidadoso para hacerlo bien, es trivial ver que esto funciona.

(Tenga en cuenta que haskey está en desuso en Python 2, eliminado en Python 3.)

Aquí hay algo de uso:

 >>> ld = LowerDict(dict(foo='bar')) >>> ld['FOO'] 'bar' >>> ld['foo'] 'bar' >>> ld.pop('FoO') 'bar' >>> ld.setdefault('Foo') >>> ld {'foo': None} >>> ld.get('Bar') >>> ld.setdefault('Bar') >>> ld {'bar': None, 'foo': None} >>> ld.popitem() ('bar', None) 

¿Estoy evitando que el decapado funcione, y necesito implementar __setstate__ etc.?

decapado

Y la subclase dict encurtidos muy bien:

 >>> import pickle >>> pickle.dumps(ld) b'\x80\x03c__main__\nLowerDict\nq\x00)\x81q\x01X\x03\x00\x00\x00fooq\x02Ns.' >>> pickle.loads(pickle.dumps(ld)) {'foo': None} >>> type(pickle.loads(pickle.dumps(ld)))  

__repr__

¿Necesito repr, update y __init__ ?

__init__ update e __init__ , pero tienes una __repr__ hermosa por defecto:

 >>> ld # without __repr__ defined for the class, we get this {'foo': None} 

Sin embargo, es bueno escribir una __repr__ para mejorar la capacidad de depuración de su código. La prueba ideal es eval(repr(obj)) == obj . Si es fácil de hacer para su código, lo recomiendo encarecidamente:

 >>> ld = LowerDict({}) >>> eval(repr(ld)) == ld True >>> ld = LowerDict(dict(a=1, b=2, c=3)) >>> eval(repr(ld)) == ld True 

Usted ve, es exactamente lo que necesitamos para recrear un objeto equivalente, esto es algo que podría aparecer en nuestros registros o en registros de retroceso:

 >>> ld LowerDict({'a': 1, 'c': 3, 'b': 2}) 

Conclusión

¿Debo usar mutablemapping (parece que no se debe usar UserDict o DictMixin )? ¿Si es así, cómo? Los documentos no son exactamente esclarecedores.

Sí, estas son algunas líneas más de código, pero pretenden ser integrales. Mi primera inclinación sería utilizar la respuesta aceptada, y si hubiera problemas con ella, vería mi respuesta, ya que es un poco más complicada y no hay ABC que me ayude a configurar correctamente la interfaz.

La optimización prematura va por una mayor complejidad en la búsqueda de rendimiento. MutableMapping es más simple, por lo que obtiene una ventaja inmediata, todo lo demás es igual. Sin embargo, para exponer todas las diferencias, comparemos y contrastemos.

Debo añadir que hubo un impulso para poner un diccionario similar en el módulo de collections , pero fue rechazado . Probablemente deberías hacer esto en su lugar:

 my_dict[transform(key)] 

Debería ser mucho más fácil de depurar.

Comparar y contrastar

Hay 6 funciones de interfaz implementadas con MutableMapping (que falta en las fromkeys ) y 11 con la subclase dict . No necesito implementar __iter__ o __len__ , sino que tengo que implementar get , setdefault , pop , update , copy , __contains__ y fromkeys , pero son bastante triviales, ya que puedo usar la herencia para la mayoría de esas implementaciones.

MutableMapping implementa algunas cosas en Python que dict implementa en C, por lo que esperaría que una subclase dict sea ​​más eficaz en algunos casos.

Obtenemos un __eq__ gratuito en ambos enfoques, los cuales asumen la igualdad solo si otro dictado es todo en minúscula, pero nuevamente, creo que la subclase dict se comparará más rápidamente.

Resumen:

  • la subclasificación MutableMapping es más simple con menos oportunidades de errores, pero más lenta, requiere más memoria (ver dict. redundante) y falla en isinstance(x, dict)
  • La dict subclases dict es más rápida, usa menos memoria y pasa isinstance(x, dict) , pero su implementación es más compleja.

¿Cuál es más perfecto? Eso depende de tu definición de perfecto.

Mis requerimientos eran un poco más estrictos:

  • Tuve que conservar la información del caso (las cadenas son rutas a los archivos que se muestran al usuario, pero es una aplicación de Windows, por lo que internamente todas las operaciones deben ser sensibles a las mayúsculas)
  • Necesitaba que las claves fueran lo más pequeñas posible (marcó una diferencia en el rendimiento de la memoria, recortando 110 mb de 370). Esto significa que el almacenamiento en caché de la versión en minúscula de las claves no es una opción.
  • Necesitaba que la creación de las estructuras de datos fuera lo más rápida posible (una vez más marcó una diferencia en el rendimiento, esta vez la velocidad). Tuve que ir con un builtin

Mi idea inicial fue sustituir nuestra clase de ruta torpe por una subclase Unicode insensible a mayúsculas y minúsculas, pero:

  • Se comprobó que era difícil hacerlo bien. Ver: una clase de cadena insensible a mayúsculas y minúsculas en Python
  • Resulta que el manejo explícito de las claves dict hace que el código sea detallado y desordenado, y propenso a errores (las estructuras se pasan de aquí para allá, y no está claro si tienen instancias CIStr como claves / elementos, fáciles de olvidar más some_dict[CIstr(path)] es feo)

Así que finalmente tuve que escribir ese dictamen insensible al caso. Gracias al código de @AaronHall que se hizo 10 veces más fácil.

 class CIstr(unicode): """See https://stackoverflow.com/a/43122305/281545, especially for inlines""" __slots__ = () # does make a difference in memory performance #--Hash/Compare def __hash__(self): return hash(self.lower()) def __eq__(self, other): if isinstance(other, CIstr): return self.lower() == other.lower() return NotImplemented def __ne__(self, other): if isinstance(other, CIstr): return self.lower() != other.lower() return NotImplemented def __lt__(self, other): if isinstance(other, CIstr): return self.lower() < other.lower() return NotImplemented def __ge__(self, other): if isinstance(other, CIstr): return self.lower() >= other.lower() return NotImplemented def __gt__(self, other): if isinstance(other, CIstr): return self.lower() > other.lower() return NotImplemented def __le__(self, other): if isinstance(other, CIstr): return self.lower() <= other.lower() return NotImplemented #--repr def __repr__(self): return '{0}({1})'.format(type(self).__name__, super(CIstr, self).__repr__()) def _ci_str(maybe_str): """dict keys can be any hashable object - only call CIstr if str""" return CIstr(maybe_str) if isinstance(maybe_str, basestring) else maybe_str class LowerDict(dict): """Dictionary that transforms its keys to CIstr instances. Adapted from: https://stackoverflow.com/a/39375731/281545 """ __slots__ = () # no __dict__ - that would be redundant @staticmethod # because this doesn't make sense as a global function. def _process_args(mapping=(), **kwargs): if hasattr(mapping, 'iteritems'): mapping = getattr(mapping, 'iteritems')() return ((_ci_str(k), v) for k, v in chain(mapping, getattr(kwargs, 'iteritems')())) def __init__(self, mapping=(), **kwargs): # dicts take a mapping or iterable as their optional first argument super(LowerDict, self).__init__(self._process_args(mapping, **kwargs)) def __getitem__(self, k): return super(LowerDict, self).__getitem__(_ci_str(k)) def __setitem__(self, k, v): return super(LowerDict, self).__setitem__(_ci_str(k), v) def __delitem__(self, k): return super(LowerDict, self).__delitem__(_ci_str(k)) def copy(self): # don't delegate w/ super - dict.copy() -> dict :( return type(self)(self) def get(self, k, default=None): return super(LowerDict, self).get(_ci_str(k), default) def setdefault(self, k, default=None): return super(LowerDict, self).setdefault(_ci_str(k), default) __no_default = object() def pop(self, k, v=__no_default): if v is LowerDict.__no_default: # super will raise KeyError if no default and key does not exist return super(LowerDict, self).pop(_ci_str(k)) return super(LowerDict, self).pop(_ci_str(k), v) def update(self, mapping=(), **kwargs): super(LowerDict, self).update(self._process_args(mapping, **kwargs)) def __contains__(self, k): return super(LowerDict, self).__contains__(_ci_str(k)) @classmethod def fromkeys(cls, keys, v=None): return super(LowerDict, cls).fromkeys((_ci_str(k) for k in keys), v) def __repr__(self): return '{0}({1})'.format(type(self).__name__, super(LowerDict, self).__repr__()) 

Implícito contra explícito sigue siendo un problema, pero una vez que el polvo se asienta, cambiar el nombre de los atributos / variables para comenzar con ci (y un gran comentario doc que explica que ci significa que no distingue entre mayúsculas y minúsculas) Creo que es una solución perfecta, ya que los lectores del código deben ser plenamente conscientes de que estamos tratando con estructuras de datos subyacentes que no distinguen entre mayúsculas y minúsculas. Esperamos que esto solucione algunos errores difíciles de reproducir, que sospecho que se reducen a la sensibilidad a las mayúsculas.

Comentarios / correcciones bienvenidos 🙂

Todo lo que tendrás que hacer es

 class BatchCollection(dict): def __init__(self, *args, **kwargs): dict.__init__(*args, **kwargs) 

O

 class BatchCollection(dict): def __init__(self, inpt={}): super(BatchCollection, self).__init__(inpt) 

Una muestra de uso para mi uso personal.

 ### EXAMPLE class BatchCollection(dict): def __init__(self, inpt={}): dict.__init__(*args, **kwargs) def __setitem__(self, key, item): if (isinstance(key, tuple) and len(key) == 2 and isinstance(item, collections.Iterable)): # self.__dict__[key] = item super(BatchCollection, self).__setitem__(key, item) else: raise Exception( "Valid key should be a tuple (database_name, table_name) " "and value should be iterable") 

Nota : probado solo en python3

Después de probar las dos sugerencias principales, me decidí por una ruta intermedia sombría para Python 2.7. Tal vez 3 sea más sensato, pero para mí:

 class MyDict(MutableMapping): # ... the few __methods__ that mutablemapping requires # and then this monstrosity @classmethod def __class__(cls): return dict 

que realmente odio, pero parece que se ajusta a mis necesidades, que son:

  • puede anular **my_dict
    • Si heredas de dict , esto pasa por alto tu código . Pruébalo.
    • esto hace que el # 2 sea inaceptable para mí en todo momento , ya que es bastante común en el código python
  • se enmascara como isinstance(my_dict, dict)
    • descarta MutableMapping solo, así que # 1 no es suficiente
    • Recomiendo encarecidamente el # 1 si no necesitas esto, es simple y predecible
  • comportamiento totalmente controlable
    • así que no puedo heredar de dict

Si necesita diferenciarse de los demás, personalmente uso algo como esto (aunque recomendaría mejores nombres):

 def __am_i_me(self): return True @classmethod def __is_it_me(cls, other): try: return other.__am_i_me() except Exception: return False 

Siempre y cuando solo necesites reconocerte a ti mismo internamente, de esta manera es más difícil llamar a __am_i_me accidentalmente debido a que el nombre de Python se entretiene (se le cambia el nombre a _MyDict__am_i_me desde cualquier lugar que llame fuera de esta clase). Un poco más privado que el _method , tanto en la práctica como culturalmente.

Hasta ahora no tengo ninguna queja, aparte de la anulación de __class__ apariencia __class__ . Me encantaría saber de cualquier problema que otros encuentren con esto, sin embargo, no entiendo completamente las consecuencias. Pero hasta ahora no he tenido ningún problema, y ​​esto me permitió migrar una gran cantidad de códigos de calidad intermedia en muchas ubicaciones sin necesidad de ningún cambio.


Como evidencia: https://repl.it/repls/TraumaticToughCockatoo

Básicamente: copie la opción # 2 actual , agregue líneas de print 'method_name' a cada método, y luego intente esto y observe la salida:

 d = LowerDict() # prints "init", or whatever your print statement said print '------' splatted = dict(**d) # note that there are no prints here 

Verás un comportamiento similar para otros escenarios. Digamos que su falso dict es un envoltorio alrededor de algún otro tipo de datos, por lo que no hay una manera razonable de almacenar los datos en el dictado de respaldo; **your_dict estará vacío, independientemente de lo que haga cualquier otro método.

Esto funciona correctamente para MutableMapping , pero tan pronto como se hereda de dict se convierte en incontrolable.