Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Decorating methods: AttributeError: 'function' object has no attribute '__self__'?

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__'
>>> 
like image 539
shackra Avatar asked Jan 07 '23 22:01

shackra


1 Answers

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
like image 130
Vlad Nikiporoff Avatar answered Jan 13 '23 16:01

Vlad Nikiporoff