每x秒重复执行一个函数的最佳方法是什么?


281

我想永远每60秒重复执行一次Python中的函数(就像Objective C中的NSTimer一样)。这段代码将作为守护程序运行,实际上就像使用cron每一分钟调用python脚本一样,但是不需要用户设置。

有关使用Python实现的cron的问题中,该解决方案似乎实际上只是将sleep()停留了x秒。我不需要这种高级功能,所以也许这样的事情会起作用

while True:
    # Code executed here
    time.sleep(60)

此代码是否存在任何可预见的问题?


83
有一点很重要,但可能很关键,您的代码上方的代码不会每60秒执行一次,这会使执行之间间隔60秒。如果您执行的代码根本不花时间,它只会每60秒发生一次。
西蒙

4
time.sleep(60)可能
早晚

5
我仍然在想:此代码是否存在任何可预见的问题?
香蕉

1
“可预见的问题”是您不能仅使用time.sleep(60)来预期每小时60次迭代。因此,如果您要在每次迭代中添加一个项目并保留一个设置长度的列表,则该列表的平均值将不会代表一致的时间。因此“移动平均值”之类的功能可能会引用过旧的数据点,这会使您的指示失真。
litepresence

2
@Banana是的,您可能会遇到任何问题,因为您的脚本并非每60秒执行一次。例如。我开始做这样的事情来分割视频流并上传它们,但由于在处理循环中的数据时媒体队列正在缓冲,所以我得到了5-10秒钟以上的strem。这取决于您的数据。如果功能是某种简单的看门狗警告您,例如当磁盘已满时,您应该完全没有问题。如果您正在检查核电站的警告警报,您可能会死于城市完全炸毁了
DGoiko '18年

Answers:


227

如果您的程序还没有事件循环,请使用sched模块,该模块实现了通用事件调度程序。

import sched, time
s = sched.scheduler(time.time, time.sleep)
def do_something(sc): 
    print("Doing stuff...")
    # do your stuff
    s.enter(60, 1, do_something, (sc,))

s.enter(60, 1, do_something, (s,))
s.run()

如果你已经在使用一个事件循环库一样asynciotriotkinterPyQt5gobjectkivy,和许多其他人-只是用您现有的事件循环库的方法安排任务,来代替。


16
sched模块用于安排函数在一段时间后运行,如何使用它每隔x秒重复一次函数调用,而不使用time.sleep()?
Baishampayan Ghose

2
@Baishampayan:安排一个新的运行。
nosklo

3
然后apscheduler在packages.python.org/APScheduler也应该得到在这一点上提。
Daniel F

6
注意:此版本可能会漂移。您enterabs()可以避免使用它。这是一个非漂移版本,用于比较
2014年

8
@JavaSa:因为“做您的工作”不是瞬时的,并且错误time.sleep可能会在此处累积。“每X秒执行一次”和“重复执行〜X秒的延迟”并不相同。另请参阅 此评论
jfs

180

只需将您的时间循环锁定到系统时钟即可。简单。

import time
starttime=time.time()
while True:
  print "tick"
  time.sleep(60.0 - ((time.time() - starttime) % 60.0))

22
+1。您和twisted答案是每秒钟运行一次函数的唯一答案x。其余部分x在每次调用后以几秒钟的延迟执行该功能。
jfs

13
如果您要在其中添加一些代码,而这些代码却花费了超过一秒钟的时间……这会浪费时间并开始落后。.在这种情况下,可接受的答案是正确的……任何人都可以循环执行简单的打印命令并让它每秒都没有延迟地运行...
愤怒84年

5
from time import time, sleep由于存在的原因,我更喜欢;)

14
出色地工作。starttime如果您要先将其同步到某个时间,则无需减去您的数据:time.sleep(60 - time.time() % 60)对于我来说一切正常。我用过它time.sleep(1200 - time.time() % 1200),它使我可以按照自己的意愿登录:00 :20 :40
TemporalWolf

