2012-01-13 11 views
12

Tengo una clase donde las instancias de esta clase necesitan rastrear los cambios a sus atributos.¿Realizar un seguimiento de los cambios a listas y diccionarios en python?

Ejemplo: obj.att = 2 sería algo que puede rastrearse fácilmente simplemente anulando el __setattr__ de obj.

Sin embargo, hay un problema cuando el atributo que quiero cambiar es un objeto en sí, como una lista o un dict.

¿Cómo podría rastrear cosas como obj.att.append(1) o obj.att.pop(2)?

Estoy pensando en ampliar la lista o la clase de diccionario, pero mono parches instancias de dichas clases una vez que el obj y la obj.att es a la vez inicializado de manera que obj recibe una notificación cuando cosas como .append se llama. De alguna manera, eso no se siente muy elegante.

La otra forma en que puedo pensar sería pasar una instancia de obj en la inicialización de la lista, pero eso rompería una gran cantidad de código existente y parece incluso menos elegante que el método anterior.

¿Alguna otra idea/sugerencia? ¿Hay una solución simple que me falta aquí?

+4

qué pasa hacer los objetos internos y permitir acceso desde el exterior solo sobre las clases getter y setter? – joni

+0

puede serializar los objetos y comparar las versiones serializadas. –

+0

Estoy de acuerdo con joni. Considera la Ley de Demeter aquí. De forma alternativa, no permita el acceso a 'attr' directamente, pero proporcione getters y setters que creen una copia profunda. – Thomas

Respuesta

4

Tenía curiosidad de cómo se podría lograr esto cuando vi la pregunta, esta es la solución que se me ocurrió. No es tan simple como me gustaría, pero puede ser útil. En primer lugar, aquí es el comportamiento:

class Tracker(object): 
    def __init__(self): 
     self.lst = trackable_type('lst', self, list) 
     self.dct = trackable_type('dct', self, dict) 
     self.revisions = {'lst': [], 'dct': []} 


>>> obj = Tracker()   # create an instance of Tracker 
>>> obj.lst.append(1)   # make some changes to list attribute 
>>> obj.lst.extend([2, 3]) 
>>> obj.lst.pop() 
3 
>>> obj.dct['a'] = 5   # make some changes to dict attribute 
>>> obj.dct.update({'b': 3}) 
>>> del obj.dct['a'] 
>>> obj.revisions    # check out revision history 
{'lst': [[1], [1, 2, 3], [1, 2]], 'dct': [{'a': 5}, {'a': 5, 'b': 3}, {'b': 3}]} 

Ahora la función trackable_type() que hace posible todo esto:

def trackable_type(name, obj, base): 
    def func_logger(func): 
     def wrapped(self, *args, **kwargs): 
      before = base(self) 
      result = func(self, *args, **kwargs) 
      after = base(self) 
      if before != after: 
       obj.revisions[name].append(after) 
      return result 
     return wrapped 

    methods = (type(list.append), type(list.__setitem__)) 
    skip = set(['__iter__', '__len__', '__getattribute__']) 
    class TrackableMeta(type): 
     def __new__(cls, name, bases, dct): 
      for attr in dir(base): 
       if attr not in skip: 
        func = getattr(base, attr) 
        if isinstance(func, methods): 
         dct[attr] = func_logger(func) 
      return type.__new__(cls, name, bases, dct) 

    class TrackableObject(base): 
     __metaclass__ = TrackableMeta 

    return TrackableObject() 

Esto básicamente utiliza una metaclase para anular todos los métodos de un objeto a añadir un poco de registro de revisión si el objeto cambia Esto no está muy probado y no he probado ningún otro tipo de objeto además de list y dict, pero parece funcionar bien para ellos.

+0

Así que esta es esencialmente la idea de parche de mono que tengo .. – Pwnna

+1

Sí, básicamente, estaba tratando de encontrar una manera sencilla de hacerlo en tales una forma en la que no tendrías que crear subclases 'list' y' dict' separadas y descubrir qué métodos necesitas parchar, obviamente, lo que se me ocurrió no es tan simple :) –

3

Puede aprovechar las clases base abstractas en el módulo de colecciones, que dict y list implementar. Esto le proporciona una interfaz de biblioteca estándar para codificar en contra con una breve lista de métodos para sobrescribir, __getitem__, __setitem__, __delitem__, insert. Envuelva los atributos en un adaptador rastreable dentro de __getattribute__.

import collections 

class Trackable(object): 
    def __getattribute__(self, name): 
     attr = object.__getattribute__(self, name) 
     if isinstance(attr, collections.MutableSequence): 
      attr = TrackableSequence(self, attr) 
     if isinstance(attr, collections.MutableMapping): 
      attr = TrackableMapping(self, attr) 
     return attr 

    def __setattr__(self, name, value): 
     object.__setattr__(self, name, value) 
     # add change tracking 


class TrackableSequence(collections.MutableSequence): 
    def __init__(self, tracker, trackee): 
     self.tracker = tracker 
     self.trackee = trackee 

    # override all MutableSequence's abstract methods 
    # override the the mutator abstract methods to include change tracking 


class TrackableMapping(collections.MutableMapping): 
    def __init__(self, tracker, trackee): 
     self.tracker = tracker 
     self.trackee = trackee 

    # override all MutableMapping's abstract methods 
    # override the the mutator abstract methods to include change tracking 
2

En lugar de mono parches, puede crear una clase de proxy:

  • hacer una clase proxy que heredan de dict/lista/conjunto lo
  • valor de atributo
  • intercepción, y si el valor es a dict/list/set, engánchelo en la clase de proxy
  • En la clase de proxy __getattribute__, asegúrese de que se invoca el método en el tipo envuelto, pero asegúrese de realizar un seguimiento antes de hacerlo.

Pro:

  • ninguna alteración clase

contra:

  • está limitado a una serie de tipos que conoces y esperar
Cuestiones relacionadas