0

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.

3
  • A classmethod can be called on an instance, just like a regular instance method can be, it will just be passed the class rather than the instance it was called on as the first argument. Since the example's run method doesn't use either self or cls, it doesn't really matter which it gets. As you say, there's no real reason for it to be a classmethod, but it still works. Commented Sep 17, 2024 at 2:35
  • In the original solution run could also be a staticmethod, right? Since the instance and the class aren't used? I'm assuming again that there is no real reason to use staticmethod here over the others and the best solution is to restructure as in your answer below. Commented Sep 17, 2024 at 3:43
  • I suspect they got a linter warning that the method could be a class method, because they did not use self inside of it and followed it blindly. Commented Sep 17, 2024 at 4:37

1 Answer 1

0

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.

Sign up to request clarification or add additional context in comments.

1 Comment

This answer makes sense and these two solutions certainly seem better than the original one. I don't have much experience with Python threading and closures so thank you for multiple examples and explanations.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.