2
@AntonSchigur避免多次迭代后的漂移。一个单独的迭代可能略有迟早启动取决于sleep()timer()精度,需要多长时间来执行循环体,但平均迭代在区间边界总是出现(即使某些被跳过): while keep_doing_it(): sleep(interval - timer() % interval)。将其与while keep_doing_it(): sleep(interval)几次迭代后可能累积错误的位置进行比较。
jfs

71

您可能需要考虑Twisted,它是实现Reactor Pattern的Python网络库。

from twisted.internet import task, reactor

timeout = 60.0 # Sixty seconds

def doWork():
    #do work here
    pass

l = task.LoopingCall(doWork)
l.start(timeout) # call every sixty seconds

reactor.run()

虽然“ while True:sleep(60)”可能会起作用Twisted可能已经实现了您最终将需要的许多功能(bobince指出的守护进程,日志记录或异常处理),并且可能是一种更强大的解决方案


答案也很好,非常准确,没有漂移。我想知道这是否会使CPU在等待执行任务时也进入睡眠状态(也就是不忙于等待)?
smoothware

1
这漂移的时间是毫秒级
Derek Eden

“漂移在毫秒级”是什么意思?
Jean-Paul Calderone

67

如果您希望采用非阻塞方式定期执行函数,而不是阻塞无限循环,我会使用线程计时器。这样,您的代码可以继续运行并执行其他任务,并且仍然每n秒调用一次函数。我经常使用这种技术来打印长时间的,CPU /磁盘/网络密集型任务的进度信息。

这是我在类似问题中发布的代码,其中包含start()和stop()控件:

from threading import Timer

class RepeatedTimer(object):
    def __init__(self, interval, function, *args, **kwargs):
        self._timer     = None
        self.interval   = interval
        self.function   = function
        self.args       = args
        self.kwargs     = kwargs
        self.is_running = False
        self.start()

    def _run(self):
        self.is_running = False
        self.start()
        self.function(*self.args, **self.kwargs)

    def start(self):
        if not self.is_running:
            self._timer = Timer(self.interval, self._run)
            self._timer.start()
            self.is_running = True

    def stop(self):
        self._timer.cancel()
        self.is_running = False

用法:

from time import sleep

def hello(name):
    print "Hello %s!" % name

print "starting..."
rt = RepeatedTimer(1, hello, "World") # it auto-starts, no need of rt.start()
try:
    sleep(5) # your long-running job goes here...
finally:
    rt.stop() # better in a try/finally block to make sure the program ends!

特征:

  • 仅标准库,无外部依赖
  • start()并且stop()即使计时器已经启动/停止也可以安全地多次拨打
  • 要调用的函数可以具有位置和命名参数
  • 您可以interval随时更改,它将在下次运行后生效。同样argskwargs甚至function

这个解决方案似乎随着时间的流逝而变化。我需要一个旨在每n秒调用一次函数而又不会漂移的版本。我将在另一个问题中发布更新。
–eraoul

def _run(self)我想弄清楚你为什么打电话self.start()之前self.function()。你能详细说明吗?我认为start()先调用self.is_running总是False如此,然后我们总是会启动一个新线程。
Rich Episcopo

1
我想我已经探究了。@MestreLion的解决方案每秒钟运行一次函数x(即t = 0,t = 1x,t = 2x,t = 3x等),其中在原始海报示例代码中,运行函数间隔为x秒。另外,我认为此解决方案如果interval比执行时间短,则存在错误function。在这种情况下,self._timerstart函数将被覆盖。
Rich Episcopo

是的,@RichieEpiscopo,调用.function().start()是在t = 0运行该函数。而且我认为如果function花费的时间比更长interval,这不会有问题,但是是的,代码中可能存在某些竞争条件。
MestreLion

这是我可以获得的唯一非阻塞方式。谢谢。
backslashN

