Python 中的延迟函数

10
在JavaScript中,我习惯于能够调用函数以便稍后执行,就像这样。
function foo() {
    alert('bar');
}

setTimeout(foo, 1000);

这不会阻塞其他代码的执行。

我不知道如何在Python中实现类似的功能。我可以使用sleep。

import time
def foo():
    print('bar')

time.sleep(1)
foo()

但这会阻塞其他代码的执行。(实际上在我的情况下,阻塞Python本身并不是问题,但我将无法对该方法进行单元测试。)

我知道线程是为异步执行而设计的,但我想知道是否存在类似于setTimeoutsetInterval的更简单的东西。

6个回答

17

使用事件循环(而非线程)在延迟后执行函数或在给定秒数内重复执行函数,可以采取以下方法:

Tkinter


#!/usr/bin/env python
from Tkinter import Tk

def foo():
    print("timer went off!")

def countdown(n, bps, root):
    if n == 0:
        root.destroy() # exit mainloop
    else:
        print(n)
        root.after(1000 / bps, countdown, n - 1, bps, root)  # repeat the call

root = Tk()
root.withdraw() # don't show the GUI window
root.after(4000, foo) # call foo() in 4 seconds
root.after(0, countdown, 10, 2, root)  # show that we are alive
root.mainloop()
print("done")

输出

10
9
8
7
6
5
4
3
timer went off!
2
1
done

Gtk

#!/usr/bin/env python
from gi.repository import GObject, Gtk

def foo():
    print("timer went off!")

def countdown(n): # note: a closure could have been used here instead
    if n[0] == 0:
        Gtk.main_quit() # exit mainloop
    else:
        print(n[0])
        n[0] -= 1
        return True # repeat the call

GObject.timeout_add(4000, foo) # call foo() in 4 seconds
GObject.timeout_add(500, countdown, [10])
Gtk.main()
print("done")

输出

10
9
8
7
6
5
4
timer went off!
3
2
1
done

Twisted

#!/usr/bin/env python
from twisted.internet import reactor
from twisted.internet.task import LoopingCall

def foo():
    print("timer went off!")

def countdown(n):
    if n[0] == 0:
        reactor.stop() # exit mainloop
    else:
        print(n[0])
        n[0] -= 1

reactor.callLater(4, foo) # call foo() in 4 seconds
LoopingCall(countdown, [10]).start(.5)  # repeat the call in .5 seconds
reactor.run()
print("done")

输出

10
9
8
7
6
5
4
3
timer went off!
2
1
done

异步编程库Asyncio

Python 3.4引入了新的临时API,用于异步IO -asyncio模块

#!/usr/bin/env python3.4
import asyncio

def foo():
    print("timer went off!")

def countdown(n):
    if n[0] == 0:
        loop.stop() # end loop.run_forever()
    else:
        print(n[0])
        n[0] -= 1

def frange(start=0, stop=None, step=1):
    while stop is None or start < stop:
        yield start
        start += step #NOTE: loss of precision over time

def call_every(loop, seconds, func, *args, now=True):
    def repeat(now=True, times=frange(loop.time() + seconds, None, seconds)):
        if now:
            func(*args)
        loop.call_at(next(times), repeat)
    repeat(now=now)

loop = asyncio.get_event_loop()
loop.call_later(4, foo) # call foo() in 4 seconds
call_every(loop, 0.5, countdown, [10]) # repeat the call every .5 seconds
loop.run_forever()
loop.close()
print("done")

输出

10
9
8
7
6
5
4
3
timer went off!
2
1
done

注意:这些方法之间的界面和行为存在细微差别。

2
我一直在寻找Twisted的LoopingCall的aysncio版本。感谢所有的示例! - Dan Gayle

8
你需要从 threading 模块中获取一个 Timer 对象。
from threading import Timer
from time import sleep

def foo():
    print "timer went off!"
t = Timer(4, foo)
t.start()
for i in range(11):
    print i
    sleep(.5)

