Ir al contenido principal

Hi, I'm Mariano Guerra, below is my blog, if you want to learn more about me and what I do check a summary here: marianoguerra.github.io or find me on twitter @warianoguerra or Mastodon @marianoguerra@hachyderm.io

Decoradores, introspeccion y señales

Jugando un poco con decoradores y el modulo inspect por primera vez, se me ocurrió intentar reemplazar alguna funcionalidad de gobject.

La funcionalidad que implemente primero, que es bastante útil, es la de notificar a un objeto cuando un atributo de otro objeto cambia, por ejemplo, es útil para la lista de usuarios saber cuando el atributo "status" de un contacto cambia.
Lo que quería, era que las clases "notificadoras" y las clases "notificables" no tuvieran tanto código feo de manejo de eventos, para eso, decidí ver si los decoradores eran la solución, parece ser que si.

ejemplo, una clase "TestObject" tiene un atributo llamado "attribute" con las properties de get/set.
Eel código seria así:


class TestObject(object):
'''a class that have a property called attribute'''

def __init__(self, attribute):
'''class contructor'''
self._attribute = attribute

def set_attribute(self, attribute):
'''set the value of self._attribute'''
self._attribute = attribute

def get_attribute(self):
'''return the value of attribute'''
return self._attribute

attribute = property(fget=get_attribute, fset=set_attribute)


si, ya se que el property en ese caso es al pedo, pero va a ser requerido mas adelante, así que lo pongo :D.

ahora, una clase que desea ser notificada cuando el atributo "attribute" de la clase "TestObject" cambia.
El código seria mas o menos así:


class TestNotificable(object):
'''a class that is notified when an attribute on other object changes'''

def __init__(self):
'''class constructor'''
pass

def on_property_changed(self, obj, attr_name, method_name,
old_value, new_value):
'''method called when an attribute is changed
on an object that we are attached to
'''
print("%s: %s changed in %s(%s).%s from %s to %s" % \
(str(id(self)), attr_name, obj.__class__.__name__,
str(id(obj)), method_name, repr(old_value),
repr(new_value)))



no hay mucha magia, solo lo que necesitamos, un atributo y un método el cual es llamado cuando este atributo cambia

imaginen que haciendo:


obj = TestObject(5)
notificable = TestNotificable()
obj.attach(notificable)
obj.attribute = 10


la salida fuera:


3084746540: attribute changed in TestObject(3084722988).set_attribute from 5 to 10


estaría bueno, pero cuantas lineas habría que agregar a cada clase para eso?
si mis cálculos no fallan, 2 lineas a TestObject y ninguna a TestNotificable.

el código de las clases seria:


import signals

class TestNotificable(signals.Notificable):
'''a class that implements the notificable interface'''

def __init__(self):
'''class constructor'''
pass

def on_property_changed(self, obj, attr_name, method_name,
old_value, new_value):
'''method called when a method with the
@notify_change decorator from a signal.Object is called
'''
print "%s: %s changed in %s(%s).%s from %s to %s" % \
(str(id(self)), attr_name, obj.__class__.__name__,
str(id(obj)), method_name, repr(old_value),
repr(new_value))

class TestObject(signals.Object):
'''a class that have a property that notify when it's changed'''

def __init__(self, attribute):
'''class contructor'''
signals.Object.__init__(self)
self._attribute = attribute

@signals.notify_change
def set_attribute(self, attribute):
'''set the value of self._attribute'''
self._attribute = attribute

def get_attribute(self):
'''return the value of attribute'''
return self._attribute

attribute = property(fget=get_attribute, fset=set_attribute)


una función para probar el ejemplo:


def test():
'''test the implementation of signals'''
obj = TestObject(5)

notificable = TestNotificable()
obj.attach(notificable)

obj.attribute = 10

notificable1 = TestNotificable()
obj.attach(notificable1)

print("")
obj.attribute = None
print("")

notificable2 = TestNotificable()
obj.attach(notificable2)