35

我认为更简单的方法是:

import time

def executeSomething():
    #code here
    time.sleep(60)

while True:
    executeSomething()

这样,您的代码将被执行,然后等待60秒,然后再次执行,然后等待执行,等等。。。


关键字真正的应该是大写
肖恩·凯恩

38
实际上,这不是答案:time sleep()只能在每次执行后等待X秒。例如,如果您的函数执行需要0.5秒,并且您使用time.sleep(1),则意味着您的函数每1.5秒执行一次,而不是每1秒执行一次。您应该使用其他模块和/或线程来确保某些函数可以工作Y次每X秒。
kommradHomer 2013年

1
@kommradHomer:Dave Rove的答案表明,您可以time.sleep()每隔X秒使用一次运行内容
jfs 2014年

2
在我看来,代码应该调用time.sleep()while True像循环:def executeSomething(): print('10 sec left') ; while True: executeSomething(); time.sleep(10)
伦纳德Lepadatu

22
import time, traceback

def every(delay, task):
  next_time = time.time() + delay
  while True:
    time.sleep(max(0, next_time - time.time()))
    try:
      task()
    except Exception:
      traceback.print_exc()
      # in production code you might want to have this instead of course:
      # logger.exception("Problem while executing repetitive task.")
    # skip tasks if we are behind schedule:
    next_time += (time.time() - next_time) // delay * delay + delay

def foo():
  print("foo", time.time())

every(5, foo)

如果要执行此操作而又不阻塞其余代码,则可以使用它使它在自己的线程中运行:

import threading
threading.Thread(target=lambda: every(5, foo)).start()

该解决方案结合了其他解决方案中很少发现的几个功能:

  • 异常处理:尽可能在此级别上正确处理异常,即在不中断程序的情况下记录日志以进行调试。
  • 没有链接:在许多答案中发现的常见链式实现(用于安排下一个事件的安排)在以下方面是脆弱的:如果在安排机制内出现任何问题(threading.Timer或其他任何原因)出了问题,它将终止链。即使问题的原因已经解决,也不会再执行任何操作。与之sleep()相比,简单的循环和简单的等待要健壮得多。
  • 无漂移:永无止境我的解决方案准确跟踪了预期运行时间。取决于执行时间,没有漂移(与许多其他解决方案一样)。
  • 跳过:如果一次执行花费了太多时间(例如,每五秒钟执行X次,但是X花费6秒执行一次),我的解决方案将跳过任务。这是标准的cron行为(并且有充分的理由)。然后,许多其他解决方案只需连续执行几次任务即可,而不会出现任何延迟。对于大多数情况(例如清理任务),这是不希望的。如果希望,简单地使用next_time += delay来代替。

2
不漂流的最佳答案。
塞巴斯蒂安·史塔克

1
@PirateApp我会在其他线程中执行此操作。您可以在同一线程中执行此操作,但是最终您要编写自己的调度系统,这对于注释来说太复杂了。
Alfe

1
在Python中,借助GIL,在两个线程中访问变量是绝对安全的。而且,仅读取两个线程就永远不会成为问题(在其他线程环境中也不会出现问题)。只有在没有GIL的系统中(例如Java,C ++等)从两个不同的线程进行写入时,才需要进行一些显式的同步。
Alfe

1
@ user50473如果没有其他信息,我将首先从线程侧处理任务。一个线程会不时读取数据,然后休眠直到再次执行该操作为止。上面的解决方案当然可以用来做到这一点。但是我可以想象出很多理由采取不同的方式。祝您好运:)
Alfe

1
可以用线程代替睡眠。事件等待超时,以便在应用程序退出时更加敏感。stackoverflow.com/questions/29082268/…–
themadmax

20

这是对MestreLion的代码的更新,可以避免随着时间的推移而变坏。

