¿Cómo anular "perfectamente" un dict?

218

¿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.

Parece que debería haber un pequeño conjunto de primitivas que puedo anular para que esto funcione, pero de acuerdo con todas mis investigaciones e intentos, parece que este no es el caso:

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

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

  • Cómo me necesito repr, updatey__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 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 
    # /programming/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()
Paul Biggar
fuente
Creo que __keytransform __ () debería ser estático. Buen enfoque sin embargo. (@staticmethod anteponiendo)
Aiyion.Prime

Respuestas:

229

Puede escribir un objeto que se comporte con dictbastante facilidad con ABC s (clases base abstractas) desde el collections.abcmódulo. Incluso te dice si te perdiste un método, así que a continuación se muestra la versión mínima que cierra el ABC.

from collections.abc import MutableMapping


class TransformedDict(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

Obtiene 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
# works too since we just use a normal dict
assert pickle.loads(pickle.dumps(s)) == s

No subclase dict(u otras construcciones) directamente. A menudo no tiene sentido, porque lo que realmente quieres hacer es implementar la interfaz de adict . Y para eso es exactamente el ABC.

Jochen Ritzel
fuente
46
Sugeriría cambiar el nombre __keytransform__()porque viola la guía de estilo PEP 8 que aconseja "Nunca invente tales nombres; solo utilícelos como se documenta" al final de la sección Descriptivo: Nomenclatura de estilos .
Martineau
1
Sin embargo, la pregunta: ¿la implementación de esta interfaz con un tipo definido por el usuario generalmente dará como resultado operaciones más lentas como dict que usar el tipo incorporado?
twneale
2
¿Hay alguna manera de hacer esto para que sea instancia (_, dict) == Verdadero? ¿O simplemente usa Mutable Mapping para construir la subclase?
Andy Hayden
55
@AndyHayden: Deberías escribir if isinstance(t, collections.MutableMapping): print t, "can be used like a dict". No verifique el tipo de objeto, verifique la interfaz.
Jochen Ritzel
2
@NeilG Esto desafortunadamente incluye el JSONEncoder en la biblioteca estándar de python - github.com/python-git/python/blob/…
Andy Smith
97

¿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.popy 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 superlos 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 valuesde forma gratuita. Si bien esto requirió un pensamiento cuidadoso para hacerlo bien, es trivial ver que esto funciona.

(Tenga en cuenta que haskeyfue 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 updatey __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. MutableMappinges 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 collectionsmó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 dictsubclase. 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 MutableMappingimplementos algunas cosas en Python que dictimplementa en C - por lo que se puede esperar de una dictsubclase 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 dictsubclase se comparará más rápidamente.

Resumen:

  • La subclasificación MutableMappinges 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 dictes 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.

Aaron Hall
fuente
¿Cómo eliminaría la respuesta aceptada el dict redundante?
Seanny123
1
Dos formas que inmediatamente vienen a la mente son declarar el atributo de la tienda __slots__o quizás reutilizarlo __dict__como la tienda, pero eso mezcla la semántica, otro punto potencial de crítica.
Aaron Hall
1
¿No hubiera sido más fácil escribir un decorador que tome un método y use su ensure_lowerprimer argumento (que siempre es la clave)? Entonces sería el mismo número de anulaciones, pero todas tendrían la forma __getitem__ = ensure_lower_decorator(super(LowerDict, self).__getitem__).
Graipher
1
Gracias por esto, recibiendo advertencias para pop y fromkeys de que no coinciden con la firma del método de clase base.
Mr_and_Mrs_D
1
@Mr_and_Mrs_D Agregué una implementación de copy- Creo que debería hacerlo, ¿no? Creo que debería probar la interfaz, por ejemplo, el objeto Pandas DataFrame no es una instancia de Mapping (en la última comprobación) pero tiene elementos / iteritems.
Aaron Hall
4

Mis requisitos 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 insensibles a las mayúsculas y minúsculas)
  • Necesitaba claves para ser tan pequeño como sea posible (que hizo hacer una diferencia en el rendimiento de memoria, cortado 110 mb de 370). Esto significaba que el almacenamiento en caché de las versiones 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 hizo una diferencia en el rendimiento, la velocidad esta vez). Tuve que ir con un builtin incorporado

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

  • resultó difícil hacerlo bien - vea: una clase de cadena que no distingue entre mayúsculas y minúsculas en Python
  • Resulta que el manejo explícito de claves dict hace que el código sea detallado y desordenado, y propenso a errores (las estructuras se pasan de un lado a otro, y no está claro si tienen instancias CIStr como claves / elementos, es fácil de olvidar y some_dict[CIstr(path)]es feo)

Así que finalmente tuve que escribir ese dict insensible a mayúsculas y minúsculas. 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__())

Lo implícito frente a lo explícito sigue siendo un problema, pero una vez que el polvo se asienta, el cambio de nombre de los atributos / variables para comenzar con ci (y un gran comentario de documentación 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 Tenga en cuenta que estamos tratando con estructuras de datos subyacentes que no distinguen entre mayúsculas y minúsculas. Con suerte, esto solucionará algunos errores difíciles de reproducir, que sospecho se reducen a mayúsculas y minúsculas.

Comentarios / correcciones bienvenidos :)

