¿Cómo puedo hacer una subclase de dict tan "perfecta" como sea posible?
El objetivo final es tener un dict simple en el que las teclas están en minúsculas.
Si anulo __getitem__
/ __setitem__
, entonces get / set no funciona. ¿Cómo los hago funcionar? ¿Seguramente no necesito implementarlos individualmente?
¿Estoy evitando que funcione el decapado y necesito implementar,
__setstate__
etc.?
¿Necesito repr, actualización y __init__
?
¿Debo 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 dado que tiene algunos problemas, y dado que nadie ha abordado la alternativa, en realidad subclasificando a dict
, voy a hacer eso aquí.
¿Qué hay de malo con la respuesta aceptada?
Esto me parece una solicitud bastante simple:
¿Cómo puedo hacer una subclase de dict tan "perfecta" como sea posible? El objetivo final es tener un dict simple en el que las teclas están en minúsculas.
La respuesta aceptada en realidad no es una subclase 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, este código fallará.
Otras objeciones que uno podría hacer:
- La respuesta aceptada también falta la classmethod:
fromkeys
.
La respuesta aceptada también tiene redundancia __dict__
, por lo tanto, ocupa más espacio en la memoria:
>>> s.foo = 'bar'
>>> s.__dict__
{'foo': 'bar', 'store': {'test': 'test'}}
Realmente subclases 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 dict en minúsculas si son cadenas.
Si anulo __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 usarlo MutableMapping
(ver la respuesta aceptada), pero en realidad no es mucho más trabajo.
Primero, factoricemos la diferencia entre Python 2 y 3, cree un singleton ( _RaiseKeyError
) para asegurarnos de saber si realmente tenemos un argumento dict.pop
y creemos una función para garantizar que nuestras teclas 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
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 caldera de la placa por cualquier método o método especial que hace referencia a una clave, pero por lo demás, por herencia, obtenemos métodos: len
, clear
, items
, keys
, popitem
, y values
de forma gratuita. Si bien esto requirió un pensamiento cuidadoso para hacerlo bien, es trivial ver que esto funciona.
(Tenga en cuenta que haskey
fue obsoleto en Python 2, eliminado en Python 3.)
Aquí hay algunos usos:
>>> 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 funcione el decapado y necesito implementar,
__setstate__
etc.?
decapado
Y la dict subclase encurtidos 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, actualización y __init__
?
Definimos update
y __init__
, pero tienes una hermosa __repr__
por defecto:
>>> ld # without __repr__ defined for the class, we get this
{'foo': None}
Sin embargo, es bueno escribir un __repr__
para mejorar la 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
Verá, es exactamente lo que necesitamos para recrear un objeto equivalente; esto es algo que podría aparecer en nuestros registros o en las trazas inversas:
>>> ld
LowerDict({'a': 1, 'c': 3, 'b': 2})
Conclusión
¿Debo 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 usar la respuesta aceptada, y si hubiera problemas con ella, entonces miraría mi respuesta, ya que es un poco más complicado y no hay un ABC que me ayude a tener mi interfaz correcta.
La optimización prematura busca 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 agregar que hubo un impulso para poner un diccionario similar en el collections
módulo, pero fue rechazado . Probablemente deberías hacer esto en su lugar:
my_dict[transform(key)]
Debería ser mucho más fácilmente debugable.
Comparar y contrastar
Hay 6 funciones de interfaz implementadas con MutableMapping
(que falta fromkeys
) y 11 con la dict
subclase. No necesitará implementar __iter__
o __len__
, pero en lugar de eso tiene que aplicar get
, setdefault
, pop
, update
, copy
, __contains__
, y fromkeys
- pero estos son bastante trivial, ya que puedo utilizar la herencia para la mayoría de las implementaciones.
Los MutableMapping
implementos algunas cosas en Python que dict
implementa en C - por lo que se puede esperar de una dict
subclase sea más performante en algunos casos.
Obtenemos una libertad __eq__
en ambos enfoques, los cuales asumen la igualdad solo si otro dict es todo en minúsculas, pero nuevamente, creo que la dict
subclase se comparará más rápidamente.
Resumen:
- La subclasificación
MutableMapping
es más simple, con menos oportunidades para errores, pero más lenta, requiere más memoria (ver dict redundante) y fallaisinstance(x, dict)
- La subclase
dict
es más rápida, usa 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.