在此,RepeatedTimer类按照OP的请求每隔“间隔”秒调用给定函数;时间表不取决于函数执行所需的时间。我喜欢此解决方案,因为它没有外部库依赖项;这只是纯python。

import threading 
import time

class RepeatedTimer(object):
  def __init__(self, interval, function, *args, **kwargs):
    self._timer = None
    self.interval = interval
    self.function = function
    self.args = args
    self.kwargs = kwargs
    self.is_running = False
    self.next_call = time.time()
    self.start()

  def _run(self):
    self.is_running = False
    self.start()
    self.function(*self.args, **self.kwargs)

  def start(self):
    if not self.is_running:
      self.next_call += self.interval
      self._timer = threading.Timer(self.next_call - time.time(), self._run)
      self._timer.start()
      self.is_running = True

  def stop(self):
    self._timer.cancel()
    self.is_running = False

用法示例(从MestreLion的答案中复制):

from time import sleep

def hello(name):
    print "Hello %s!" % name

print "starting..."
rt = RepeatedTimer(1, hello, "World") # it auto-starts, no need of rt.start()
try:
    sleep(5) # your long-running job goes here...
finally:
    rt.stop() # better in a try/finally block to make sure the program ends!

5

不久前,我也遇到了类似的问题。可能是http://cronus.readthedocs.org可能有帮助?

对于v0.2,以下代码段有效

import cronus.beat as beat

beat.set_rate(2) # 2 Hz
while beat.true():
    # do some time consuming work here
    beat.sleep() # total loop duration would be 0.5 sec

4

那和cron之间的主要区别是,异常将永久杀死守护程序。您可能需要包装异常捕获器和记录器。


4

一个可能的答案:

import time
t=time.time()

while True:
    if time.time()-t>10:
        #run your task here
        t=time.time()

1
这很忙,因此非常糟糕。
Alfe

对于寻找无阻塞计时器的人的好解决方案。
Noel

3

我最终使用了调度模块。该API很不错。

import schedule
import time

def job():
    print("I'm working...")

schedule.every(10).minutes.do(job)
schedule.every().hour.do(job)
schedule.every().day.at("10:30").do(job)
schedule.every(5).to(10).minutes.do(job)
schedule.every().monday.do(job)
schedule.every().wednesday.at("13:15").do(job)
schedule.every().minute.at(":17").do(job)

while True:
    schedule.run_pending()
    time.sleep(1)

我在尝试使用此模块时遇到了困难,我需要取消阻止主线程,已经在日程安排的文档网站上查看了FAQ,但是我并不真正理解所提供的解决方法。有谁知道我在哪里可以找到不阻塞主线程的工作示例?
Daydreams

1

我使用Tkinter after()方法,它不会“窃取游戏”(就像前面介绍的sched模块一样),即它允许其他事情并行运行:

import Tkinter

def do_something1():
  global n1
  n1 += 1
  if n1 == 6: # (Optional condition)
    print "* do_something1() is done *"; return
  # Do your stuff here
  # ...
  print "do_something1() "+str(n1)
  tk.after(1000, do_something1)

def do_something2(): 
  global n2
  n2 += 1
  if n2 == 6: # (Optional condition)
    print "* do_something2() is done *"; return
  # Do your stuff here
  # ...
  print "do_something2() "+str(n2)
  tk.after(500, do_something2)

tk = Tkinter.Tk(); 
n1 = 0; n2 = 0
do_something1()
do_something2()
tk.mainloop()

do_something1()do_something2()可以以任意速度间隔并行运行。在这里,第二个函数的执行速度是第二个函数的两倍。还要注意,我使用一个简单的计数器作为终止这两个函数的条件。您可以使用任何喜欢的其他条件,也可以不使用任何条件,直到在程序终止之前要运行什么功能(例如时钟)。


措辞要小心:after不允许事物并行运行。Tkinter是单线程的,一次只能做一件事。如果调度程序after正在运行,则它不会与其余代码并行运行。如果同时do_something1do_something2被安排在同一时间运行,它们将按顺序运行,而不是并行。
布莱恩·奥克利

