python inheritance dictionary get set

python - ¿Cómo "anular" perfectamente un dict?



inheritance dictionary (5)

¿Cómo puedo hacer que una subclase de dict sea ​​"perfecta" como sea posible? El objetivo final es tener un dict simple en el que las teclas sean minúsculas.

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

  • Si __setitem__ __getitem__ / __setitem__ , entonces get / set no funciona. ¿Cómo puedo hacer que funcionen? Seguramente no necesito implementarlos individualmente?

  • ¿Estoy impidiendo que el decapado funcione? ¿Debo implementar __setstate__ etc.?

  • ¿ Necesito repr , update y __init__ ?

  • ¿Debería usar mutablemapping (parece que uno no debería usar UserDict o DictMixin )? ¿Si es así, cómo? Los documentos no son exactamente esclarecedores.

Aquí está mi primer intento, get() no funciona y sin dudas 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()


¿Cómo puedo hacer que una subclase de dict sea "perfecta" como sea posible?

El objetivo final es tener un dict simple en el que las teclas sean minúsculas.

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

  • ¿Estoy impidiendo que el decapado funcione? ¿Debo implementar __setstate__ etc.?

  • ¿Necesito repr, update y __init__ ?

  • ¿Debería usar mutablemapping (parece que uno no debería 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 como nadie ha abordado la alternativa, en realidad subclases de un dict , voy a hacer eso aquí.

¿Qué pasa con la respuesta aceptada?

Esto me parece una solicitud bastante simple:

¿Cómo puedo hacer que una subclase de dict sea "perfecta" como sea posible? El objetivo final es tener un dict simple en el que las teclas sean minúsculas.

La respuesta aceptada no es 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 el dict , y no podemos "arreglar" esas funciones, este el código fallará

Otras objeciones que uno podría hacer:

  • A la respuesta aceptada también le falta el método class: 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 subclasing dict

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

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

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

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

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 casi de placa de caldera para cualquier método o método especial que haga referencia a una clave, pero de lo contrario, por herencia, obtenemos métodos: len , clear , items , keys , popitem y values de forma gratuita. Si bien esto requirió una reflexión cuidadosa para hacer las cosas bien, es trivial ver que esto funciona.

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

Aquí hay un poco 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 impidiendo que el decapado funcione? ¿Debo implementar __setstate__ etc.?

decapado

Y la subclase dict salmuera 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))) <class ''__main__.LowerDict''>

__repr__

¿Necesito repr, update y __init__ ?

__init__ update y __init__ , pero tiene un __repr__ precioso por defecto:

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

Sin embargo, es bueno escribir un __repr__ para mejorar la capacidad de búsqueda de tu código. La prueba ideal es eval(repr(obj)) == obj . Si es fácil de hacer por 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

Verá, es exactamente lo que necesitamos para recrear un objeto equivalente: esto es algo que podría aparecer en nuestros registros o en trazas inversas:

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

Conclusión

¿Debería usar mutablemapping (parece que uno no debería 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 están destinadas a ser exhaustivas. Mi primera inclinación sería utilizar la respuesta aceptada, y si hubiera problemas con ella, luego miraría mi respuesta, ya que es un poco más complicada, y no hay ABC que me ayude a configurar mi interfaz correctamente.

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

Debo agregar 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 debbieble.

Comparar y contrastar

Hay 6 funciones de interfaz implementadas con MutableMapping (que falta de las fromkeys ) y 11 con la subclase dict . No necesito implementar __iter__ o __len__ , pero en su lugar 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.

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

Obtenemos un __eq__ libre en ambos enfoques, ambos asumen igualdad solo si otro dict es todo en minúscula, pero de nuevo, creo que la subclase dict se comparará más rápidamente.

Resumen:

  • La subclasificación de MutableMapping es más simple con menos oportunidades para errores, pero más lenta, requiere más memoria (vea dict redundante) y falla isinstance(x, dict)
  • La subclassing dict es más rápida, utiliza menos memoria y pasa isinstance(x, dict) , pero tiene una mayor complejidad para implementar.

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


Después de probar las two top sugerencias, me he decidido por una ruta intermedia de aspecto sombrío para Python 2.7. Tal vez 3 es mejor, pero para mí:

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

lo cual realmente odio, pero parece ajustarse 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 two inaceptable para mí todo el tiempo , ya que esto es bastante común en el código python
  • se enmascara como isinstance(my_dict, dict)
    • descarta MutableMapping solo, por lo que el top no es suficiente
    • Recomiendo sinceramente el top si no lo necesitas, es simple y predecible
  • comportamiento completamente controlable
    • entonces no puedo heredar de dict

Si necesita distinguirse 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 internamente, de esta manera es más difícil llamar __am_i_me accidentalmente debido al nombre de python-munging (esto se renombra a _MyDict__am_i_me de cualquier cosa que llame fuera de esta clase). Un poco más privado que los _method , tanto en la práctica como culturalmente.

Hasta el momento no tengo quejas, aparte de la __class__ aspecto __class__ . Sin embargo, me encantaría escuchar cualquier problema que otros encuentren con esto, no entiendo completamente las consecuencias. Pero hasta ahora no he tenido ningún problema, y ​​esto me permitió migrar muchos códigos de calidad mediana en muchos lugares sin necesidad de cambios.

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

Básicamente: copie two , agregue líneas de print ''method_name'' a cada método, y luego intente esto y observe el resultado:

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

Verá un comportamiento similar para otros escenarios. Digamos que su falso es un envoltorio alrededor de otro tipo de datos, por lo que no hay una forma razonable de almacenar los datos en el dict 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 heredes de dict se vuelve incontrolable.


Mis requisitos eran un poco más estrictos:

  • Tuve que retener 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 insensibles a mayúsculas y minúsculas)
  • Necesitaba claves para ser lo más pequeño posible ( que hizo una diferencia en el rendimiento de la memoria, cortó 110 mb de 370). Esto significaba que el almacenamiento en caché de la versión en minúsculas 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 se hizo una diferencia en el rendimiento, la velocidad esta vez). Tenía que ir con un builtin

Mi pensamiento inicial fue sustituir nuestra clase de ruta de acceso poco común para una subclase Unicode insensible a mayúsculas y minúsculas, pero:

  • demostró ser difícil de entender bien - ver: una clase de cadena insensible a mayúsculas y minúsculas en python
  • Resulta que el manejo explícito de claves dict hace que el código sea prolijo 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 caso insensible. Gracias al code de @AaronHall que se hizo 10 veces más fácil.

class CIstr(unicode): """See https://.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://.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 vs explícito sigue siendo un problema, pero una vez que el polvo se asienta, el cambio de nombre de atributos / variables para comenzar con ci (y un gran comentario de doc explicando que ci no distingue entre mayúsculas y minúsculas) creo que es una solución perfecta, ya que los lectores del código deben tenga en cuenta que estamos tratando con estructuras de datos subyacentes insensibles a mayúsculas y minúsculas. Espero que esto solucione algunos errores difíciles de reproducir, que sospecho se reducen a la sensibilidad de mayúsculas y minúsculas.

Comentarios / correcciones bienvenidas :)


Puede escribir un objeto que se comporta como un dict con bastante facilidad con ABC s (clases base abstractas) del módulo de collections . Incluso te dice si te perdiste un método, así que debajo está 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

No sometería a subclase dict (u otros builtins) directamente. A menudo no tiene sentido, porque lo que realmente quieres hacer es implementar la interfaz de un dict . Y eso es exactamente para lo que son los ABC.


Todo lo que debes 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)

Un uso de muestra 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