python dictionary subclass

Subclassing Python dictionary para anular__setitem__



(4)

¿Cuál es su caso de uso para subclasificar dict?

No es necesario que haga esto para implementar un objeto tipo dict, y en su caso podría ser más simple escribir una clase ordinaria, luego agregar soporte para el subconjunto requerido de la interfaz dict.

La mejor forma de lograr lo que busca es, probablemente, la clase base abstracta MutableMapping. PEP 3119 - Introducción de clases base abstractas

Esto también lo ayudará a responder la pregunta "¿Hay algún otro método que deba anular?". Tendrá que anular todos los métodos abstractos. Para MutableMapping: los métodos abstractos incluyen setitem , delitem . Los métodos concretos incluyen pop, popitem, clear, update.

Estoy construyendo una clase que subclasifica dict y anula __setitem__ . Me gustaría estar seguro de que se llamará a mi método en todas las instancias en las que se puedan establecer los elementos del diccionario.

He descubierto tres situaciones en las que Python (en este caso, 2.6.4) no llama a mi método __setitem__ anulado al establecer valores, y en su lugar llama a PyDict_SetItem directamente

  1. En el constructor
  2. En el método setdefault
  3. En el método de update

Como una prueba muy simple:

class MyDict(dict): def __setitem__(self, key, value): print "Here" super(MyDict, self).__setitem__(key, str(value).upper()) >>> a = MyDict(abc=123) >>> a[''def''] = 234 Here >>> a.update({''ghi'': 345}) >>> a.setdefault(''jkl'', 456) 456 >>> print a {''jkl'': 456, ''abc'': 123, ''ghi'': 345, ''def'': ''234''}

Puede ver que el método reemplazado solo se invoca cuando se configuran los elementos de forma explícita. Para que Python siempre llame a mi método __setitem__ , tuve que __setitem__ esos tres métodos, como este:

class MyUpdateDict(dict): def __init__(self, *args, **kwargs): self.update(*args, **kwargs) def __setitem__(self, key, value): print "Here" super(MyUpdateDict, self).__setitem__(key, value) def update(self, *args, **kwargs): if args: if len(args) > 1: raise TypeError("update expected at most 1 arguments, got %d" % len(args)) other = dict(args[0]) for key in other: self[key] = other[key] for key in kwargs: self[key] = kwargs[key] def setdefault(self, key, value=None): if key not in self: self[key] = value return self[key]

¿Hay algún otro método que deba sobrescribir para saber que Python siempre llamará a mi método __setitem__ ?

ACTUALIZAR

Por sugerencia de Per gs, he tratado de subclasificar UserDict (en realidad, IterableUserDict, ya que quiero iterar sobre las teclas) de esta manera:

from UserDict import *; class MyUserDict(IterableUserDict): def __init__(self, *args, **kwargs): UserDict.__init__(self,*args,**kwargs) def __setitem__(self, key, value): print "Here" UserDict.__setitem__(self,key, value)

Esta clase parece llamar correctamente a mi __setitem__ en setdefault , pero no lo llama en la update , o cuando los datos iniciales se proporcionan al constructor.

ACTUALIZACIÓN 2

La sugerencia de Peter Hansen me hizo mirar más detenidamente a dictobject.c, y me di cuenta de que el método de actualización podría simplificarse un poco, ya que el constructor del diccionario incorporado simplemente llama al método de actualización incorporado de todos modos. Ahora se ve así:

def update(self, *args, **kwargs): if len(args) > 1: raise TypeError("update expected at most 1 arguments, got %d" % len(args)) other = dict(*args, **kwargs) for key in other: self[key] = other[key]


Encontré Ian respuesta y comentarios muy útiles y claros. Solo señalaría que tal vez una primera llamada al método __init__ clase __init__ podría ser más segura, cuando no sea necesario: recientemente tuve que implementar un OrderedDict personalizado (estoy trabajando con Python 2.7): después de implementar y modificar mi código de acuerdo a la implementación propuesta de MyUpdateDict , descubrí que simplemente reemplazando

class MyUpdateDict(dict):

con:

from collections import OrderedDict class MyUpdateDict(OrderedDict):

