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
- En el constructor
- En el método
setdefault
- 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