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__
, entoncesget
/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
oDictMixin
)? ¿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 usarUserDict
oDictMixin
)? ¿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 usarUserDict
oDictMixin
)? ¿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 fallaisinstance(x, dict)
- La subclassing
dict
es más rápida, utiliza menos memoria y pasaisinstance(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
- si heredas de
- 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
- entonces no puedo heredar de
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 ( sí 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