try:
obj.attach("")
print("[EE] doesn't catch not notificables being attached")
except TypeError:
print("catch not notificables being attached")

notificable = None

if len(obj.notificables) == 2:
print("clean the list if a reference is removed")
else:
print("[EE] doesn't clean the list if a reference is removed")

print("")
obj.attribute = True

obj.deattach(notificable2)
print("\nshould show 1 notification\n")
obj.attribute = []

notificable1 = None

if len(obj.notificables) == 0:
print("clean the list if a reference is removed")
else:
print("[EE] doesn't clean the list if a reference is removed")

obj.attribute = "shouldn't be shown"

if __name__ == '__main__':
test()


que produce la salida:


3083871212: attribute changed in TestObject(3083871244).set_attribute from 5 to 10

3083871212: attribute changed in TestObject(3083871244).set_attribute from 10 to None
3083221740: attribute changed in TestObject(3083871244).set_attribute from 10 to None

catch not notificables being attached
clean the list if a reference is removed

3083221740: attribute changed in TestObject(3083871244).set_attribute from None to True
3083222572: attribute changed in TestObject(3083871244).set_attribute from None to True

should show 1 notification

3083221740: attribute changed in TestObject(3083871244).set_attribute from True to []
clean the list if a reference is removed


el código de signals.py es el siguiente:


import weakref
import inspect

'''a module that implement classes and methods to do signals on a pythonic
way
'''

class Object(object):
'''a class that represent an object that can call other objects
to notify about a changed property, must be used with the
@notify_change decorator, the classes that want to register
to receive notifications over property changes must inherit from
the Notificable interface
'''

def __init__(self):
'''class constructor'''
# a list of weakrefs
self.notificables = []

def notify_property_change(self, attr_name, method_name,
old_value, new_value):
'''call the on_property_changed of all the objects on the list
'''
# uncomment the code below to have a nice debug of the changes
# made to the properties on the objects that inherit from this
# class
#print("%s: %s changed in %s(%s).%s from %s to %s" % \
# (str(id(self)), attr_name, obj.__class__.__name__,
# str(id(obj)), method_name, repr(old_value),
# repr(new_value)))

for notificable in self.notificables:
notificable.on_property_changed(self, attr_name, method_name,
old_value, new_value)

def attach(self, notificable):
'''attach an object that implement the Notificable interface,
add a weakref so it's deleted when the other reference to the
object are 0
'''

if Notificable not in notificable.__class__.mro():
raise TypeError("the object must implement Notificable")

self.notificables.append(weakref.proxy(notificable,
self.__clean_refs))

def deattach(self, notificable):
'''remove the notificable obect from the list, if it exists'''
proxy = weakref.proxy(notificable)

if proxy in self.notificables:
self.notificables.remove(proxy)

def __clean_refs(self, reference):
'''remove the reference from the list, since it's going to be
cleaned by the garbage collector
'''

self.notificables.remove(reference)

class Notificable(object):
'''Interface that define the methods to be implemented to be able
to register to an signals.Object to receive notifications when
a method with the @notify_change decorator is called
'''

def on_property_changed(self, obj, attr_name, method_name,
old_value, new_value):
'''method called when a method with the
@notify_change decorator from a signal.Object is called
'''
pass

def notify_change(method):
'''decorator that add the ability to a setter/fset property to
notify other objects when an attribute of the object is modified
'''

def new_method(*args):
'''the function that decorates the class method, basically it
run the setter and then try to notify the objects about the
change, the setter is runned first since it may raise an
exception, and we dont want to notify over a change that
wasn't made
'''

arguments = inspect.getargspec(method)[0]

if Object not in args[0].__class__.mro():
raise TypeError("object doesn't inherit from signals.Object")

if len(arguments) != 2:
raise ValueError("method should receive two arguments")

self_object = args[0]
name = arguments[1]
method_name = method.__name__
old_value = getattr(self_object, name)
new_value = args[1]

return_value = method(*args)
self_object.notify_property_change(name, method_name,
old_value, new_value)

return return_value

return new_method