如果你想要重复执行,这里有一个简单的解决方案:不要使用 Timer,而是使用 Thread,但是传递一个类似于以下函数的工作函数即可:
def call_delay(delay, repetitions, func, *args, **kwargs):             
    for i in range(repetitions):    
        sleep(delay)
        func(*args, *kwargs)

这样做不会产生无限循环,因为如果不正确地执行可能会导致线程无法结束和其他不良行为。更复杂的方法可能使用基于 Event 的方法,像这个


让我提出一个变化。如果我想像 setInterval 一样每 x 秒重复调用该函数怎么办?我可以让函数在结束时启动自己的计时器,但这会无限产生线程吗? - Andrea
@Andrea,是的,那不是一个好的方法。抱歉回复晚了,当时已经是睡觉时间了。请参考上面的第一种方法。 - senderle
谢谢。实际上,在提问后不久,我开始为此行为制作一个装饰器。唯一的问题是如何阻止它永远运行,这得益于这个问题中的输入。https://dev59.com/p2435IYBdhLWcg3w1z4t 我最终更新了带有可工作版本的装饰器的问题 :-) - Andrea
@Andrea,啊,是的,我本来想建议使用“Event”来终止循环。不错的装饰器。 - senderle
基于事件的方法并不需要很复杂,例如call_repeatedly(interval, func, *args) - jfs

4

异步回调,如Javascript的setTimeout,需要事件驱动架构。

Python的异步框架,如流行的twisted,有CallLater可以做到这一点,但这意味着在应用程序中采用事件驱动架构。

另一个选择是使用线程并在线程中休眠。 Python提供了timer来使等待部分变得容易。然而,当您的线程唤醒并且函数执行时,它在一个单独的线程中,并且必须以线程安全的方式执行其操作。


2
抱歉,我不能发布超过2个链接,因此请查看PEP 380和最重要的是 asyncio 的文档以获取更多信息。

asyncio是这种问题的首选解决方案,除非您坚持使用线程或多进程。它由GvR设计和实现,名为“Tulip”。它在PyCon 2013上由GvR介绍,旨在成为支配(和标准化)所有事件循环(如twisted、gevent等)并使它们彼此兼容的唯一事件循环。 asyncio之前已经被提到过,但其真正的威力是通过yield from释放的。

# asyncio is in standard lib for latest python releases (since 3.3)
import asyncio

# there's only one event loop, let's fetch that
loop = asyncio.get_event_loop()

# this is a simple reminder that we're dealing with a coro
@asyncio.coroutine
def f():
    for x in range(10):
        print(x)
        # we return with a coroutine-object from the function, 
        # saving the state of the execution to return to this point later
        # in this case it's a special sleep
        yield from asyncio.sleep(3)

# one of a few ways to insert one-off function calls into the event loop
loop.call_later(10, print, "ding!")
# we insert the above function to run until the coro-object from f is exhausted and 
# raises a StopIteration (which happens when the function would return normally)
# this also stops the loop and cleans up - keep in mind, it's not DEAD but can be restarted
loop.run_until_complete(f())
# this closes the loop - now it's DEAD
loop.close()

================

>>> 
0
1
2
3
ding!
4
5
6
7
8
9
>>>

请问您能否提供更多关于您回答的细节? - abarisone
当然,你有什么特别想要我添加的吗?我可以注释这些行,或许能更清楚地说明它的工作原理。 - Amo

1

JavaScript之所以能够做到这一点,是因为它在事件循环中运行。在Python中,可以通过使用像Twisted这样的事件循环或通过GLib或Qt等工具包来实现。


0
问题在于您的普通Python脚本无法在框架中运行。脚本被调用并控制主循环。使用JavaScript,页面上运行的所有脚本都在框架中运行,当超时时间到达时,框架会调用您的方法。
我自己没有使用过pyQt(只用过C++ Qt),但是您可以使用startTimer()在任何QObject上设置定时器。当计时器到期时,将调用您的方法回调。您还可以使用QTimer,并将超时信号连接到任意插槽。这是可能的,因为Qt运行一个事件循环,可以在稍后调用您的方法。

感谢您的回答,但仅为了这种方法而将我的代码转换为Qt应用程序并不是一个选项。 - Andrea

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