Python 計時器
Python 定時器函數
示例
第一個 Python 計時器
一個 Python 定時器類
理解 Python 中的類
創建 Python 計時器類
使用 Python 計時器類
增加更多的便利性和靈活性
Timer改進
總結
雖然許多數據工作者認為 Python 是一種有效的編程語言,但純 Python 程序比C、Rust 和 Java 等編譯語言中的對應程序運行得更慢,為了更好地監控和優化Python程序,雲朵君將和大家一起學習如何使用 Python 計時器來監控程序運行的速度,以便正對性改善代碼性能。
為了更好地掌握 Python 計時器的應用,我們後面還補充了有關Python類、上下文管理器和裝飾器的背景知識。因篇幅限制,其中利用上下文管理器和裝飾器優化 Python 計時器,將在後續文章學習,不在本篇文章范圍內。
Python 計時器首先,我們向某段代碼中添加一個Python 計時器以監控其性能。
Python 定時器函數Python 中的內置time[1]模塊中有幾個可以測量時間的函數:
monotonic()
perf_counter()
process_time()
time()
Python 3.7 引入了幾個新函數,如thread_time()[2],以及上述所有函數的納秒版本,以_ns
後綴命名。例如,perf_counter_ns()
是perf_counter()
的納秒版本的。
perf_counter()
返回性能計數器的值(以秒為單位),即具有最高可用分辨率的時鐘以測量短持續時間。
首先,使用perf_counter()
創建一個 Python 計時器。將把它與其他 Python 計時器函數進行比較,看看 perf_counter()
的優勢。
創建一個腳本,定義一個簡短的函數:從清華雲上下載一組數據。
import requestsdef main(): source_url = 'https://cloud.tsinghua.edu.cn/d/e1ccfff39ad541908bae/files/?p=%2Fall_six_datasets.zip&dl=1' headers = {'User-Agent': 'Mozilla/5.0'} res = requests.get(source_url, headers=headers) with open('dataset/datasets.zip', 'wb') as f: f.write(res.content)if __name__=="__main__": main()
我們可以使用 Python 計時器來監控該腳本的性能。
第一個 Python 計時器現在使用函數time.perf_counter()
函數創建一個計時器,這是一個非常適合針對部分代碼的性能計時的計數器。
perf_counter()
從某個未指定的時刻開始測量時間(以秒為單位),這意味著對該函數的單個調用的返回值沒有用。但當查看對perf_counter()
兩次調用之間的差異時,可以計算出兩次調用之間經過了多少秒。
>>> import time>>> time.perf_counter()394.540232282>>> time.perf_counter() # 幾秒鐘後413.31714087
在此示例中,兩次調用 perf_counter()
相隔近 19 秒。可以通過計算兩個輸出之間的差異來確認這一點:413.31714087 - 394.540232282 = 18.78。
現在可以將 Python 計時器添加到示例代碼中:
# download_data.pyimport requestsimport timedef main(): tic = time.perf_counter() source_url = 'https://cloud.tsinghua.edu.cn/d/e1ccfff39ad541908bae/files/?p=%2Fall_six_datasets.zip&dl=1' headers = {'User-Agent': 'Mozilla/5.0'} res = requests.get(source_url, headers=headers) with open('dataset/datasets.zip', 'wb') as f: f.write(res.content) toc = time.perf_counter() print(f"該程序耗時: {toc - tic:0.4f} seconds")if __name__=="__main__": main()
注意perf_counter()
通過計算兩次調用之間的差異來打印整個程序運行所花費的時間。
print()
函數中 f
字符串前面的表示這是一個 f-string
,這是格式化文本字符串的較為便捷的方式。:0.4f
是一個格式說明符,表示數字,toc - tic
應打印為帶有四位小數的十進制數。
運行程序可以看到程序經過的時間:
該程序耗時: 0.026 seconds
就是這麼簡單。接下來我們一起學習如何將 Python 計時器包裝到一個類、一個上下文管理器和一個裝飾器中,這樣可以更加一致和方便使用計時器。
一個 Python 定時器類這裡我們至少需要一個變量來存儲 Python 計時器的狀態。接下來我們創建一個與手動調用 perf_counter()
相同的類,但更具可讀性和一致性。
創建和更新Timer
類,使用該類以多種不同方式對代碼進行計時。
$ python -m pip install codetiming
理解 Python 中的類Class類是面向對象編程的主要構建塊。類本質上是一個模板,可以使用它來創建對象。
在 Python 中,當需要對需要跟蹤特定狀態的事物進行建模時,類非常有用。一般來說,類是屬性的集合,稱為屬性,以及行為,稱為方法。
創建 Python 計時器類類有利於跟蹤狀態。在Timer
類中,想要跟蹤計時器何時開始以及已經多少時間。對於Timer
類的第一個實現,將添加一個._start_time
屬性以及.start()
和.stop()
方法。將以下代碼添加到名為 timer.py
的文件中:
# timer.pyimport timeclass TimerError(Exception): """一個自定義異常,用於報告使用Timer類時的錯誤"""class Timer: def __init__(self): self._start_time = None def start(self): """Start a new timer""" if self._start_time is not None: raise TimerError(f"Timer is running. Use .stop() to stop it") self._start_time = time.perf_counter() def stop(self): """Stop the timer, and report the elapsed time""" if self._start_time is None: raise TimerError(f"Timer is not running. Use .start() to start it") elapsed_time = time.perf_counter() - self._start_time self._start_time = None print(f"Elapsed time: {elapsed_time:0.4f} seconds")
這裡我們需要花點時間仔細地浏覽代碼,會發現一些不同的事情。
首先定義了一個TimerError
Python 類。該(Exception)
符號表示TimerError
繼承自另一個名為Exception
的父類。使用這個內置類進行錯誤處理。不需要向TimerError
添加任何屬性或方法,但自定義錯誤可以更靈活地處理Timer
內部問題。
接下來自定義Timer
類。當從一個類創建或實例化一個對象時,代碼會調用特殊方法.__init__()
初始化實例。在這裡定義的第一個Timer
版本中,只需初始化._start_time
屬性,將用它來跟蹤 Python 計時器的狀態,計時器未運行時它的值為None
。計時器運行後,用它來跟蹤計時器的啟動時間。
注意: ._start_time
的第一個下劃線(_)
前綴是Python約定。它表示._start_time
是Timer類的用戶不應該操作的內部屬性。
當調用.start()
啟動新的 Python 計時器時,首先檢查計時器是否運行。然後將perf_counter()
的當前值存儲在._start_time
中。
另一方面,當調用.stop()
時,首先檢查Python計時器是否正在運行。如果是,則將運行時間計算為perf_counter()
的當前值與存儲在._start_time
中的值的差值。最後,重置._start_time
,以便重新啟動計時器,並打印運行時間。
以下是使用Timer
方法:
from timer import Timert = Timer()t.start()# 幾秒鐘後t.stop()
Elapsed time: 3.8191 seconds
將此示例與前面直接使用perf_counter()
的示例進行比較。代碼的結構相似,但現在代碼更清晰了,這也是使用類的好處之一。通過仔細選擇類、方法和屬性名稱,可以使你的代碼非常具有描述性!
現在Timer
類中寫入download_data.py
。只需要對以前的代碼進行一些更改:
# download_data.pyimport requestsfrom timer import Timerdef main(): t = Timer() t.start() source_url = 'https://cloud.tsinghua.edu.cn/d/e1ccfff39ad541908bae/files/?p=%2Fall_six_datasets.zip&dl=1' headers = {'User-Agent': 'Mozilla/5.0'} res = requests.get(source_url, headers=headers) with open('dataset/datasets.zip', 'wb') as f: f.write(res.content) t.stop()if __name__=="__main__": main()
注意,該代碼與之前使用的代碼非常相似。除了使代碼更具可讀性之外,Timer
還負責將經過的時間打印到控制台,使得所用時間的記錄更加一致。運行代碼時,得到的輸出幾乎相同:
Elapsed time: 0.502 seconds
...
打印經過的時間Timer
可能是一致的,但這種方法好像不是很靈活。下面我們添加一些更加靈活的東西到代碼中。
到目前為止,我們已經了解到類適用於我們想要封裝狀態並確保代碼一致性的情況。在本節中,我們將一起給 Python 計時器加入更多便利性和靈活性,那怎麼做呢?
在報告消耗的時間時,使用可調整的文本和格式
將日志記錄打印到控制台、寫入到日志文件或程序的其他部分
創建一個可以在多次調用中可積累的Python計時器
構建 Python 計時器的信息表示
首先,自定義用於報告所用時間的文本。在前面的代碼中,文本 f"Elapsed time: {elapsed_time:0.4f} seconds"
被生硬編碼到 .stop()
中。如若想使得類代碼更加靈活, 可以使用實例變量,其值通常作為參數傳遞給.__init__()
並存儲到 self
屬性。為方便起見,我們還可以提供合理的默認值。
要添加.text
為Timer
實例變量,可執行以下操作timer.py
:
# timer.pydef __init__(self, text="Elapsed time: {:0.4f} seconds"): self._start_time = None self.text = text
注意,默認文本"Elapsed time: {:0.4f} seconds"
是作為一個常規字符串給出的,而不是f-string
。這裡不能使用f-string
,因為f-string
會立即計算,當你實例化Timer時,你的代碼還沒有計算出消耗的時間。
注意: 如果要使用f-string
來指定.text
,則需要使用雙花括號來轉義實際經過時間將替換的花括號。
如:f"Finished {task} in {{:0.4f}} seconds"
。如果task
的值是"reading"
,那麼這個f-string
將被計算為"Finished reading in {:0.4f} seconds"
。
在.stop()
中,.text
用作模板並使用.format()
方法填充模板:
# timer.pydef stop(self): """Stop the timer, and report the elapsed time""" if self._start_time is None: raise TimerError(f"Timer is not running. Use .start() to start it") elapsed_time = time.perf_counter() - self._start_time self._start_time = None print(self.text.format(elapsed_time))
在此更新為timer.py
之後,可以將文本更改如下:
from timer import Timert = Timer(text="You waited {:.1f} seconds")t.start()# 幾秒鐘後t.stop()
You waited 4.1 seconds
接下來,我們不只是想將消息打印到控制台,還想保存時間測量結果,這樣可以便於將它們存儲在數據庫中。可以通過從.stop()
返回elapsed_time
的值來實現這一點。然後,調用代碼可以選擇忽略該返回值或保存它以供以後處理。
如果想要將Timer集成到日志logging中。要支持計時器的日志記錄或其他輸出,需要更改對print()
的調用,以便用戶可以提供自己的日志記錄函數。這可以用類似於你之前定制的文本來完成:
# timer.py# ...class Timer: def __init__( self, text="Elapsed time: {:0.4f} seconds", logger=print ): self._start_time = None self.text = text self.logger = logger # 其他方法保持不變 def stop(self): """Stop the timer, and report the elapsed time""" if self._start_time is None: raise TimerError(f"Timer is not running. Use .start() to start it") elapsed_time = time.perf_counter() - self._start_time self._start_time = None if self.logger: self.logger(self.text.format(elapsed_time)) return elapsed_time
不是直接使用print()
,而是創建另一個實例變量 self.logger
,引用一個接受字符串作為參數的函數。除此之外,還可以對文件對象使用logging.info()
或.write()
等函數。還要注意if中,它允許通過傳遞logger=None
來完全關閉打印。
以下是兩個示例,展示了新功能的實際應用:
from timer import Timerimport loggingt = Timer(logger=logging.warning)t.start()# 幾秒鐘後t.stop() # A few seconds later
WARNING:root:Elapsed time: 3.1610 seconds
3.1609658249999484
t = Timer(logger=None)t.start()# 幾秒鐘後value = t.stop()value
4.710851433001153
接下來第三個改進是積累時間度量的能力。例如,在循環中調用一個慢速函數時,希望以命名計時器的形式添加更多的功能,並使用一個字典來跟蹤代碼中的每個Python計時器。
我們擴展download_data.py腳本。
# download_data.pyimport requestsfrom timer import Timerdef main(): t = Timer() t.start() source_url = 'https://cloud.tsinghua.edu.cn/d/e1ccfff39ad541908bae/files/?p=%2Fall_six_datasets.zip&dl=1' headers = {'User-Agent': 'Mozilla/5.0'} for i in range(10): res = requests.get(source_url, headers=headers) with open('dataset/datasets.zip', 'wb') as f: f.write(res.content) t.stop()if __name__=="__main__": main()
這段代碼的一個微妙問題是,不僅要測量下載數據所需的時間,還要測量 Python 存儲數據到磁盤所花費的時間。這可能並重要,有時候這兩者所花費的時間可以忽略不計。但還是希望有一種方法可以精確地計時沒一個步驟,將會更好。
有幾種方法可以在不改變Timer當前實現的情況下解決這個問題,且只需要幾行代碼即可實現。
首先,將引入一個名為.timers
的字典作為Timer的類變量,此時Timer的所有實例將共享它。通過在任何方法之外定義它來實現它:
class Timer: timers = {}
類變量可以直接在類上訪問,也可以通過類的實例訪問:
>>> from timer import Timer>>> Timer.timers{}>>> t = Timer()>>> t.timers{}>>> Timer.timers is t.timersTrue
在這兩種情況下,代碼都返回相同的空類字典。
接下來向 Python 計時器添加可選名稱。可以將該名稱用於兩種不同的目的:
在代碼中查找經過的時間
累加同名定時器
要向Python計時器添加名稱,需要對 timer.py
進行更改。首先,Timer 接受 name 參數。第二,當計時器停止時,運行時間應該添加到 .timers
中:
# timer.py# ...class Timer: timers = {} def __init__( self, name=None, text="Elapsed time: {:0.4f} seconds", logger=print, ): self._start_time = None self.name = name self.text = text self.logger = logger # 向計時器字典中添加新的命名計時器 if name: self.timers.setdefault(name, 0) # 其他方法保持不變 def stop(self): """Stop the timer, and report the elapsed time""" if self._start_time is None: raise TimerError(f"Timer is not running. Use .start() to start it") elapsed_time = time.perf_counter() - self._start_time self._start_time = None if self.logger: self.logger(self.text.format(elapsed_time)) if self.name: self.timers[self.name] += elapsed_time return elapsed_time
注意,在向.timers
中添加新的Python計時器時,使用了.setdefault()
方法。它只在沒有在字典中定義name的情況下設置值,如果name已經在.timers
中使用,那麼該值將保持不變,此時可以積累幾個計時器:
>>> from timer import Timer>>> t = Timer("accumulate")>>> t.start()>>> t.stop() # A few seconds laterElapsed time: 3.7036 seconds3.703554293999332>>> t.start()>>> t.stop() # A few seconds laterElapsed time: 2.3449 seconds2.3448921170001995>>> Timer.timers{'accumulate': 6.0484464109995315}
現在可以重新訪問download_data.py
並確保僅測量下載數據所花費的時間:
# download_data.pyimport requestsfrom timer import Timerdef main(): t = Timer("download", logger=None) source_url = 'https://cloud.tsinghua.edu.cn/d/e1ccfff39ad541908bae/files/?p=%2Fall_six_datasets.zip&dl=1' headers = {'User-Agent': 'Mozilla/5.0'} for i in range(10): t.start() res = requests.get(source_url, headers=headers) t.stop() with open('dataset/datasets.zip', 'wb') as f: f.write(res.content) download_time = Timer.timers["download"] print(f"Downloaded 10 dataset in {download_time:0.2f} seconds")if __name__=="__main__": main()
現在你有了一個非常簡潔的版本,Timer
它一致、靈活、方便且信息豐富!也可以將本節中所做的許多改進應用於項目中的其他類型的類。
最後一個改進Timer
,以交互方式使用它時使其更具信息性。下面操作是實例化一個計時器類,並查看其信息:
>>> from timer import Timer>>> t = Timer()>>> t<timer.Timer object at 0x7f0578804320>
最後一行是 Python 表示對象的默認方式。我們從這個結果中看到的信息,並不是很明確,我們接下來對其進行改進。
這裡介紹一個 dataclasses
類,該類僅包含在 Python 3.7 及更高版本中。
pip install dataclasses
可以使用@dataclass
裝飾器將 Python 計時器轉換為數據類
# timer.pyimport timefrom dataclasses import dataclass, fieldfrom typing import Any, ClassVar# [email protected] Timer: timers: ClassVar = {} name: Any = None text: Any = "Elapsed time: {:0.4f} seconds" logger: Any = print _start_time: Any = field(default=None, init=False, repr=False) def __post_init__(self): """Initialization: add timer to dict of timers""" if self.name: self.timers.setdefault(self.name, 0) # 其余代碼不變
此代碼替換了之前的 .__init__()
方法。請注意數據類如何使用類似於之前看到的用於定義所有變量的類變量語法的語法。事實上,.__init__()
是根據類定義中的注釋變量自動為數據類創建的。
如果需要注釋變量以使用數據類。可以使用此注解向代碼添加類型提示。如果不想使用類型提示,那麼可以使用 Any 來注釋所有變量。接下來我們很快就會學習如何將實際類型提示添加到我們的數據類中。
以下是有關 Timer 數據類的一些注意事項:
第 6 行:@dataclass 裝飾器將 Timer
定義為數據類。
第 8 行:數據類需要特殊的 ClassVar 注釋來指定 .timers
是一個類變量。
第 9 到 11 行:.name
、.text
和 .logger
將被定義為 Timer 上的屬性,可以在創建 Timer 實例時指定其值。它們都有給定的默認值。
第 12 行:回想一下 ._start_time
是一個特殊屬性,用於跟蹤 Python 計時器的狀態,但它應該對用戶隱藏。使用 dataclasses.field()
, ._start_time
應該從 .__init__()
和 Timer 的表示中刪除。
除了設置實例屬性之外,可以使用特殊的 .__post_init__()
方法進行初始化。這裡使用它將命名的計時器添加到 .timers
。
新 Timer 數據類與之前的常規類使用功能一樣,但它現在有一個很好的信息表示:
from timer import Timert = Timer()t
Timer(name=None, text='Elapsed time: {:0.4f} seconds', logger=<built-in function print>)
t.start()# 幾秒鐘後t.stop()
總結Elapsed time: 6.7197 seconds
6.719705373998295
現在我們有了一個非常簡潔的 Timer 版本,它一致、靈活、方便且信息豐富!我們還可以將本文中所做的許多改進應用於項目中的其他類型的類。
現在我們訪問當前的完整源代碼Timer
。會注意到在代碼中添加了類型提示以獲取額外的文檔:
# timer.pyfrom dataclasses import dataclass, fieldimport timefrom typing import Callable, ClassVar, Dict, Optionalclass TimerError(Exception): """A custom exception used to report errors in use of Timer class"""@dataclassclass Timer: timers: ClassVar[Dict[str, float]] = {} name: Optional[str] = None text: str = "Elapsed time: {:0.4f} seconds" logger: Optional[Callable[[str], None]] = print _start_time: Optional[float] = field(default=None, init=False, repr=False) def __post_init__(self) -> None: """Add timer to dict of timers after initialization""" if self.name is not None: self.timers.setdefault(self.name, 0) def start(self) -> None: """Start a new timer""" if self._start_time is not None: raise TimerError(f"Timer is running. Use .stop() to stop it") self._start_time = time.perf_counter() def stop(self) -> float: """Stop the timer, and report the elapsed time""" if self._start_time is None: raise TimerError(f"Timer is not running. Use .start() to start it") # Calculate elapsed time elapsed_time = time.perf_counter() - self._start_time self._start_time = None # Report elapsed time if self.logger: self.logger(self.text.format(elapsed_time)) if self.name: self.timers[self.name] += elapsed_time return elapsed_time
總結下: 使用類創建 Python 計時器有幾個好處:
可讀性:仔細選擇類和方法名稱,你的代碼將更自然地閱讀。
一致性:將屬性和行為封裝到屬性和方法中,你的代碼將更易於使用。
靈活性:使用具有默認值而不是硬編碼值的屬性,你的代碼將是可重用的。
這個類非常靈活,幾乎可以在任何需要監控代碼運行時間的情況下使用它。但是,在接下來的部分中,雲朵君將和大家一起了解如何使用上下文管理器和裝飾器,這將更方便地對代碼塊和函數進行計時。
以上就是手把手帶你用Python實現一個計時器的詳細內容,更多關於Python計時器的資料請關注軟件開發網其它相關文章!