Cómo subclasificar correctamente dict y anular __getitem__ y __setitem__

84

Estoy depurando un código y quiero saber cuándo se accede a un diccionario en particular. Bueno, en realidad es una clase que es una subclase dicte implementa un par de características adicionales. De todos modos, lo que me gustaría hacer es subclasificarme dicty agregar override __getitem__y __setitem__producir una salida de depuración. Ahora mismo, tengo

class DictWatch(dict):
    def __init__(self, *args):
        dict.__init__(self, args)

    def __getitem__(self, key):
        val = dict.__getitem__(self, key)
        log.info("GET %s['%s'] = %s" % str(dict.get(self, 'name_label')), str(key), str(val)))
        return val

    def __setitem__(self, key, val):
        log.info("SET %s['%s'] = %s" % str(dict.get(self, 'name_label')), str(key), str(val)))
        dict.__setitem__(self, key, val)

' name_label'es una clave que eventualmente se establecerá y que quiero usar para identificar la salida. Luego cambié la clase que estoy instrumentando a subclase en DictWatchlugar de dicty cambié la llamada al superconstructor. Aún así, parece que no pasa nada. Pensé que estaba siendo inteligente, pero me pregunto si debería ir en una dirección diferente.

¡Gracias por la ayuda!

Michael Mior
fuente
¿Intentaste usar imprimir en lugar de registrar? Además, ¿podría explicar cómo crea / configura su registro?
pajton
2
¿No dict.__init__toma *args?
Tom Russell
4
Parece un buen candidato para decorador.
Tom Russell

Respuestas:

39

Lo que estás haciendo debería funcionar absolutamente. Probé tu clase y, aparte de un paréntesis de apertura que falta en tus declaraciones de registro, funciona bien. Solo puedo pensar en dos cosas. Primero, ¿la salida de su declaración de registro está configurada correctamente? Es posible que deba poner un logging.basicConfig(level=logging.DEBUG)al principio de su secuencia de comandos.

En segundo lugar, __getitem__y __setitem__solo se llaman durante los []accesos. Así que asegúrese de acceder solo a DictWatchtravés de d[key], en lugar de d.get()yd.set()

BrainCore
fuente
En realidad, no se trata de pares extra, sino de un par de apertura que falta alrededor(str(dict.get(self, 'name_label')), str(key), str(val)))
cobbal
3
Cierto. Al OP: Para referencia futura, simplemente puede hacer log.info ('% s% s% s', a, b, c), en lugar de un operador de formato de cadena de Python.
BrainCore
El nivel de registro terminó siendo el problema. Estoy depurando el código de otra persona y originalmente estaba probando en otro archivo que encabeza un nivel diferente de conjunto de depuración. ¡Gracias!
Michael Mior
73

Otro problema al crear subclases dictes que el integrado __init__no llama updatey el integrado updateno llama __setitem__. Por lo tanto, si desea que todas las operaciones de setitem pasen por su __setitem__función, debe asegurarse de que se llame usted mismo:

class DictWatch(dict):
    def __init__(self, *args, **kwargs):
        self.update(*args, **kwargs)

    def __getitem__(self, key):
        val = dict.__getitem__(self, key)
        print 'GET', key
        return val

    def __setitem__(self, key, val):
        print 'SET', key, val
        dict.__setitem__(self, key, val)

    def __repr__(self):
        dictrepr = dict.__repr__(self)
        return '%s(%s)' % (type(self).__name__, dictrepr)

    def update(self, *args, **kwargs):
        print 'update', args, kwargs
        for k, v in dict(*args, **kwargs).iteritems():
            self[k] = v
Matt Anderson
fuente
9
Si está usando Python 3, querrá cambiar este ejemplo para que printsea ​​la print()función y el update()método use en items()lugar de iteritems().
Al Sweigart
Probé su sol, pero parece que solo funciona para un solo nivel de indexación (es decir, dict [clave] y no dict [clave1] [clave2] ...) *
Andrew Naguib
d [key1] devuelve algo, quizás un diccionario. La segunda clave indexa eso. Esta técnica no puede funcionar a menos que lo devuelto sea compatible con el comportamiento del reloj.
Matt Anderson
1
@AndrewNaguib: ¿Por qué debería funcionar con matrices anidadas? La matriz anidada tampoco funciona con el dictado normal de Python (si no lo implementó usted mismo)
Igor Chubin
1
@AndrewNaguib: __getitem__necesitaría probar valy solo hacerlo condicionalmente, es decirif isinstance(val, dict): ...
martineau
14

Considere la posibilidad de subclasificar UserDicto UserList. Estas clases están diseñadas para ser subclasificadas mientras que las normales dicty listno lo son, y contienen optimizaciones.

Andrew Paté
fuente
9
Como referencia, la documentación en Python 3.6 dice "La necesidad de esta clase ha sido parcialmente suplantada por la capacidad de realizar subclases directamente desde dict; sin embargo, esta clase puede ser más fácil de trabajar porque el diccionario subyacente es accesible como un atributo".
Sean
@andrew un ejemplo podría ser útil.
Vasantha Ganesh K
2
@VasanthaGaneshK treyhunner.com/2019/04/…
SirDorius
9

Eso realmente no debería cambiar el resultado (que debería funcionar, para buenos valores de umbral de registro): su init debería ser:

def __init__(self,*args,**kwargs) : dict.__init__(self,*args,**kwargs) 

en cambio, porque si llama a su método con DictWatch ([(1,2), (2,3)]) o DictWatch (a = 1, b = 2), esto fallará.

(o, mejor, no defina un constructor para esto)

makapuf
fuente
Solo me preocupa la dict[key]forma de acceso, por lo que esto no es un problema.
Michael Mior
1

Todo lo que tendrás que hacer es

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={}):
        super(BatchCollection, self).__init__(inpt)

    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
0

Para completar la respuesta de andrew pate, aquí hay un ejemplo que muestra una diferencia entre dicty UserDict:

Es complicado sobrescribir correctamente dict:

class MyDict(dict):

  def __setitem__(self, key, value):
    super().__setitem__(key, value * 10)


d = MyDict(a=1, b=2)  # Bad! MyDict.__setitem__ not called
d.update(c=3)  # Bad! MyDict.__setitem__ not called
d['d'] = 4  # Good!
print(d)  # {'a': 1, 'b': 2, 'c': 3, 'd': 40}

UserDictheredar collections.abc.MutableMapping, por lo que es mucho más fácil de personalizar:

class MyDict(collections.UserDict):

  def __setitem__(self, key, value):
    super().__setitem__(key, value * 10)


d = MyDict(a=1, b=2)  # Good: MyDict.__setitem__ correctly called
d.update(c=3)  # Good: MyDict.__setitem__ correctly called
d['d'] = 4  # Good
print(d)  # {'a': 10, 'b': 20, 'c': 30, 'd': 40}

Del mismo modo, es suficiente con aplicar __getitem__para ser compatible con automáticamente key in my_dict, my_dict.get...

Nota: UserDictno es una subclase de dict, por isinstance(UserDict(), dict)lo que fallará (pero isinstance(UserDict(), collections.abc.MutableMapping)funcionará)

Conchylicultor
fuente