Python中类似于setInterval的函数

21

最近我发布了一个关于如何推迟Python函数执行的问题(类似于Javascript的setTimeout),结果使用threading.Timer是一个简单的任务(只要该函数不与其他代码共享状态,但在任何事件驱动环境中都会创建问题)。

现在我正在尝试做得更好,模拟setInterval。对于那些不熟悉Javascript的人来说,setInterval允许每x秒重复调用函数,而不会阻塞其他代码的执行。我已经创建了这个示例装饰器:

import time, threading

def setInterval(interval, times = -1):
    # This will be the actual decorator,
    # with fixed interval and times parameter
    def outer_wrap(function):
        # This will be the function to be
        # called
        def wrap(*args, **kwargs):
            # This is another function to be executed
            # in a different thread to simulate setInterval
            def inner_wrap():
                i = 0
                while i != times:
                    time.sleep(interval)
                    function(*args, **kwargs)
                    i += 1
            threading.Timer(0, inner_wrap).start()
        return wrap
    return outer_wrap

以下用法

@setInterval(1, 3)
def foo(a):
    print(a)

foo('bar')
# Will print 'bar' 3 times with 1 second delays

看起来对我来说它运行得很好。我的问题是:

  • 它似乎过于复杂,我担心可能错过了更简单/更好的机制
  • 装饰器可以在没有第二个参数的情况下被调用,在这种情况下,它会一直进行下去。当我说永远时,我的意思是永远——即使从主线程中调用sys.exit()也无法停止它,按下Ctrl+c也不行。唯一停止它的方法是从外部杀死Python进程。我想能够从主线程发送信号来停止回调。但我是一个新手,不知道如何在线程之间通信?

编辑如果有人想知道,这是装饰器的最终版本,感谢jd的帮助。

import threading

def setInterval(interval, times = -1):
    # This will be the actual decorator,
    # with fixed interval and times parameter
    def outer_wrap(function):
        # This will be the function to be
        # called
        def wrap(*args, **kwargs):
            stop = threading.Event()

            # This is another function to be executed
            # in a different thread to simulate setInterval
            def inner_wrap():
                i = 0
                while i != times and not stop.isSet():
                    stop.wait(interval)
                    function(*args, **kwargs)
                    i += 1

            t = threading.Timer(0, inner_wrap)
            t.daemon = True
            t.start()
            return stop
        return wrap
    return outer_wrap

它可以像上面那样使用固定的重复次数

@setInterval(1, 3)
def foo(a):
    print(a)

foo('bar')
# Will print 'bar' 3 times with 1 second delays

或者可以一直运行,直到收到停止信号

import time

@setInterval(1)
def foo(a):
    print(a)

stopper = foo('bar')

time.sleep(5)
stopper.set()
# It will stop here, after printing 'bar' 5 times.

我看了一下你的代码,有点困惑,因为我没有看到是什么导致循环实际上停止的。在我看来,这实际上会导致一个循环调用function,直到调用stopper.set(),此时循环会突然加速!我检查了一下,确实,如果在上面的示例中在stopper.set()之后再添加另一个time.sleep(5),那就是发生的事情。你需要加入一些逻辑来确保循环在stop.is_set()为真时停止--例如while i != times and not stop.is_set(): - senderle
你是对的,我在文档中误解了wait()方法的一个段落,并且没有发现我的测试中的问题。我现在会纠正代码。 - Andrea
我已经回答了你的其他问题,其中展示了如何在Tkinter、Gtk、Twisted中模拟Javascript的setInterval()而不使用线程。答案链接为:https://dev59.com/4sKRzogBFxS5KdRjmcet#14040516 - jfs
这里有一个简单的基于事件的 call_repeatedly(interval, func, *args) 实现。请参考链接:https://dev59.com/52Eh5IYBdhLWcg3wXCZL#22498708 - jfs
4个回答

11

我认为你的解决方案很好。

与线程通信有几种方法。要命令线程停止,可以使用threading.Event(),它有一个wait()方法,可以代替time.sleep()

stop_event = threading.Event()
...
stop_event.wait(1.)
if stop_event.isSet():
    return
...

如果要在程序终止时退出线程,请在调用 start()之前将其daemon属性设置为True。这适用于Timer()对象,因为它们是threading.Thread的子类。请参见http://docs.python.org/library/threading.html#threading.Thread.daemon


太棒了!我成功地移除了时间模块的依赖,并且成功实现了通信。现在回调可以轻松停止。 :-) - Andrea

3
也许这是 Python 中最简单的 setInterval 等价物:
 import threading

 def set_interval(func, sec):
    def func_wrapper():
        set_interval(func, sec) 
        func()  
    t = threading.Timer(sec, func_wrapper)
    t.start()
    return t

内部计时器的引用丢失了。如果第一个计时器已经触发,就没有引用来停止后续的计时器了。 - htellez

2
也许更简单的方法是使用 Timer 的递归调用:
from threading import Timer
import atexit

class Repeat(object):

    count = 0
    @staticmethod
    def repeat(rep, delay, func):
        "repeat func rep times with a delay given in seconds"

        if Repeat.count < rep:
            # call func, you might want to add args here
            func()
            Repeat.count += 1
            # setup a timer which calls repeat recursively
            # again, if you need args for func, you have to add them here
            timer = Timer(delay, Repeat.repeat, (rep, delay, func))
            # register timer.cancel to stop the timer when you exit the interpreter
            atexit.register(timer.cancel)
            timer.start()

def foo():
    print "bar"

Repeat.repeat(3,2,foo)

atexit函数允许使用CTRL-C信号停止程序。


谢谢您的建议,但我已经接受了jd的答案。无论如何,我更喜欢睡觉而不是递归地创建新的计时器 - 顺便问一下,这会产生无限多的线程吗?或者当函数返回时,它们创建的线程是否被终止,即使一个线程仍然处于打开状态? - Andrea
据我所知,它将生成多个线程,当最后一个计时器完成时,它们都会终止。 - Bernhard
1
如果你想重复执行这个函数成千上万次,甚至永远执行下去,那么这可能会很烦人。无论如何,感谢您的输入! :-) - Andrea

0

这个类 Interval

class ali:
    def __init__(self):
        self.sure = True;

    def aliv(self,func,san):
        print "ali naber";
        self.setInterVal(func, san);

    def setInterVal(self,func, san):

        # istenilen saniye veya dakika aralığında program calışır. 
        def func_Calistir():

            func(func,san);  #calışıcak fonksiyon.
        self.t = threading.Timer(san, func_Calistir)
        self.t.start()
        return self.t


a = ali();
a.setInterVal(a.aliv,5);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接