Mr_and_Mrs_D
fuente
Los CIstr __repr__deberían usar la clase padre __repr__para pasar la prueba eval (repr (obj)) == obj (no creo que lo haga ahora) y no confiar en ella __str__.
Aaron Hall
También echa un vistazo al total_orderingdecorador de clases , que eliminará 4 métodos de tu subclase Unicode. Pero la subclase dict parece implementada de manera muy inteligente. : P
Aaron Hall
Gracias @AaronHall - eres tú quien implementó eso: P Re: pedido total - Escribí intencionalmente los métodos en línea según lo aconsejado por Raymond Hettinger aquí: stackoverflow.com/a/43122305/281545 . Re: repr: Recuerdo haber leído un comentario (por algún desarrollador central IIRC) que bueno, realmente no vale la pena tratar de hacer que repr pase esa prueba (es una molestia), mejor concéntrate en que sea lo más informativo posible ( pero no más)
Mr_and_Mrs_D
Le permitiré sus métodos de comparación redundantes (debe hacer una nota al respecto en su respuesta), pero CIstr.__repr__, en su caso, puede pasar la prueba de repr con muy poca molestia, y debería hacer que la depuración sea mucho más agradable. También agregaría un __repr__para su dict. Lo haré en mi respuesta para demostrar.
Aaron Hall
@AaronHall: agregué __slots__en CIstr: hace una diferencia en el rendimiento (CIstr no está destinado a ser subclasificado o, de hecho, debe usarse fuera de LowerDict, debería ser una clase final anidada estática). Todavía no estoy seguro de cómo resolver con elegancia el problema de repr (la picadura puede contener una combinación de 'y "citas)
Mr_and_Mrs_D
4

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

ravi404
fuente
3

Después de probar los dos principales dos sugerencias, me he decidido por una ruta central de aspecto sombrío para Python 2.7. Quizás 3 es más sano, pero para mí:

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

que realmente odio, pero parece satisfacer mis necesidades, que son:

  • puede anular **my_dict
    • si hereda de dict, esto omite su código . Pruébalo.
    • esto hace que # 2 sea inaceptable para mí en todo momento , ya que esto es bastante común en el código de Python
  • disfraces de isinstance(my_dict, dict)
    • descarta MutableMapping solo, por lo que # 1 no es suficiente
    • Recomiendo encarecidamente el n . ° 1 si no necesita 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

Mientras solo necesite reconocerse internamente, de esta manera es más difícil llamar accidentalmente __am_i_me debido al cambio de nombre de Python (esto se renombra _MyDict__am_i_mede cualquier cosa que llame fuera de esta clase). Ligeramente más privado que _methods, tanto en la práctica como culturalmente.

Hasta el momento no tengo quejas, aparte de la anulación seriamente sombría __class__. sería encantada de oír de cualquier problema que otros encuentran con esto, sin embargo, no entiendo plenamente las consecuencias. Pero hasta ahora no he tenido ningún problema, y ​​esto me permitió migrar una gran cantidad de código de calidad media en muchas ubicaciones sin necesidad de ningún cambio.


Como evidencia: 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 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. Di tu falsodict es una envoltura alrededor de algún otro tipo de datos, por lo que no hay una forma razonable de almacenar los datos en el dictado de respaldo; **your_dictestará vacío, independientemente de lo que haga cualquier otro método.

Esto funciona correctamente para MutableMapping, pero tan pronto como herede dedict él se vuelve incontrolable.


Editar: como una actualización, esto se ha estado ejecutando sin un solo problema durante casi dos años, en varios cientos de miles (eh, podrían ser un par de millones) de líneas de python complicado y heredado. Así que estoy bastante feliz con eso :)

Edición 2: aparentemente copié mal esto o algo hace mucho tiempo. @classmethod __class__no funciona para isinstancecheques - @property __class__sí: https://repl.it/repls/UnitedScientificSequence

Groxx
fuente
¿Qué quieres decir exactamente con " **your_dictestará vacío" (si subclase de dict)? No he visto ningún problema con el desempaquetado de dict ...
Matt P
Si realmente coloca datos en el dict primario (como lo hace LowerDict), funciona: obtendrá esos datos almacenados en dict. Si no lo hace (digamos que desea generar datos sobre la marcha, como {access_count: "stack trace of access"} que se completa cada vez que se lee), notará que **your_dictno ejecuta su código, por lo que no puede mostrar nada "especial". Por ejemplo, no puede contar "lecturas" porque no ejecuta su código de recuento de lectura. MutableMapping hace el trabajo para esto (use si usted puede!), Pero no isinstance(..., dict)así que no podía usarlo. yay software heredado.
Groxx
Ok, ahora entiendo a qué te refieres. Supongo que no esperaba la ejecución del código **your_dict, pero me parece muy interesante que MutableMappinglo haga.
Matt P
Sí. Es necesario para una serie de cosas (p. Ej., Estaba cambiando las llamadas RPC a lo que solía ser una lectura local-dict, y tuve que hacerlo a pedido por Reasons ™), y parece que muy pocas personas lo saben, incluso aunque **some_dictEs bastante común. Como mínimo, ocurre con mucha frecuencia en los decoradores, por lo que si tiene alguno , corre el riesgo inmediato de un comportamiento aparentemente imposible si no lo tiene en cuenta.
Groxx
Quizás me falta algo, pero el def __class__()truco no parece funcionar con Python 2 o 3, al menos para el código de ejemplo en la pregunta ¿Cómo registrar la implementación de abc.MutableMapping como una subclase dict? (modificado para funcionar de otra manera en las dos versiones). Quiero isinstance(SpreadSheet(), dict)volver True.
Martineau