@Apostolos所有的解决方案确实是使用Tkinter的主循环,而不是章附表主循环,所以它的工作原理完全一样,但是它允许Tkinter的接口继续响应。如果您不使用tkinter做其他事情,那么就调度解决方案而言,它不会有任何改变。您可以在sched解决方案中以不同的间隔使用两个或多个计划的函数,它的工作原理与您完全相同。
nosklo

不,它的工作方式不同。我解释了这一点。一个“锁定”程序(即停止流程,您不能做其他任何事情-甚至不按照您的建议开始其他已完成的工作),直到程序完成,另一个让您的手/自由释放(即您可以它开始后还有其他事情。您不必等到它完成。这是一个巨大的差异。如果您尝试了我介绍的方法,那么您会自己发现的。我已经尝试了您的方法。为什么不也尝试我的吗?
Apostolos

1

这是MestreLion的代码的改编版本。除了原始功能外,此代码:

1)添加用于在特定时间触发计时器的first_interval(调用者需要计算first_interval并传入)

2)解决原始代码中的竞争条件。在原始代码中,如果控制线程未能取消正在运行的计时器(“停止计时器,并取消执行计时器的操作。这仅在计时器仍处于等待阶段时才有效。”引自https:// docs.python.org/2/library/threading.html),计时器将无休止地运行。

class RepeatedTimer(object):
def __init__(self, first_interval, interval, func, *args, **kwargs):
    self.timer      = None
    self.first_interval = first_interval
    self.interval   = interval
    self.func   = func
    self.args       = args
    self.kwargs     = kwargs
    self.running = False
    self.is_started = False

def first_start(self):
    try:
        # no race-condition here because only control thread will call this method
        # if already started will not start again
        if not self.is_started:
            self.is_started = True
            self.timer = Timer(self.first_interval, self.run)
            self.running = True
            self.timer.start()
    except Exception as e:
        log_print(syslog.LOG_ERR, "timer first_start failed %s %s"%(e.message, traceback.format_exc()))
        raise

def run(self):
    # if not stopped start again
    if self.running:
        self.timer = Timer(self.interval, self.run)
        self.timer.start()
    self.func(*self.args, **self.kwargs)

def stop(self):
    # cancel current timer in case failed it's still OK
    # if already stopped doesn't matter to stop again
    if self.timer:
        self.timer.cancel()
    self.running = False

1

这似乎比公认的解决方案简单得多-是否有我没有考虑的缺点?来到这里寻找一些简单的复制面食,感到失望。

import threading, time

def print_every_n_seconds(n=2):
    while True:
        print(time.ctime())
        time.sleep(n)

thread = threading.Thread(target=print_every_n_seconds, daemon=True)
thread.start()

异步输出。

#Tue Oct 16 17:29:40 2018
#Tue Oct 16 17:29:42 2018
#Tue Oct 16 17:29:44 2018

从某种意义上说,它确实存在偏差,如果正在运行的任务花费大量时间,则间隔变为2秒+任务时间,因此,如果您需要精确的计划,那么这不适合您。

请注意,该daemon=True标志表示该线程不会阻止应用程序关闭。例如,有一个问题,pytest在运行测试等待该主题停止运行后,会无限期挂起。


不,它只打印第一个日期时间,然后停止...
Alex Poca

您确定吗-我只是复制并粘贴到终端中。它立即返回,但打印输出继续在后台运行。
亚当·休斯

看来我在这里缺少什么。我将代码复制/粘贴到test.py中,并使用python test.py运行。使用Python2.7时,我需要删除无法识别的daemon = True,并且可以读取多张照片。使用Python3.8时,它将在第一次打印后停止,并且在结束后没有任何活动。删除守护程序=正确,我阅读了多份印刷品...
亚历克斯·波卡

嗯,很奇怪-我在python 3.6.10上,但是不知道为什么会很重要
Adam Hughes

再说一次:Python3.4.2(Debian GNU / Linux 8(jessie)),必须删除daemon = True以便可以多次打印。使用守护程序时, 出现语法错误。先前使用Python2.7和3.8进行的测试是在Ubuntu 19.10上进行的,是否可以根据操作系统对守护进程进行不同的处理?
Alex Poca

0

我用它来每小时导致60个事件,而大多数事件在整分钟后的相同秒数内发生:

import math
import time
import random

TICK = 60 # one minute tick size
TICK_TIMING = 59 # execute on 59th second of the tick
TICK_MINIMUM = 30 # minimum catch up tick size when lagging

def set_timing():

    now = time.time()
    elapsed = now - info['begin']
    minutes = math.floor(elapsed/TICK)
    tick_elapsed = now - info['completion_time']
    if (info['tick']+1) > minutes:
        wait = max(0,(TICK_TIMING-(time.time() % TICK)))
        print ('standard wait: %.2f' % wait)
        time.sleep(wait)
    elif tick_elapsed < TICK_MINIMUM:
        wait = TICK_MINIMUM-tick_elapsed
        print ('minimum wait: %.2f' % wait)
        time.sleep(wait)
    else:
        print ('skip set_timing(); no wait')
    drift = ((time.time() - info['begin']) - info['tick']*TICK -
        TICK_TIMING + info['begin']%TICK)
    print ('drift: %.6f' % drift)

info['tick'] = 0
info['begin'] = time.time()
info['completion_time'] = info['begin'] - TICK

while 1:

    set_timing()

    print('hello world')

    #random real world event
    time.sleep(random.random()*TICK_MINIMUM)

    info['tick'] += 1
    info['completion_time'] = time.time()

根据实际情况,您可能会发现一些问题:

60,60,62,58,60,60,120,30,30,60,60,60,60,60...etc.

但在60分钟结束时,您将有60个滴答声;并且大多数会以您希望的分钟数正确偏移。

在我的系统上,我得到的典型漂移小于<1/20秒,直到需要校正为止。

这种方法的优点是可以解决时钟漂移问题。如果您正在做一些事情,例如每滴答声追加一项,而您希望每小时追加60项,则可能会引起问题。不能考虑漂移会导致诸如移动平均线之类的次要指示,以至于认为数据太过深了而导致输出错误。


0

例如,显示当前本地时间

import datetime
import glib
import logger

def get_local_time():
    current_time = datetime.datetime.now().strftime("%H:%M")
    logger.info("get_local_time(): %s",current_time)
    return str(current_time)

def display_local_time():
    logger.info("Current time is: %s", get_local_time())
    return True

# call every minute
glib.timeout_add(60*1000, display_local_time)

0
    ''' tracking number of times it prints'''
import threading

global timeInterval
count=0
def printit():
  threading.Timer(timeInterval, printit).start()
  print( "Hello, World!")
  global count
  count=count+1
  print(count)
printit

if __name__ == "__main__":
    timeInterval= int(input('Enter Time in Seconds:'))
    printit()

根据用户输入,它将在每个时间间隔内迭代该方法。
raviGupta

0

这是不使用任何额外库的另一种解决方案。

def delay_until(condition_fn, interval_in_sec, timeout_in_sec):
    """Delay using a boolean callable function.

    `condition_fn` is invoked every `interval_in_sec` until `timeout_in_sec`.
    It can break early if condition is met.

    Args:
        condition_fn     - a callable boolean function
        interval_in_sec  - wait time between calling `condition_fn`
        timeout_in_sec   - maximum time to run

    Returns: None
    """
    start = last_call = time.time()
    while time.time() - start < timeout_in_sec:
        if (time.time() - last_call) > interval_in_sec:
            if condition_fn() is True:
                break
            last_call = time.time()
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.