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.