pythonmultithreadingscheduleclass-methodinstance-methods

Why would the run method of a Thread subclass be a class method?


I'm reading through the documentation for the schedule API and in the "Run in the Background" example they define the run method as a class method in their ScheduleThread class. The code is below:

import threading
import time

import schedule


def run_continuously(interval=1):
    """Continuously run, while executing pending jobs at each
    elapsed time interval.
    @return cease_continuous_run: threading. Event which can
    be set to cease continuous run. Please note that it is
    *intended behavior that run_continuously() does not run
    missed jobs*. For example, if you've registered a job that
    should run every minute and you set a continuous run
    interval of one hour then your job won't be run 60 times
    at each interval but only once.
    """
    cease_continuous_run = threading.Event()

    class ScheduleThread(threading.Thread):
        @classmethod
        def run(cls):
            while not cease_continuous_run.is_set():
                schedule.run_pending()
                time.sleep(interval)

    continuous_thread = ScheduleThread()
    continuous_thread.start()
    return cease_continuous_run


def background_job():
    print('Hello from the background thread')


schedule.every().second.do(background_job)

# Start the background thread
stop_run_continuously = run_continuously()

# Do some other things...
time.sleep(10)

# Stop the background thread
stop_run_continuously.set()

I don't understand why they use @classmethod here. From doing a bit of research it seems that run() should always be an instance method in Thread subclasses. Is this a mistake or am I missing something?

I ran the code unaltered from the documentation and then I changed the classmethod to an instance method (removed the decorator and replaced run(cls) with run(self)) and ran the code again and the behavior was identical.

I expected something to break or there to be different behavior.


Solution

  • There's no good reason for the method to be a classmethod. As you note, it still works, but it's not the usual API for thread subclasses.

    Indeed, the whole design of that code is a bit odd. If you want to bundle some data along with a running thread, a thread subclass does make sense, but it doesn't make sense to implement the whole thing as a closure if you're going to do that. There are two more reasonable alternatives:

    First, you could put the data (the event) into the class:

    class run_continuously(threading.Thread):
        def __init__(self, interval=1):
            super().__init__()
    
            self.interval = interval                 # make these values instance variables
            self.cease_continuous_run = threading.Event()
    
            self.start()
            
        def run(self): # now we need to be an instance method, because we use instance vars
            while not self.cease_continuous_run.is_set():
                schedule.run_pending()
                time.sleep(self.interval)
    
        def stop(self):   # the event API can now be an internal implementation detail, not
            self.cease_continuous_run.set()  # something the user of our code needs to know
    
    schedule.every().second.do(background_job)
    
    rc = run_continuously()   # create the thread, which starts itself
    
    time.sleep(10)
    
    rc.stop()
    

    Or secondly, you could abandon the use of a thread subclass and just create a thread with a function target, using a closure to hold the data in the same way the original code did:

    def run_continuously(interval=1):
        cease_continuous_run = threading.Event()
    
        def helper():
            while not cease_continuous_run.is_set():
                schedule.run_pending()
                time.sleep(interval)
    
        continuous_thread = threading.Thread(target=helper)
        continuous_thread.start()
        return cease_continuous_run
    

    In both of these examples, I did away with the spurious classmethod decorator, either because it was necessary (since we wanted to access instance variables) or because I'd done away with the class altogether.