3

Using Python 3.7 and PySide2, I created a worker object on a dedicated QThread to execute a long-running function. This is illustrated in the code below.

import threading
from time import sleep
from PySide2.QtCore import QObject, QThread, Signal, Slot
from PySide2.QtWidgets import QApplication

class Main(QObject):   
    signal_for_function = Signal()

    def __init__(self):
        print('The main thread is "%s"' % threading.current_thread().name)
        super().__init__()
        self.thread = QThread(self)
        self.worker = Worker()
        self.worker.moveToThread(self.thread)
        self.thread.start()
        self.signal_for_function.connect(self.worker.some_function)

def some_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

class Worker(QObject):
    # @some_decorator
    def some_function(self):
        print('some_function is running on thread "%s"' % threading.current_thread().name)

app = QApplication()
m = Main()
m.signal_for_function.emit()

sleep(0.100)
m.thread.quit()
m.thread.wait()

If I use some_function without the decorator, I get this as expected:

The main thread is "MainThread"
some_function is running on thread "Dummy-1"

However, if I apply a decorator (i.e. uncomment "@some_decorator"), I get:

The main thread is "MainThread"
some_function is running on thread "MainThread"

Why does this happen, and how do I make the decorated function run on the worker thread as I intented to?

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
derren
  • 33
  • 3
  • After some more research, here are some related questions: https://stackoverflow.com/questions/43937897/pyside-method-is-not-executed-in-thread-context-if-method-is-invoked-via-lambda and https://stackoverflow.com/questions/23317195/pyqt-movetothread-does-not-work-when-using-partial-for-slot – derren Mar 26 '20 at 17:05

1 Answers1

1

Solution:

You must use @functools.wrap:

import functools
# ...

def some_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

Output:

The main thread is "MainThread"
some_function is running on thread "Dummy-1"

Explanation:

To analyze the difference of using @functools.wrap or not then the following code must be used:

def some_decorator(func):
    print(func.__name__, func.__module__, func.__doc__, func.__dict__)

    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs) 
    print(wrapper.__name__, wrapper.__module__, wrapper.__doc__, wrapper.__dict__)
    return wrapper

By removing @functools.wrap you should get the following:

some_function __main__ None {}
wrapper __main__ None {}

By not removing @functools.wrap you should get the following:

some_function __main__ None {}
some_function __main__ None {'__wrapped__': <function Worker.some_function at 0x7f610d926a60>}

The main difference is in __name__, in the case of @functools.wrap it makes the wrapper function have the same name as "func", and what difference does that make? It serves to identify if the function belongs to the Worker class or not, that is, when the Worker class is created, a dictionary is created that stores the methods, attributes, etc., but when the signal invokes some_function then it returns the wrapper that has the name "wrapper" that is not in the Worker's dictionary, but in the case of using @functools.wrapper some_function is invoked then it returns to wrapper with the name "some_function" causing the Worker object to invoke it.

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • The fix works for this specific case, but I'm not sure your explanation is entirely correct though. If you define `some_function` in an `__init__` constructor of Worker, `some_function` will be in the Worker's dictionary and have the correct name but it still won't execute on the worker thread. Why? – derren Mar 20 '20 at 19:24
  • 1
    @derren 1) What do you mean by *define some_function in an \__init__ constructor of Worker* ?, I make it explicit in what you point out, 2) PySide2 creates a dictionary of the modes when creating the instance (one step before \__init__ is called, in the \__new__ method). But in general you should always use functools.wrap so that the decorator (which in the end is a method wrapper) behaves the same as the function. – eyllanesc Mar 20 '20 at 19:37