Cómo "perfectamente" anular un dict?


¿Cómo puedo hacer tan "perfecto" una subclase de dict como sea posible? El objetivo final es tener un dict simple en el que las claves estén en minúsculas.

Parecería que debería haber algún pequeño conjunto de primitivas que pueda anular para hacer que esto funcione, pero de acuerdo con toda mi investigación e intentos, parece que este no es el caso:{[17]]}

  • If I override __getitem__/__setitem__, entonces get/set no trabajes. ¿Cómo puedo hacer que funcionen? Seguro que no necesito ¿implementarlos individualmente?

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

  • ¿Necesito repr, update y __init__?

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

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

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()
Author: Bonifacio2, 2010-08-02

5 answers

Puede escribir un objeto que se comporte como un dict con bastante facilidad con ABC s (Clases Base abstractas) del módulo collections. Incluso te dice si ha perdido un método, por lo que 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 libres 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 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 están los ABC.

 171
Author: Jochen Ritzel,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-10-02 11:44:48

¿Cómo puedo hacer lo más "perfecto" posible una subclase de dict?

El objetivo final es tener un dicto simple en el que las claves estén en minúsculas.

  • Si anulo __getitem__/__setitem__, entonces get / set no funciona. Cuan ¿hago que funcionen? Seguramente no necesito implementarlos ¿individualmente?

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

  • Necesito repr, actualizar y __init__?

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

La respuesta aceptada sería mi primer enfoque, pero ya que tiene algunos problemas, y ya que nadie ha abordado la alternativa, en realidad subclase a dict, voy a hacer eso aquí.

¿Qué hay de malo en la respuesta aceptada?

Esto parece una petición bastante simple para me:

¿Cómo puedo hacer lo más "perfecto" posible una subclase de dict? El objetivo final es tener un dicto simple en el que las teclas estén en minúsculas.

La respuesta aceptada en realidad no subclase dict, y una prueba para esto falla:

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

Idealmente, cualquier código de comprobación de tipos 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" esos funciones, este código fallará.

Otras objeciones que uno podría hacer: {[74]]}

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

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

En realidad subclase 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 diccionario en minúsculas forma si son cadenas.

Si anulo __getitem__/__setitem__, entonces get / set no funciona. ¿Cómo hago que funcionen? 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 realmente no es mucho más trabajo.

Primero, vamos a factorizar la diferencia entre Python 2 y 3, crear un singleton (_RaiseKeyError) para asegurarnos de que sabemos si realmente obtenemos un argumento a dict.pop, y creamos una función para garantizar 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 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 gratis. Mientras que esto requirió un pensamiento cuidadoso para ve bien, es trivial ver que esto funciona.

(Tenga en cuenta que haskey fue obsoleto en Python 2, eliminado en Python 3.)

He aquí 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 el decapado funcione, y necesito implementar __setstate__ etc?

Decapado

Y los pepinillos de la subclase dict 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__?

Definimos update y __init__, pero tener un hermoso __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 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

Verás, es exactamente lo que necesitamos para recrear un objeto equivalente - esto es algo que podría aparecer en nuestros registros o en retrotracciones:{[74]]}

>>> 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 médicos no son exactamente esclarecedores.

Sí, estas son algunas líneas más de código, pero están destinadas a ser completas. Mi primera inclinación sería utilizar la respuesta aceptada, y si hubiera problemas con él, entonces miraría mi respuesta, ya que es un poco más complicado, y no hay ABC que me ayude a obtener mi interfaz correcta.

La optimización prematura va hacia una mayor complejidad en busca de rendimiento. MutableMapping es más simple - por lo que obtiene una ventaja inmediata, todo lo demás igual. Sin embargo, para establecer todas las diferencias, vamos a comparar y contrastar.

Debo añadir que hubo un empujón para poner un diccionario similar en el módulo 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 el MutableMapping (que falta 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 estos 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 esperaría que una subclase dict tenga más rendimiento en algunos casos.

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

Resumen:

  • la subclase MutableMapping es más simple con menos oportunidades para errores, pero más lenta, toma más memoria (ver dict redundante), y falla isinstance(x, dict)
  • la subclase dict es más rápida, usa menos memoria y pasa isinstance(x, dict), pero tiene mayor complejidad de implementar.

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

 58
Author: Aaron Hall,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-07-23 12:45:18

Mis requisitos eran un poco más estrictos:

  • Tuve que conservar la información de mayúsculas y minúsculas (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 que las teclas fueran lo más pequeñas posible ( 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 la creación de las estructuras de datos para ser tan rápido como posible (de nuevo hizo una diferencia en el rendimiento, la velocidad esta vez). Tuve que ir con un builtin

Mi pensamiento inicial fue sustituir nuestra clase Path torpe por una subclase unicode insensible a mayúsculas y minúsculas - pero:

  • resultó 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 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 tener instancias CIStr como claves / elementos, fácil de olvidar más {[1] } es feo)

Así que finalmente tuve que escribir ese dictado insensible a mayúsculas y minúsculas. Gracias a code de @AaronHall, esto 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__())

Implicit vs explicit 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 que explica que ci significa mayúsculas y minúsculas) creo que es una solución perfecta, ya que los lectores del código deben consciente de que estamos tratando con estructuras de datos subyacentes insensibles a mayúsculas y minúsculas. Con suerte, esto solucionará algunos errores difíciles de reproducir, que sospecho que se reducen a la sensibilidad de mayúsculas y minúsculas.

Comentarios / correcciones bienvenidos:)

 2
Author: Mr_and_Mrs_D,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:26:19

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

Un ejemplo 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

 1
Author: ravi404,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-10-06 08:06:06

Después de probar ambos arriba dos sugerencias, me he establecido en una ruta intermedia de aspecto sombrío para Python 2.7. Tal vez 3 es más sano, 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:{[21]]}

  • puede anular **my_dict
    • si usted hereda de dict, esto omite su código . pruébalo.
    • esto hace #2 inaceptable para mí en todo momento , ya que esto es bastante común en código python
  • se hace pasar por isinstance(my_dict, dict)
    • descarta el mapeo mutable solo, por lo que #1 no es suficiente
    • Lo recomiendo de todo corazón#1 si usted no necesita esto, es simple y predecible
  • comportamiento totalmente controlable
    • así que no puedo heredar de dict

Si necesita distinguirse de los demás, personalmente uso algo como esto (aunque lo recomendaría mejor 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

Mientras solo necesite reconocerse internamente, de esta manera es más difícil llamar accidentalmente a __am_i_me debido al munging de nombres de python (esto se cambia a _MyDict__am_i_me de cualquier llamada fuera de esta clase). Un poco más privado que _methods, tanto en la práctica como culturalmente.

Hasta ahora no tengo ninguna queja, aparte de la seriamente sombría __class__ anulación. Yo estaría emocionado {[29] } para escuchar de cualquier problema que otros encuentran con esto, sin embargo, yo no entiendo completamente las consecuencias. Pero hasta ahora no he tenido ningún problema, y esto me permitió migrar una gran cantidad de código de calidad intermedia en muchas ubicaciones sin necesidad de ningún cambio.


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

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

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 fake - dict es un contenedor alrededor de otro tipo de datos, por lo que no hay una manera razonable de almacenar los datos en el diccionario 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 vuelve incontrolable.

 0
Author: Groxx,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-11-18 02:01:07