I'm working with asyncio
for scheduling methods to be called at certain relative times intervals. I decided to centralize the scheduling into one method of the class I wrote to reduce the chance of getting errors with the logic of my project.
Such method should be called every time a scheduled method finished. I though on adding loop.call_soon
at the end of each method, but I decided to give a shot to decorators.
I wrote a class decorator and then applied it to some methods of my main class, wrote the rest of the logic and all that. But when trying to test my changes on my project I get an exception:
AttributeError: 'function' object has no attribute '__self__'
Somehow, decorating my method made it a function. This is something I cannot understand, why does this happened? how can I work around this without giving up decorators?
Here is a minimal, complete, and verifiable example of what I'm trying to do:
import asyncio
from datetime import datetime
class thinkagain:
loop = asyncio.get_event_loop()
def __init__(self, f):
self.fun = f
self.class_ = f.__self__
def __call__(self):
self.fun(*args, **kwords)
# everything in Python is an object
setattr(self.fun, "called", datetime.utcnow())
self.loop.call_later(self.class_.think, 5 * 60)
class DoSomething:
loop = asyncio.get_event_loop()
@thinkagain
def think(self):
attr = getattr(self.dosomething, "called")
if attr:
elapsed = attr - datetime.utcnow()
seconds = elapsed.seconds
else:
seconds = 99999
if seconds >= 20 * 60:
self.loop.call_soon(self.dosomething)
@thinkagain
def dosomething(self):
print("I did something awesome!")
loop = asyncio.get_event_loop()
something = DoSomething()
loop.call_soon(something.think)
loop.run_forever()
and here is the exception I get:
Python 3.5.1 (default, Dec 7 2015, 13:41:59)
[GCC 5.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/tmp/mcve.py", line 19, in <module>
class DoSomething:
File "/tmp/mcve.py", line 22, in DoSomething
@thinkagain
File "/tmp/mcve.py", line 10, in __init__
self.class_ = f.__self__
AttributeError: 'function' object has no attribute '__self__'
>>>
Regarding decorators, Graham Dumpleton gave excellent talk Advanced methods for creating decorators, discussing internal implementations of various decoration flavours and techniques. Highly recommended.
Relevant module he introduced at the end: https://github.com/GrahamDumpleton/wrapt
Never the less, i modified your example with two versions. Version below stores attributes directly in methods as you intended.
from datetime import datetime
class thinkagain:
def __init__(self, f):
# Plain function as argument to be decorated
self.func = f
def __get__(self, instance, owner):
self.instance_ = instance
return self.__call__
def __call__(self, *args, **kwargs):
"""Invoked on every call of any decorated method"""
# set attribute directly within bound method
bound_method = getattr(self.instance_, self.func.__name__)
bound_method.__dict__['called'] = datetime.utcnow()
# returning original function with class' instance as self
return self.func(self.instance_, *args, **kwargs)
class DoSomething_A:
@thinkagain
def think(self, *args, **kwargs):
print('\n%s' % locals())
print(self.think.called, args, kwargs)
self.dosomething()
@thinkagain
def dosomething(self):
print('%s\n' % ('-'*30), locals())
print("%s I did something awful" % self.dosomething.called)
Second version looks cleaner and it skips storing attributes inside methods and assigns them directly within the instance.
from datetime import datetime
class thinkagain:
def __init__(self, f):
# Plain function as argument to be decorated
self.func = f
def __get__(self, instance, owner):
self.instance_ = instance
return self.__call__
def __call__(self, *args, **kwargs):
"""Invoked on every call of decorated method"""
# set attribute on instance
name = '%s_called' % self.func.__name__
setattr(self.instance_, name, datetime.utcnow())
# returning original function with class' instance as self
return self.func(self.instance_, *args, **kwargs)
class DoSomething_B:
@thinkagain
def think(self, *args, **kwargs):
print('\n%s' % locals())
print(self.think_called)
self.dosomething()
@thinkagain
def dosomething(self):
print('%s\n' % ('-'*30), locals())
print(self.dosomething_called)
Both produce same desired behaviour:
>>> something = DoSomething_A().think(1, 2)
{'args': (1, 2), 'kwargs': {}, 'self': <__main__.DoSomething_A object at 0x10209f128>}
2015-12-26 04:13:25.629887 (1, 2) {}
------------------------------
{'self': <__main__.DoSomething_A object at 0x10209f128>}
2015-12-26 04:13:25.647476 I did something awful
and
>>> something = DoSomething_B().think('arg_a', 'arg_b')
{'args': ('arg_a', 'arg_b'), 'kwargs': {}, 'self': <__main__.DoSomething_B object at 0x10209f208>}
2015-12-26 04:13:25.648039
------------------------------
{'self': <__main__.DoSomething_B object at 0x10209f208>}
2015-12-26 04:13:25.648390
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With