<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
雖然許多資料工作者認為 Python 是一種有效的程式語言,但純 Python 程式比C、Rust 和 Java 等編譯語言中的對應程式執行得更慢,為了更好地監控和優化Python程式,雲朵君將和大家一起學習如何使用 Python 計時器來監控程式執行的速度,以便正對性改善程式碼效能。
為了更好地掌握 Python 計時器的應用,我們後面還補充了有關Python類、上下文管理器和裝飾器的背景知識。因篇幅限制,其中利用上下文管理器和裝飾器優化 Python 計時器,將在後續文章學習,不在本篇文章範圍內。
首先,我們向某段程式碼中新增一個Python 計時器以監控其效能。
Python 中的內建time[1]模組中有幾個可以測量時間的函數:
Python 3.7 引入了幾個新函數,如thread_time()[2],以及上述所有函數的納秒版本,以_ns
字尾命名。例如,perf_counter_ns()
是perf_counter()
的納秒版本的。
perf_counter()
返回效能計數器的值(以秒為單位),即具有最高可用解析度的時鐘以測量短持續時間。
首先,使用perf_counter()
建立一個 Python 計時器。將把它與其他 Python 計時器函數進行比較,看看 perf_counter()
的優勢。
建立一個指令碼,定義一個簡短的函數:從清華雲上下載一組資料。
import requests def 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 計時器來監控該指令碼的效能。
現在使用函數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.py import requests import time def 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 計時器的狀態。接下來我們建立一個與手動呼叫 perf_counter()
相同的類,但更具可讀性和一致性。
建立和更新Timer
類,使用該類以多種不同方式對程式碼進行計時。
$ python -m pip install codetiming
Class類是物件導向程式設計的主要構建塊。類本質上是一個模板,可以使用它來建立物件。
在 Python 中,當需要對需要跟蹤特定狀態的事物進行建模時,類非常有用。一般來說,類是屬性的集合,稱為屬性,以及行為,稱為方法。
類有利於跟蹤狀態。在Timer
類中,想要跟蹤計時器何時開始以及已經多少時間。對於Timer
類的第一個實現,將新增一個._start_time
屬性以及.start()
和.stop()
方法。將以下程式碼新增到名為 timer.py
的檔案中:
# timer.py import time class 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 Timer t = Timer() t.start() # 幾秒鐘後 t.stop()
Elapsed time: 3.8191 seconds
將此範例與前面直接使用perf_counter()
的範例進行比較。程式碼的結構相似,但現在程式碼更清晰了,這也是使用類的好處之一。通過仔細選擇類、方法和屬性名稱,可以使你的程式碼非常具有描述性!
現在Timer
類中寫入download_data.py
。只需要對以前的程式碼進行一些更改:
# download_data.py import requests from timer import Timer def 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 計時器加入更多便利性和靈活性,那怎麼做呢?
首先,自定義用於報告所用時間的文字。在前面的程式碼中,文字 f"Elapsed time: {elapsed_time:0.4f} seconds"
被生寫死到 .stop()
中。如若想使得類程式碼更加靈活, 可以使用範例變數,其值通常作為引數傳遞給.__init__()
並儲存到 self
屬性。為方便起見,我們還可以提供合理的預設值。
要新增.text
為Timer
範例變數,可執行以下操作timer.py
:
# timer.py def __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.py 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(self.text.format(elapsed_time))
在此更新為timer.py
之後,可以將文字更改如下:
from timer import Timer t = 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 Timer import logging t = 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.py import requests from timer import Timer def 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.timers True
在這兩種情況下,程式碼都返回相同的空類字典。
接下來向 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 later Elapsed time: 3.7036 seconds 3.703554293999332 >>> t.start() >>> t.stop() # A few seconds later Elapsed time: 2.3449 seconds 2.3448921170001995 >>> Timer.timers {'accumulate': 6.0484464109995315}
現在可以重新存取download_data.py
並確保僅測量下載資料所花費的時間:
# download_data.py import requests from timer import Timer def 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.py import time from dataclasses import dataclass, field from typing import Any, ClassVar # ... @dataclass class 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 資料類的一些注意事項:
Timer
定義為資料類。.timers
是一個類變數。.name
、.text
和 .logger
將被定義為 Timer 上的屬性,可以在建立 Timer 範例時指定其值。它們都有給定的預設值。._start_time
是一個特殊屬性,用於跟蹤 Python 計時器的狀態,但它應該對使用者隱藏。使用 dataclasses.field()
, ._start_time
應該從 .__init__()
和 Timer 的表示中刪除。 .__post_init__()
方法進行初始化。這裡使用它將命名的計時器新增到 .timers
。新 Timer 資料類與之前的常規類使用功能一樣,但它現在有一個很好的資訊表示:
from timer import Timer t = 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.py from dataclasses import dataclass, field import time from typing import Callable, ClassVar, Dict, Optional class TimerError(Exception): """A custom exception used to report errors in use of Timer class""" @dataclass class 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計時器的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45