2010-01-13 20 views
35

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.Subclassing Python dictionary para reemplazar __setitem__

he descubierto tres situaciones en Python (en este caso, 2.6.4) no llamar a mi __setitem__ método anulado cuando los valores de ajuste, y en lugar de llamadas PyDict_SetItem directamente

  1. En el constructor
  2. En el setdefault método
  3. En el método 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 al establecer los elementos de forma explícita. Para obtener Python para llamar siempre a mi método __setitem__, he tenido que volver a implementar esos tres métodos, como esto:

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 otros métodos que necesito para anular, con el fin de saber que Python siempre llamar a mi método __setitem__?

ACTUALIZACIÓN

Por sugerencia de GS, he tratado de subclases UserDict (en realidad, IterableUserDict, ya que quiero para iterar sobre las claves) como esto:

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 correcta llame a mi __setitem__ en setdefault, pero no lo llama en update, o cuando los datos iniciales se proporcionan al constructor.

ACTUALIZACIÓN 2

la sugerencia de Peter Hansen me llevó a mirar con más cuidado dictobject.c, y me di cuenta de que el método de actualización se podría simplificar un poco, ya que el constructor diccionario incorporado, simplemente llama a la incorporada -en el método de actualización 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] 
+1

¿Funciona si subclasifica UserDict en lugar del dict normal? –

+0

Honestamente, no me di cuenta de que UserDict todavía estaba por aquí :) Lo intentaré –

+0

El UserDict habría sido bueno, pero desafortunadamente su método de actualización simplemente llama a update en el diccionario de datos subyacente. –

Respuesta

41

Estoy respondiendo a mi propia pregunta, ya que, finalmente, decidí que realmente no quiero subclase Dict, en lugar de crear una nueva clase de mapeo, y todavía UserDict aplaza al objeto Dict que subyace en algunos casos, en lugar de usando el __setitem__ proporcionado.

Después de leer y re-leer el código fuente de Python 2.6.4 (en su mayoría Objects/dictobject.c, pero grepped por todos los sitios más para ver donde se utilizan los diversos métodos,) mi entendimiento es que el código siguiente es suficiente para tener mi __setitem__ llamado cada vez que se cambia el objeto, y para que se comporte exactamente como un Dict Python:

La sugerencia de Peter Hansen me hizo mirar más detenidamente al dictobject.c, y me di cuenta de que el método de actualización en mi respuesta original podría simplificarse poco, ya que el constructor del diccionario incorporado simplemente llama al método de actualización incorporado 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. Todavía aceptaré cualquier respuesta que me demuestre que me he perdido algo, pero de lo contrario, estoy marcando la marca de verificación al lado de éste en un par de días, y llamándola la respuesta correcta :)

+0

Si entiendo lo que describes en la ACTUALIZACIÓN 2 de tu pregunta, creo que también deberías actualizar el código en esta respuesta tuya que tú aceptado. – martineau

+0

Aunque su código para las pruebas no es muy riguroso, ya que ni siquiera comprueba si se están llamando a los métodos de su subclase dict cuando se pasa una instancia 'MyUpdateDict', mi propia versión de la que comprueba que eso indica , entonces mi voto positivo se mantiene. – martineau

+0

¿ha investigado por casualidad esto por 'defaultdict' y/o versiones más recientes de python (2.7 en particular)? – drevicko

4

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

No necesita hacer esto para implementar un objeto parecido a un dictado, y puede ser más simple en su caso escribir una clase ordinaria, luego agregar soporte para el subconjunto requerido de la interfaz dict.

La mejor manera de lograr lo que busca es probablemente la clase base abstracta MutableMapping. PEP 3119 -- Introducing Abstract Base Classes

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.

+1

Estoy anulando setitem para hacer algo de validación de datos. El objeto es esencialmente un diccionario con algunos métodos adicionales que actúan sobre sus datos, y me gustaría que los codificadores lo usaran para usarlo exactamente como lo harían con un diccionario, sin una larga lista de métodos para evitarlo. Echaré un vistazo al abecedario; Si funcionan, tendré que ver si puedo actualizar todos los entornos de implementación a 2.6 –

0

Utilice el objeto. nombre clave = valor en lugar de objeto ["nombre clave"] = valor

+2

Mientras * puedes * hacer esto para guardar un par de clave-valor ... no ayuda a solucionar el problema y esta pregunta no requiere una solución alternativa (basada en la respuesta aceptada ...) – Jared

3

Encontré la respuesta y los comentarios de Ian muy útiles y claros. Simplemente señalaría que tal vez una primera llamada al método de clase superior __init__ podría ser más seguro, 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 propuesta de aplicación MyUpdateDict, descubrí que mediante la simple sustitución

class MyUpdateDict(dict): 

con:

from collections import OrderedDict 
class MyUpdateDict(OrderedDict): 

entonces el código de prueba publicado anteriormente fallaron:

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' 

En cuanto a collections.py code, resulta que OrderedDict necesita su método __init__ que debe invocarse para inicializar y configurar los atributos personalizados necesarios.

Por lo tanto, simplemente añadiendo una primera llamada al método súper __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 y OrderedDict.

No puedo afirmar 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, al tratar de extender otras subclases dict

Cuestiones relacionadas