作者:雲朵君
來源:數據STUDIO
在本文中,雲朵君將和大家一起了解裝飾器的工作原理,如何將我們之前定義的定時器類 Timer 擴展為裝飾器,以及如何簡化計時功能。最後對 Python 定時器系列文章做個小結。 這是我們手把手教你實現 Python 定時器的第三篇文章。前兩篇:分別是手把手教你實現一個 Python 計時器,和用上下文管理器擴展 Python 計時器,使得我們的 Timer 類方便用、美觀實用。 但我們並不滿足於此,仍然有一個用例可以進一步簡化它。假設我們需要跟蹤代碼庫中一個給定函數所花費的時間。使用上下文管理器,基本上有兩種不同的選擇: 1. 每次調用函數時使用 Timer: 當我們在一個py文件里多次調用函數do_something(),那麼這將會變得非常繁瑣並且難以維護。 2. 將代碼包裝在上下文管理器中的函數中: Timer 只需要在一個地方添加,但這會為do_something()的整個定義增加一個縮進級別。 更好的解決方案是使用 Timer 作為裝飾器。裝飾器是用於修改函數和類行為的強大構造。 裝飾器是包裝另一個函數以修改其行為的函數。你可能會有疑問,這怎麼實現呢?其實函數是 Python 中的first-class 對象,換句話說,函數可以以變量的形式傳遞給其他函數的參數,就像任何其他常規對象一樣。因此此處有較大的靈活性,也是 Python 幾個最強大功能的基礎。 我們首先創建第一個示例,一個什麼都不做的裝飾器: 首先注意這個turn_off()只是一個常規函數。之所以成為裝飾器,是因為它將一個函數作為其唯一參數並返回另一個函數。我們可以使用turn_off()來修改其他函數,例如: 代碼行 print = turn_off(print) 用 turn_off() 裝飾器裝飾了 print 語句。實際上,它將函數 print() 替換為匿名函數lambda *args, **kwargs: None並返回 turn_off()。匿名函數lambda 除了返回 None 之外什麼都不做。 要定義更多豐富的裝飾器,需要了解內部函數。內部函數是在另一個函數內部定義的函數,它的一種常見用途是創建函數工廠: multiplier() 是一個內部函數,在 create_multiplier() 內部定義。注意可以訪問 multiplier() 內部的因子,而 multiplier()未在 create_multiplier() 外部定義: 相反,可以使用create_multiplier()創建新的 multiplier 函數,每個函數都基於不同的參數factor: 同樣,可以使用內部函數來創建裝飾器。裝飾器是一個返回函數的函數: triple() 是一個裝飾器,因為它是一個期望函數 func() 作為其唯一參數並返回另一個函數 wrapper_triple() 的函數。注意 triple() 本身的結構: 這是種定義裝飾器的一般模式(注意內部函數的部分): 接下來的代碼中,knock() 是一個返回單詞 Penny 的函數,將其傳給triple() 函數,並看看輸出結果是什麼。 我們都知道,文本字符串與數字相乘,是字符串的一種重複形式,因此字符串 'Penny' 重複了 3 次。可以認為,裝飾發生在knock = triple(knock)。 上述方法雖然實現了裝飾器的功能,但似乎有點笨拙。PEP 318 引入了一種更方便的語法來應用裝飾器。下面的 knock() 定義與上面的定義相同,但裝飾器用法不同。 @ 符號用於應用裝飾器,@triple 表示 triple() 應用於緊隨其後定義的函數。 Python 標準庫中定義的裝飾器方法之一是:@functools.wraps。這在定義你自己的裝飾器時非常有用。前面說過,裝飾器是用另一個函數替換了一個函數,會給你的函數帶來一個微妙的變化: @triple 裝飾了 knock(),然後被 wrapper_triple() 內部函數替換,被裝飾的函數的名字會變成裝飾器函數,除了名稱,還有文檔字符串和其他元數據都將會被替換。但有時,我們並不總是想將被修飾的函數的所有信息都被修改了。此時 @functools.wraps 正好解決了這個問題,如下所示: 使用 @triple 的這個新定義保留元數據: 注意knock() 即使在被裝飾之後,也同樣保留了它的原有函數名稱。當定義裝飾器時,使用 @functools.wraps是一種不錯的選擇,可以為大多數裝飾器使用的如下模板: 在本節中,雲朵君將和大家一起學習如何擴展 Python 計時器,並以裝飾器的形式使用它。接下來我們從頭開始創建 Python 計時器裝飾器。 根據上面的模板,我們只需要決定在調用裝飾函數之前和之後要做什麼。這與進入和退出上下文管理器時的注意事項類似。在調用修飾函數之前啟動 Python 計時器,並在調用完成後停止 Python 計時器。可以按如下方式定義 @timer 裝飾器: 可以按如下方式應用 @timer: 回想一下,還可以將裝飾器應用於先前定義的下載數據的函數: 使用裝飾器的一個優點是只需要應用一次,並且每次都會對函數計時: 雖然@timer順利完成了對目標函數的定時。但從某種意義上說,你又回到了原點,因為該裝飾器 @timer失去了前面定義的類Timer 的靈活性或便利性。換句話說,我們需要將Timer 類表現得像一個裝飾器。 現在我們似乎已經將裝飾器用作應用於其他函數的函數,但其實不然,因為裝飾器必須是可調用的。Python中有許多可調用的類型,可以通過在其類中定義特殊的.__call__()方法來使自己的對象可調用。以下函數和類的行為類似: 這裡,square 是一個可調用的實例,可以對數字求平方,就像square()第一個示例中的函數一樣。 我們現在向現有Timer類添加裝飾器功能,首先需要import functools。 在之前定義的上下文管理器 Timer ,給我們帶來了不少便利。而這裡使用的裝飾器,似乎更加方便。 有一種更直接的方法可以將 Python 計時器變成裝飾器。其實上下文管理器和裝飾器之間的一些相似之處:它們通常都用於在執行某些給定代碼之前和之後執行某些操作。 基於這些相似之處,在 python 標準庫中定義了一個名為 ContextDecorator 的 mixin 類,它可以簡單地通過繼承 ContextDecorator 來為上下文管理器類添加裝飾器函數。 當以這種方式使用 ContextDecorator 時,無需自己實現 .__call__(),因此我們可以大膽地將其從 Timer 類中刪除。 接下來,再最後一次重改download_data.py 示例,使用 Python 計時器作為裝飾器: 我們與之前的寫法進行比較,唯一的區別是第 3 行的 Timer 的導入和第 4 行的 @Timer() 的應用。使用裝飾器的一個顯着優勢是它們通常很容易調用。 但是,裝飾器仍然適用於整個函數。這意味着代碼除了記錄了下載數據所需的時間外,還考慮了保存數據所需的時間。運行腳本: 從上面打印出來的結果可以看到,代碼記錄了下載數據和保持數據一共所需的時間。 當使用 Timer 作為裝飾器時,會看到與使用上下文管理器類似的優勢: 然而,裝飾器不如上下文管理器靈活,只能將它們應用於完整函數。 這裡展開下面的代碼塊以查看 Python 計時器timer.py的完整源代碼。 上下滑動查看更多源碼 可以自己使用代碼,方法是將其保存到一個名為的文件中timer.py並將其導入: PyPI 上也提供了 Timer,因此更簡單的選擇是使用 pip 安裝它: 注意,PyPI 上的包名稱是codetiming,安裝包和導入時都需要使用此名稱Timer: 除了名稱和一些附加功能之外,codetiming.Timer 與 timer.Timer 完全一樣。總而言之,可以通過三種不同的方式使用 Timer: 1. 作為一個類: 2. 作為上下文管理器: 3. 作為裝飾器: 這種 Python 計時器主要用於監控代碼在單個關鍵代碼塊或函數上所花費的時間。 Python定時器裝飾器已經學習完畢了,接下來是總結了一些其他的 Python 定時器函數,如果你對其不太感興趣,可以直接跳到最後,給雲朵君點個讚和在看支持下! 使用 Python 對代碼進行計時有很多選擇。這裡我們學習了如何創建一個靈活方便的類,可以通過多種不同的方式使用該類。對 PyPI 的快速搜索發現,已經有許多項目提供 Python 計時器解決方案。 在本節中,我們首先了解有關標準庫中用於測量時間的不同函數的更多信息,包括為什麼 perf_counter() 更好,然後探索優化代碼的替代方案。 在本文之前,包括前面介紹python定時器的文章中,我們一直在使用 perf_counter() 來進行實際的時間測量,但是 Python 的時間庫附帶了幾個其他也可以測量時間的函數。這裡有一些: 擁有多個函數的一個原因是 Python 將時間表示為浮點數。浮點數本質上是不準確的。之前可能已經看到過這樣的結果: Python 的 Float 遵循 IEEE 754 浮點算術標準[5],該標準以 64 位表示所有浮點數。因為浮點數有無限多位數,即不能用有限的位數來表達它們。 考慮time()這個函數的主要目的,是它表示的是現在的實際時間。它以自給定時間點(稱為紀元)以來的秒數來表示函數。time()返回的數字很大,這意味着可用的數字較少,因而分辨率會受到影響。簡而言之, time()無法測量納秒級差異: 一納秒是十億分之一秒。上面代碼中,將納秒添加到參數 t ,他並不會影響結果。與time()不同的是,perf_counter() 使用一些未定義的時間點作為它的紀元,它可以使用更小的數字,從而獲得更好的分辨率: 眾所周知,將時間表示為浮點數是非常具有挑戰的一件事,因此 Python 3.7 引入了一個新選項:每個時間測量函數現在都有一個相應的 _ns 函數,它以 int 形式返回納秒數,而不是以浮點數形式返回秒數。例如,time() 現在有一個名為 time_ns() 的納秒對應項: 整數在 Python 中是無界的,因此time_ns()可以為所有永恆提供納秒級分辨率。同樣,perf_counter_ns() 是 perf_counter() 的納秒版本: 我們注意到,因為 perf_counter() 已經提供納秒級分辨率,所以使用 perf_counter_ns() 的優勢較少。 注意: perf_counter_ns() 僅在 Python 3.7 及更高版本中可用。在 Timer 類中使用了 perf_counter()。這樣,也可以在較舊的 Python 版本上使用 Timer。 有兩個函數time不測量time.sleep時間:process_time()和thread_time()。通常希望Timer能夠測量代碼所花費的全部時間,因此這兩個函數並不常用。而函數monotonic(),顧名思義,它是一個單調計時器,一個永遠不會向後移動的 Python 計時器。 除了 time() 之外,所有這些函數都是單調的,如果調整了系統時間,它也隨之倒退。在某些系統上,monotonic() 與 perf_counter() 的功能相同,可以互換使用。我們可以使用 time.get_clock_info() 獲取有關 Python 計時器函數的更多信息: 注意,不同系統上的結果可能會有所不同。 PEP 418 描述了引入這些功能的一些基本原理。它包括以下簡短描述: 在實際工作中,通常會想優化代碼進一步提升代碼性能,例如想知道將列錶轉換為集合的最有效方法。下面我們使用函數 set() 和直接花括號定義集合{...} 進行比較,看看這兩種方法哪個性能更優,此時需要使用 Python 計時器來比較兩者的運行速度。 該測試結果表明直接花括號定義集合可能會稍微快一些,但其實這些結果非常不確定。如果重新運行代碼,可能會得到截然不同的結果。因為這會受計算機的性能和計算機運行狀態所影響:例如當計算機忙於其他任務時,就會影響我們程序的結果。 更好的方法是多次重複運行相同過程,並獲取平均耗時,就能夠更加精確地測量目標程序的性能大小。因此可以使用 timeit標準庫,它旨在精確測量小代碼片段的執行時間。雖然可以從 Python 導入和調用 timeit.timeit() 作為常規函數,但使用命令行界面通常更方便。可以按如下方式對這兩種變體進行計時: timeit 自動多次調用代碼以平均噪聲測量。timeit 的結果證實 {*nums} 量比 set(nums) 快。 注意:在下載文件或訪問數據庫的代碼上使用 timeit 時要小心。由於 timeit 會自動多次調用程序,因此可能會無意中向服務器發送請求! 最後,IPython 交互式 shell 和 Jupyter Notebook 使用 %timeit 魔術命令對此功能提供了額外支持: 同樣,測量結果表明直接花括號定義集合更快。在 Jupyter Notebooks 中,還可以使用 %%timeit cell-magic 來測量運行整個單元格的時間。 timeit 非常適合對特定代碼片段進行基準測試。但使用它來檢查程序的所有部分並找出哪些部分花費的時間最多會非常麻煩。此時我們想到可以使用分析器。 cProfile是一個分析器,可以隨時從標準庫中訪問它。可以通過多種方式使用它,儘管將其用作命令行工具通常是最直接的: 此命令在打開分析器的情況下運行 download_data.py。將 cProfile 的輸出保存在 download_data.prof 中,由 -o 選項指定。輸出數據是二進制格式,需要專門的程序才能理解。同樣,Python 在標準庫中有一個選項pstats!它可以在 .prof 文件上運行 pstats 模塊會打開一個交互式配置文件統計瀏覽器。 要使用 pstats,請在提示符下鍵入命令。通常你會使用 sort 和 stats 命令,strip可以獲得更清晰的輸出: 此輸出顯示總運行時間為 0.586 秒。它還列出了代碼花費最多時間的十個函數。這裡按累積時間 ( cumtime) 排序,這意味着當給定函數調用另一個函數時,代碼會計算時間。 總時間 ( tottime) 列表示代碼在函數中花費了多少時間,不包括在子函數中的時間。要查找代碼花費最多時間的位置,需要發出另一個sort命令: 可以使用pstats了解代碼大部分時間花在哪裡,然後嘗試優化我們發現的任何瓶頸。還可以使用該工具更好地理解代碼的結構。例如,被調用者和調用者命令將顯示給定函數調用和調用的函數。 還可以研究某些函數。通過使用短語 timer 過濾結果來檢查 Timer 導致的開銷: 完成調查後,使用 quit 離開 pstats 瀏覽器。 如需更加深入了解更強大的配置文件數據接口,可以查看 KCacheGrind[8]。它使用自己的數據格式,也可以使用 pyprof2calltree[9] 從 cProfile 轉換數據: 該命令將轉換 download_data.prof 並打開 KCacheGrind 來分析數據。 這裡為代碼計時的最後一個選項是 line_profiler[10]。cProfile 可以告訴我們代碼在哪些函數中花費的時間最多,但它不會深入顯示該函數中的哪些行最慢,此時就需要 line_profiler 。 注意:還可以分析代碼的內存消耗。這超出了本教程的範圍,如果你需要監控程序的內存消耗,可以查看 memory-profiler[11] 。 行分析需要時間,並且會為我們的運行時增加相當多的開銷。正常的工作流程是首先使用 cProfile 來確定要調查的函數,然後在這些函數上運行 line_profiler。line_profiler 不是標準庫的一部分,因此應該首先按照安裝說明[12]進行設置。 在運行分析器之前,需要告訴它要分析哪些函數。可以通過在源代碼中添加 @profile 裝飾器來實現。例如,要分析 Timer.stop(),在 timer.py 中添加以下內容: 注意,不需要導入profile配置文件,它會在運行分析器時自動添加到全局命名空間中。不過,我們需要在完成分析後刪除該行。否則,會拋出一個 NameError 異常。 接下來,使用 kernprof 運行分析器,它是 line_profiler 包的一部分: 此命令自動將探查器數據保存在名為 download_data.py.lprof 的文件中。可以使用 line_profiler 查看這些結果: 首先,注意本報告中的時間單位是微秒(1e-06 s)。通常,最容易查看的數字是 %Time,它告訴我們代碼在每一行的函數中花費的總時間的百分比。 在本文中,我們嘗試了幾種不同的方法來將 Python 計時器添加到代碼中: 我們還了解了為什麼在對代碼進行基準測試時應該更喜歡time.perf_counter()而不是 time.time(),以及在優化代碼時還有哪些其他有用的替代方法。 現在我們可以在自己的代碼中添加Python計時器函數了!在日誌中跟蹤程序的運行速度將有助於監視腳本。對於類、上下文管理器和裝飾器一起工作的其他用例,你有什麼想法嗎?請在下方留言! 上下滑動查看更多 要更深入地了解 Python 計時器函數,請查看以下資源: time():https://docs.python.org/3/library/time.html#time.time perf_counter_ns():https://docs.python.org/3/library/time.html#time.perf_counter_ns monotonic():https://docs.python.org/3/library/time.html#time.monotonic process_time():https://docs.python.org/3/library/time.html#time.process_time IEEE 754 浮點算術標準:https://en.wikipedia.org/wiki/IEEE_754 timeit:https://docs.python.org/3/library/timeit.html cProfile:https://docs.python.org/3/library/profile.html KCacheGrind:https://kcachegrind.github.io/ pyprof2calltree:https://pypi.org/project/pyprof2calltree/ line_profiler:https://pypi.org/project/line-profiler/ memory-profiler:https://pypi.org/project/memory-profiler/ 安裝說明:https://github.com/pyutils/line_profiler#installation codetiming:https://pypi.org/project/codetiming/ time.perf_counter():https://docs.python.org/3/library/time.html#time.perf_counter timeit:https://docs.python.org/3/library/timeit.html cProfile:https://docs.python.org/3/library/profile.html pstats:https://docs.python.org/3/library/profile.html#pstats.Stats KCachegrind:https://kcachegrind.github.io/ line_profiler:https://pypi.org/project/line-profiler/ memory-profiler:https://pypi.org/project/memory-profiler/ 點這裡👇關注我,記得標星哦~ 推薦閱讀 CDA課程諮詢