Home > other >  Decorated and wrapped function is not passing self to instance method
Decorated and wrapped function is not passing self to instance method

Time:06-10

I am trying to run some instance methods as background threads using a decorator. Several nested functions are chained (as found there) to make it work:

import traceback
from functools import partial
from threading import Thread


def backgroundThread(name=''):
    def fnWrapper(decorated_func):
        def argsWrapper(name, *inner_args, **inner_kwargs):
            def exceptionWrapper(fn, *args, **kwargs):
                try:
                    fn(*args, **kwargs)
                except:
                    traceback.print_exc()

            if not name:
                name = decorated_func.__name__

            th = Thread(
                name=name,
                target=exceptionWrapper,
                args=(decorated_func, )   inner_args,
                kwargs=inner_kwargs
            )
            th.start()

        return partial(argsWrapper, name)

    return fnWrapper


class X:
    @backgroundThread()
    def myfun(self, *args, **kwargs):
        print(args, kwargs)
        print("myfun was called")
        #1 / 0


x = X()
x.myfun(1, 2, foo="bar")
x.myfun()

Output/Error (on Windows, Python 3.6.6):

(2,) {'foo': 'bar'}
myfun was called
Traceback (most recent call last):
  File "t3.py", line 11, in exceptionWrapper
    fn(*args, **kwargs)
TypeError: myfun() missing 1 required positional argument: 'self'

The code works partly, how to be able to 'bind' self to the call: x.myfun() which takes no arguments ?

CodePudding user response:

Fundamentally, the problem is that @backgroundThread() doesn't wrap an instance method x.myfun; it wraps the function X.myfun that is namespaced to the class.

We can inspect the wrapped result:

>>> X.myfun
functools.partial(<function backgroundThread.<locals>.fnWrapper.<locals>.argsWrapper at 0x7f0a1e2e7a60>, '')

This is not usable as a method, because functools.partial is not a descriptor:

>>> X.myfun.__get__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'functools.partial' object has no attribute '__get__'
>>> class Y:
...     # no wrapper
...     def myfun(self, *args, **kwargs):
...         print(args, kwargs)
...         print("myfun was called")
...         #1 / 0
... 
>>> Y.myfun.__get__
<method-wrapper '__get__' of function object at 0x7f0a1e2e7940>

Because X.myfun is not usable as a descriptor, when it is looked up via x.myfun, it is called like an ordinary function. self does not receive the value of x, but instead of the first argument that was passed, resulting in the wrong output for the (1, 2, foo='bar') case and the exception for the () case.


Instead of having argsWrapper accept a name and then binding it with partial, we can just use the name from the closure - since we are already doing that with decorated_func anyway. Thus:

def backgroundThread(name=''):
    def fnWrapper(decorated_func):
        def argsWrapper(*inner_args, **inner_kwargs):
            nonlocal name
            def exceptionWrapper(fn, *args, **kwargs):
                try:
                    fn(*args, **kwargs)
                except:
                    traceback.print_exc()
            if not name:
                name = decorated_func.__name__
            th = Thread(
                name=name,
                target=exceptionWrapper,
                args=(decorated_func, )   inner_args,
                kwargs=inner_kwargs
            )
            th.start()
        return argsWrapper
    return fnWrapper

Here, nonlocal name is needed so that argsWrapper has access to a name from a scope that is not the immediate closure, but also isn't global.

  • Related