entonces el código de prueba publicado arriba falló:

Traceback (most recent call last): File "Desktop/test_updates.py", line 52, in <module> my_dict = MyUpdateDict([(''b'',2),(''c'',3)],a=1) File "Desktop/test_updates.py", line 5, in __init__ self.update(*args, **kwargs) File "Desktop/test_updates.py", line 18, in update self[key] = other[key] File "Desktop/test_updates.py", line 9, in __setitem__ super(MyUpdateDict, self).__setitem__(key, value) File "/usr/lib/python2.7/collections.py", line 59, in __setitem__ root = self.__root AttributeError: ''MyUpdateDict'' object has no attribute ''_OrderedDict__root''

Al mirar el código de collections.py , resulta que OrderedDict necesita que se llame su método __init__ para inicializar y configurar los atributos personalizados necesarios.

Por lo tanto, simplemente agregando una primera llamada al método super __init__ ,

from collections import OrderedDict class MyUpdateDict(Orderedict): def __init__(self, *args, **kwargs): super(MyUpdateDict, self).__init__() #<-- HERE call to super __init__ self.update(*args, **kwargs)

tenemos una solución más general que aparentemente funciona tanto para dict como para OrderedDict.

No puedo decir si esta solución es generalmente válida, porque la probé solo con OrderedDict. Sin embargo, es probable que una llamada al método super __init__ sea ​​inofensiva o necesaria en lugar de dañina cuando se intenta extender otras subclases dict


Estoy respondiendo mi propia pregunta, ya que finalmente decidí que realmente quiero subclasificar Dict, en lugar de crear una nueva clase de mapeo, y UserDict todavía difiere al objeto Dict subyacente en algunos casos, en lugar de usar el __setitem__ proporcionado.

Después de leer y volver a leer el código fuente de Python 2.6.4 (en su mayoría Objects/dictobject.c , pero lo crucé en otro lugar para ver dónde se usan los distintos métodos), entiendo que el siguiente código es suficiente para que mi __setitem__ sea llamado cada vez que el objeto ha cambiado, y para que se comporte exactamente como un Dict de Python:

La sugerencia de Peter Hansen me hizo mirar más detenidamente a dictobject.c , y me di cuenta de que el método de actualización en mi respuesta original podría simplificarse un poco, ya que el constructor del diccionario incorporado simplemente llama al método de actualización integrado de todos modos. Así que la segunda actualización en mi respuesta ha sido agregada al código a continuación (por alguna persona útil ;-).

class MyUpdateDict(dict): def __init__(self, *args, **kwargs): self.update(*args, **kwargs) def __setitem__(self, key, value): # optional processing here super(MyUpdateDict, self).__setitem__(key, value) def update(self, *args, **kwargs): if args: if len(args) > 1: raise TypeError("update expected at most 1 arguments, " "got %d" % len(args)) other = dict(args[0]) for key in other: self[key] = other[key] for key in kwargs: self[key] = kwargs[key] def setdefault(self, key, value=None): if key not in self: self[key] = value return self[key]

Lo he probado con este código:

def test_updates(dictish): dictish[''abc''] = 123 dictish.update({''def'': 234}) dictish.update(red=1, blue=2) dictish.update([(''orange'', 3), (''green'',4)]) dictish.update({''hello'': ''kitty''}, black=''white'') dictish.update({''yellow'': 5}, yellow=6) dictish.setdefault(''brown'',7) dictish.setdefault(''pink'') try: dictish.update({''gold'': 8}, [(''purple'', 9)], silver=10) except TypeError: pass else: raise RunTimeException("Error did not occur as planned") python_dict = dict([(''b'',2),(''c'',3)],a=1) test_updates(python_dict) my_dict = MyUpdateDict([(''b'',2),(''c'',3)],a=1) test_updates(my_dict)

y pasa Todas las demás implementaciones que he probado han fallado en algún momento. Seguiré aceptando cualquier respuesta que me demuestre que me he perdido algo, pero de lo contrario, estoy marcando la marca de verificación junto a este en un par de días, y llamándola la respuesta correcta :)


Use object.keyname = value en lugar de object ["keyname"] = value