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