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 ?
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.
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