發(fā)布于:2021-02-03 11:45:20
0
2038
0
您是否需要讓Python程序等待一些東西?大多數(shù)情況下,您希望代碼能夠盡快執(zhí)行。但有時(shí),讓代碼休眠一段時(shí)間實(shí)際上符合您的最佳利益。
例如,您可以使用Pythonsleep()調(diào)用來模擬程序中的延遲。也許您需要等待文件上傳或下載,或者等待圖形加載或繪制到屏幕上。您甚至可能需要在調(diào)用Web API或查詢數(shù)據(jù)庫之間暫停。sleep()在每種情況下,向程序添加Python調(diào)用都可以提供幫助,甚至更多!
本文的目標(biāo)讀者是希望增加Python知識(shí)的中級(jí)開發(fā)人員。如果這聽起來像你,那我們就開始吧!
使用添加Pythonsleep()調(diào)用time.sleep()
內(nèi)置支持使程序進(jìn)入睡眠狀態(tài)。time
模塊有一個(gè)函數(shù)sleep()
,您可以使用它來暫停調(diào)用線程的執(zhí)行,不管您指定了多少秒。
下面是一個(gè)如何使用time.sleep()
的示例:
>>> import time
>>> time.sleep(3) # Sleep for 3 seconds
如果您在控制臺(tái)中運(yùn)行此代碼,那么您應(yīng)該會(huì)遇到一個(gè)延遲可以在REPL中輸入新語句。
注意:在Python3.5中,核心開發(fā)人員稍微更改了time.sleep()
的行為。新的Pythonsleep()
系統(tǒng)調(diào)用將至少持續(xù)指定的秒數(shù),即使睡眠被信號(hào)中斷。但是,如果信號(hào)本身引發(fā)異常,則這不適用。
您可以使用Python的timeit
模塊測試睡眠持續(xù)時(shí)間:
$ python3 -m timeit -n 3 "import time; time.sleep(3)"
3 loops, best of 3: 3 sec per loop
在這里,您使用-n
參數(shù)運(yùn)行timeit
模塊,該參數(shù)告訴timeit
運(yùn)行語句的次數(shù)接下來就是。您可以看到timeit
運(yùn)行語句3次,最佳運(yùn)行時(shí)間是3秒,這是預(yù)期的。
默認(rèn)的timeit
運(yùn)行代碼的次數(shù)是一百萬次。如果您使用默認(rèn)的-n
運(yùn)行上述代碼,那么每次迭代3秒時(shí),您的終端將掛起大約34天!timeit
模塊還有其他幾個(gè)命令行選項(xiàng),您可以在它的文檔中查看這些選項(xiàng)。
讓我們創(chuàng)建一些更真實(shí)的選項(xiàng)。系統(tǒng)管理員需要知道他們的網(wǎng)站何時(shí)關(guān)閉。您希望能夠定期檢查網(wǎng)站的狀態(tài)代碼,但不能經(jīng)常查詢web服務(wù)器,否則會(huì)影響性能。執(zhí)行此檢查的一種方法是使用Pythonsleep()
系統(tǒng)調(diào)用:
import time
import urllib.request
import urllib.error
def uptime_bot(url):
while True:
try:
conn = urllib.request.urlopen(url)
except urllib.error.HTTPError as e:
# Email admin / log
print(f'HTTPError: {e.code} for {url}')
except urllib.error.URLError as e:
# Email admin / log
print(f'URLError: {e.code} for {url}')
else:
# Website is up
print(f'{url} is up')
time.sleep(60)
if __name__ == '__main__':
url = 'http://www.google.com/py'
uptime_bot(url)
在這里創(chuàng)建uptime_bot()
,它將URL作為其參數(shù)。然后,該函數(shù)嘗試使用urllib
打開該URL。如果有一個(gè)HTTPError
或URLError
,那么程序?qū)⒉东@它并打印出錯(cuò)誤。(在實(shí)時(shí)環(huán)境中,您將記錄錯(cuò)誤,并可能向網(wǎng)站管理員或系統(tǒng)管理員發(fā)送電子郵件。)
如果沒有出現(xiàn)錯(cuò)誤,那么您的代碼將打印出一切正常。不管發(fā)生什么,你的程序都會(huì)休眠60秒。這意味著你每分鐘只訪問一次網(wǎng)站。本例中使用的URL不正確,因此它將每分鐘向控制臺(tái)輸出一次以下內(nèi)容:
HTTPError: 404 for http://www.google.com/py
繼續(xù)并更新代碼以使用已知的好URL,如http://www.google.com
。然后您可以重新運(yùn)行它以查看它是否成功工作。您還可以嘗試更新代碼以發(fā)送電子郵件或記錄錯(cuò)誤。有關(guān)如何執(zhí)行此操作的詳細(xì)信息,請(qǐng)查看使用Python發(fā)送電子郵件和登錄Python。
sleep()使用Decorators添加Python調(diào)用
有時(shí)需要重試失敗的函數(shù)。一個(gè)流行的使用案例是,當(dāng)您因?yàn)榉?wù)器忙而需要重試文件下載時(shí)。您通常不想太頻繁地向服務(wù)器發(fā)出請(qǐng)求,因此在每個(gè)請(qǐng)求之間添加一個(gè)Python調(diào)用是可取的。
我親身經(jīng)歷的另一個(gè)用例是在自動(dòng)測試期間需要檢查用戶界面的狀態(tài)。用戶界面的加載速度可能比平時(shí)快,也可能慢,這取決于我運(yùn)行測試的計(jì)算機(jī)。這會(huì)改變程序正在驗(yàn)證某個(gè)內(nèi)容時(shí)屏幕上的內(nèi)容。
在這種情況下,我可以讓程序休眠片刻,然后在一兩秒鐘后重新檢查。這可能意味著通過測試和失敗測試之間的區(qū)別。
在這兩種情況下,您都可以使用裝飾程序添加Pythonsleep()
系統(tǒng)調(diào)用。如果您不熟悉decorators,或者您想對(duì)它們進(jìn)行深入了解,那么請(qǐng)查看Python decorators入門。讓我們看一個(gè)例子:
import time
import urllib.request
import urllib.error
def sleep(timeout, retry=3):
def the_real_decorator(function):
def wrapper(*args, **kwargs):
retries = 0
while retries < retry:
try:
value = function(*args, **kwargs)
if value is None:
return
except:
print(f'Sleeping for {timeout} seconds')
time.sleep(timeout)
retries += 1
return wrapper
return the_real_decorator
sleep()
是您的裝飾者。它接受一個(gè)timeout
值和它應(yīng)該retry
的次數(shù),默認(rèn)值為3。在sleep()
內(nèi)部是另一個(gè)函數(shù),the_real_decorator()
,它接受修飾函數(shù)。
最后,最內(nèi)部的函數(shù)wrapper()
接受傳遞給修飾函數(shù)的參數(shù)和關(guān)鍵字參數(shù)。這就是魔法發(fā)生的地方!使用while
循環(huán)重試調(diào)用函數(shù)。如果出現(xiàn)異常,則調(diào)用time.sleep()
,增加retries
計(jì)數(shù)器,然后再次嘗試運(yùn)行該函數(shù)。
現(xiàn)在重寫uptime_bot()
以使用新的裝飾器:
@sleep(3)
def uptime_bot(url):
try:
conn = urllib.request.urlopen(url)
except urllib.error.HTTPError as e:
# Email admin / log
print(f'HTTPError: {e.code} for {url}')
# Re-raise the exception for the decorator
raise urllib.error.HTTPError
except urllib.error.URLError as e:
# Email admin / log
print(f'URLError: {e.code} for {url}')
# Re-raise the exception for the decorator
raise urllib.error.URLError
else:
# Website is up
print(f'{url} is up')
if __name__ == '__main__':
url = 'http://www.google.com/py'
uptime_bot(url)
這里,您用3秒的sleep()
來裝飾uptime_bot()
。您還刪除了原始的while
循環(huán),以及對(duì)sleep(60)
的舊調(diào)用。裝飾程序現(xiàn)在處理這個(gè)問題。
您所做的另一個(gè)更改是在異常處理塊中添加一個(gè)raise
。這是為了裝飾工能正常工作。您可以編寫decorator來處理這些錯(cuò)誤,但是由于這些異常只適用于urllib
,因此最好保持decorator的原樣。這樣,它將適用于更廣泛的函數(shù)。
注意:如果您想了解Python中的異常處理,請(qǐng)查看Python異常:簡介。
您可以對(duì)decorator進(jìn)行一些改進(jìn)。如果它的重試次數(shù)用完,仍然失敗,那么您可以讓它重新引發(fā)最后一個(gè)錯(cuò)誤。最后一次失敗后,裝飾程序還會(huì)等待3秒鐘,這可能是您不希望發(fā)生的事情。你可以把這些當(dāng)作練習(xí)來嘗試!
sleep()使用線程添加Python調(diào)用
有時(shí)候,您可能想將Pythonsleep()調(diào)用添加到線程中。也許您正在對(duì)具有數(shù)百萬條記錄的數(shù)據(jù)庫運(yùn)行遷移腳本。您不想造成任何停機(jī)時(shí)間,但是您也不想等待比完成遷移所需的時(shí)間更長的時(shí)間,因此決定使用線程。
注意:線程是Python中執(zhí)行并發(fā)的一種方法。您可以一次運(yùn)行多個(gè)線程以提高應(yīng)用程序的吞吐量。如果您不熟悉Python中的線程,請(qǐng)查看Python中的線程簡介。
為了防止客戶注意到任何類型的減速,每個(gè)線程都需要運(yùn)行一段時(shí)間,然后休眠。有兩種方法可以做到這一點(diǎn):
像以前一樣使用time.sleep()
。
使用threading
模塊中的Event.wait()
。
讓我們先看看time.sleep()
使用time.sleep()
Python日志記錄烹飪書展示了一個(gè)使用time.sleep()
的好例子。Python的logging
模塊是線程安全的,因此在本練習(xí)中,它比print()
語句更有用。下面的代碼基于此示例:
import logging
import threading
import time
def worker(arg):
while not arg["stop"]:
logging.debug("worker thread checking in")
time.sleep(1)
def main():
logging.basicConfig(
level=logging.DEBUG,
format="%(relativeCreated)6d %(threadName)s %(message)s"
)
info = {"stop": False}
thread = threading.Thread(target=worker, args=(info,))
thread_two = threading.Thread(target=worker, args=(info,))
thread.start()
thread_two.start()
while True:
try:
logging.debug("Checking in from main thread")
time.sleep(0.75)
except KeyboardInterrupt:
info["stop"] = True
logging.debug('Stopping')
break
thread.join()
thread_two.join()
if __name__ == "__main__":
main()
這里,您使用Python的threading
模塊創(chuàng)建兩個(gè)線程。您還可以創(chuàng)建一個(gè)日志對(duì)象,將threadName
日志記錄到stdout。接下來,啟動(dòng)兩個(gè)線程并每隔一段時(shí)間從主線程啟動(dòng)一個(gè)循環(huán)來記錄日志。您可以使用KeyboardInterrupt
捕捉用戶按下+
嘗試在終端中運(yùn)行上述代碼。您將看到類似于以下內(nèi)容的輸出:
0 Thread-1 worker thread checking in
1 Thread-2 worker thread checking in
1 MainThread Checking in from main thread
752 MainThread Checking in from main thread
1001 Thread-1 worker thread checking in
1001 Thread-2 worker thread checking in
1502 MainThread Checking in from main thread
2003 Thread-1 worker thread checking in
2003 Thread-2 worker thread checking in
2253 MainThread Checking in from main thread
3005 Thread-1 worker thread checking in
3005 MainThread Checking in from main thread
3005 Thread-2 worker thread checking in
當(dāng)每個(gè)線程運(yùn)行然后休眠時(shí),日志輸出將打印到控制臺(tái)。現(xiàn)在您已經(jīng)嘗試了一個(gè)示例,您將能夠在自己的代碼中使用這些概念。
使用Event.wait()
模塊提供了一個(gè)Event()
,您可以像time.sleep()
一樣使用它。然而,Event()
還有一個(gè)額外的好處,那就是反應(yīng)更快。原因是當(dāng)事件被設(shè)置時(shí),程序?qū)⒘⒓刺鲅h(huán)。使用time.sleep()
時(shí),代碼需要等待Pythonsleep()
調(diào)用完成,線程才能退出。
之所以要在這里使用wait()
是因?yàn)?code>wait()是非阻塞的,而time.sleep()
是阻塞的。這意味著,當(dāng)您使用time.sleep()
時(shí),將阻止主線程在等待sleep()
調(diào)用結(jié)束時(shí)繼續(xù)運(yùn)行。wait()
解決了這個(gè)問題。您可以在Python的線程文檔中閱讀更多關(guān)于這些工作原理的信息。
下面是如何使用Event.wait()
添加Pythonsleep()
調(diào)用:
import logging
import threading
def worker(event):
while not event.isSet():
logging.debug("worker thread checking in")
event.wait(1)
def main():
logging.basicConfig(
level=logging.DEBUG,
format="%(relativeCreated)6d %(threadName)s %(message)s"
)
event = threading.Event()
thread = threading.Thread(target=worker, args=(event,))
thread_two = threading.Thread(target=worker, args=(event,))
thread.start()
thread_two.start()
while not event.isSet():
try:
logging.debug("Checking in from main thread")
event.wait(0.75)
except KeyboardInterrupt:
event.set()
break
if __name__ == "__main__":
main()
在本例中,您創(chuàng)建threading.Event()
并將其傳遞給worker()
。(回想一下,在上一個(gè)示例中,您傳遞了一個(gè)dictionary。)接下來,設(shè)置循環(huán)以檢查是否設(shè)置了event
。如果不是,那么代碼將打印一條消息并等待一段時(shí)間,然后再次檢查。要設(shè)置事件,可以按Ctrl+C。設(shè)置事件后,worker()
將返回,循環(huán)將中斷,從而結(jié)束程序。
注意:如果您想了解有關(guān)詞典的更多信息,請(qǐng)查看Python中的詞典。
請(qǐng)仔細(xì)查看上面的代碼塊。您如何為每個(gè)工作線程分配不同的睡眠時(shí)間?你能想出來嗎?你可以自己解決這個(gè)問題!
sleep()使用異步IO添加Python調(diào)用
異步功能已在3.4版本中添加到Python,此功能集從那時(shí)以來一直在積極擴(kuò)展。異步編程是一種并行編程,它允許您一次運(yùn)行多個(gè)任務(wù)。任務(wù)完成后,它將通知主線程。
asyncio
是一個(gè)允許您異步添加Python調(diào)用的模塊。如果您不熟悉Python的異步編程實(shí)現(xiàn),請(qǐng)查看Python中的異步IO:完整的演練和Python并發(fā)與并行編程。
這里有一個(gè)來自Python自己文檔的示例:
import asyncio
async def main():
print('Hello ...')
await asyncio.sleep(1)
print('... World!')
# Python 3.7+
asyncio.run(main())
在這個(gè)示例中,您運(yùn)行main()
并讓它休眠兩次調(diào)用之間的一秒鐘。
以下是asyncio
文檔中的協(xié)同程序和任務(wù)部分中一個(gè)更引人注目的示例:
import asyncio
import time
async def output(sleep, text):
await asyncio.sleep(sleep)
print(text)
async def main():
print(f"Started: {time.strftime('%X')}")
await output(1, 'First')
await output(2, 'Second')
await output(3, 'Third')
print(f"Ended: {time.strftime('%X')}")
# Python 3.7+
asyncio.run(main())
在這段代碼中,您創(chuàng)建了一個(gè)名為output()
的工作進(jìn)程,它占用到sleep
和text
的秒數(shù)打印出來。然后,使用Python的await
關(guān)鍵字等待output()
代碼運(yùn)行。這里需要await
,因?yàn)?code>output()已標(biāo)記為async
函數(shù),所以不能像調(diào)用普通函數(shù)那樣調(diào)用它。
運(yùn)行此代碼時(shí),程序?qū)?zhí)行await
3次。代碼將等待1、2和3秒,總等待時(shí)間為6秒。您還可以重寫代碼,使任務(wù)并行運(yùn)行:
import asyncio
import time
async def output(text, sleep):
while sleep > 0:
await asyncio.sleep(1)
print(f'{text} counter: {sleep} seconds')
sleep -= 1
async def main():
task_1 = asyncio.create_task(output('First', 1))
task_2 = asyncio.create_task(output('Second', 2))
task_3 = asyncio.create_task(output('Third', 3))
print(f"Started: {time.strftime('%X')}")
await task_1
await task_2
await task_3
print(f"Ended: {time.strftime('%X')}")
if __name__ == '__main__':
asyncio.run(main())
現(xiàn)在您使用的是任務(wù)的概念,您可以使用create_task()
來創(chuàng)建它。在asyncio
中使用任務(wù)時(shí),Python將異步運(yùn)行這些任務(wù)。因此,當(dāng)您運(yùn)行上述代碼時(shí),它應(yīng)該在3秒內(nèi)完成,而不是6秒。
sleep()使用GUI添加Python調(diào)用
命令行應(yīng)用程序并不是唯一需要添加Pythonsleep()
調(diào)用的地方。創(chuàng)建圖形用戶界面(GUI)時(shí),有時(shí)需要添加延遲。例如,您可以創(chuàng)建一個(gè)FTP應(yīng)用程序來下載數(shù)百萬個(gè)文件,但您需要在批處理之間添加一個(gè)調(diào)用,這樣就不會(huì)使服務(wù)器陷入困境。
GUI代碼將在一個(gè)名為事件循環(huán)的主線程中運(yùn)行其所有處理和繪圖。如果在GUI代碼中使用time.sleep()
,則會(huì)阻塞其事件循環(huán)。從用戶的角度來看,應(yīng)用程序可能會(huì)凍結(jié)。當(dāng)應(yīng)用程序使用此方法休眠時(shí),用戶將無法與應(yīng)用程序交互。(在Windows上,您甚至可能會(huì)收到有關(guān)應(yīng)用程序現(xiàn)在如何無響應(yīng)的警報(bào)。)
幸運(yùn)的是,除了time.sleep()
之外,您還可以使用其他方法。在接下來的幾節(jié)中,您將學(xué)習(xí)如何在Tkinter和wxPython中添加Python調(diào)用。如果您在Linux或Mac上使用的是Python的預(yù)裝版本,那么它可能不可用。如果您得到一個(gè)ImportError
,那么您需要研究如何將它添加到您的系統(tǒng)中。但是如果您自己安裝Python,那么應(yīng)該已經(jīng)可以使用了。運(yùn)行此代碼以查看添加Python調(diào)用時(shí)發(fā)生的情況:
import tkinter
import time
class MyApp:
def __init__(self, parent):
self.root = parent
self.root.geometry("400x400")
self.frame = tkinter.Frame(parent)
self.frame.pack()
b = tkinter.Button(text="click me", command=self.delayed)
b.pack()
def delayed(self):
time.sleep(3)
if __name__ == "__main__":
root = tkinter.Tk()
app = MyApp(root)
root.mainloop()
運(yùn)行代碼后,按GUI中的按鈕。當(dāng)按鈕等待sleep()
完成時(shí),它將向下停留3秒鐘。如果應(yīng)用程序有其他按鈕,那么您將無法單擊它們。您也不能在應(yīng)用程序睡眠時(shí)關(guān)閉它,因?yàn)樗鼰o法響應(yīng)關(guān)閉事件。
要使tkinter
正常睡眠,您需要使用after()
:
import tkinter
class MyApp:
def __init__(self, parent):
self.root = parent
self.root.geometry("400x400")
self.frame = tkinter.Frame(parent)
self.frame.pack()
self.root.after(3000, self.delayed)
def delayed(self):
print('I was delayed')
if __name__ == "__main__":
root = tkinter.Tk()
app = MyApp(root)
root.mainloop()
在這里創(chuàng)建一個(gè)寬400像素、高400像素的應(yīng)用程序。它上面沒有小部件。它只會(huì)顯示一個(gè)框架。然后,調(diào)用self.root.after()
,其中self.root
是對(duì)Tk()
對(duì)象的引用。after()
接受兩個(gè)參數(shù):
睡眠的毫秒數(shù)
睡眠完成時(shí)要調(diào)用的方法
在這種情況下,應(yīng)用程序?qū)⒃?秒鐘后將字符串打印到標(biāo)準(zhǔn)輸出。您可以將after()
視為tkinter
版本的time.sleep()
,但它還增加了在睡眠結(jié)束后調(diào)用函數(shù)的功能。
您可以使用此功能來改善用戶體驗(yàn)。通過添加Pythonsleep()
調(diào)用,可以使應(yīng)用程序看起來加載更快,然后在啟動(dòng)后啟動(dòng)一些運(yùn)行時(shí)間更長的進(jìn)程。那樣的話,用戶無需等待應(yīng)用程序打開。
睡眠在wxPython
wxPython和Tkinter之間有兩個(gè)主要區(qū)別:
wxPython有更多的小部件。
wxPython的目標(biāo)是在所有平臺(tái)上看起來和感覺都是本地的。
Python中沒有wxPython框架,所以您需要自己安裝它。如果您不熟悉wxPython,請(qǐng)查看如何使用wxPython構(gòu)建Python GUI應(yīng)用程序。
在wxPython中,您可以使用wx.CallLater()
添加Python調(diào)用:
import wx
class MyFrame(wx.Frame):
def __init__(self):
super().__init__(parent=None, title='Hello World')
wx.CallLater(4000, self.delayed)
self.Show()
def delayed(self):
print('I was delayed')
if __name__ == '__main__':
app = wx.App()
frame = MyFrame()
app.MainLoop()
在這里,您可以直接將wx.Frame
子類化,然后調(diào)用wx.CallLater()
。此函數(shù)采用與Tkinter的after()
相同的參數(shù):
睡眠的毫秒數(shù)
睡眠完成時(shí)調(diào)用的方法
運(yùn)行此代碼時(shí),應(yīng)該會(huì)看到一個(gè)沒有任何小部件的小空白窗口出現(xiàn)。4秒鐘后,您將看到字符串'I was delayed'
打印到stdout。
使用wx.CallLater()
的好處之一是線程安全。您可以在線程中使用此方法來調(diào)用wxPython主應(yīng)用程序中的函數(shù)。
結(jié)論
通過本教程,您獲得了一項(xiàng)有價(jià)值的新技術(shù),可以添加到Python工具箱中!您知道如何添加延遲來加快應(yīng)用程序的速度,并防止它們耗盡系統(tǒng)資源。您甚至可以使用Pythonsleep()
調(diào)用來幫助更有效地重新繪制GUI代碼。這將為您的客戶提供更好的用戶體驗(yàn)!
作者介紹