這篇文章主要介紹“如何用裝飾器擴(kuò)展Python計(jì)時(shí)器”,在日常操作中,相信很多人在如何用裝飾器擴(kuò)展Python計(jì)時(shí)器問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”如何用裝飾器擴(kuò)展Python計(jì)時(shí)器”的疑惑有所幫助!接下來,請跟著小編一起來學(xué)習(xí)吧!
創(chuàng)新互聯(lián)服務(wù)項(xiàng)目包括民樂網(wǎng)站建設(shè)、民樂網(wǎng)站制作、民樂網(wǎng)頁制作以及民樂網(wǎng)絡(luò)營銷策劃等。多年來,我們專注于互聯(lián)網(wǎng)行業(yè),利用自身積累的技術(shù)優(yōu)勢、行業(yè)經(jīng)驗(yàn)、深度合作伙伴關(guān)系等,向廣大中小型企業(yè)、政府機(jī)構(gòu)等提供互聯(lián)網(wǎng)行業(yè)的解決方案,民樂網(wǎng)站推廣取得了明顯的社會(huì)效益與經(jīng)濟(jì)效益。目前,我們服務(wù)的客戶以成都為中心已經(jīng)輻射到民樂省份的部分城市,未來相信會(huì)繼續(xù)擴(kuò)大服務(wù)區(qū)域并繼續(xù)獲得客戶的支持與信任!
with Timer("some_name"): do_something()
當(dāng)我們在一個(gè)py文件里多次調(diào)用函數(shù) do_something(),那么這將會(huì)變得非常繁瑣并且難以維護(hù)。
def do_something(): with Timer("some_name"): ...
Timer 只需要在一個(gè)地方添加,但這會(huì)為do_something()的整個(gè)定義增加一個(gè)縮進(jìn)級(jí)別。
更好的解決方案是使用 Timer 作為裝飾器。裝飾器是用于修改函數(shù)和類行為的強(qiáng)大構(gòu)造。
裝飾器是包裝另一個(gè)函數(shù)以修改其行為的函數(shù)。你可能會(huì)有疑問,這怎么實(shí)現(xiàn)呢?其實(shí)函數(shù)是 Python 中的first-class 對象,換句話說,函數(shù)可以以變量的形式傳遞給其他函數(shù)的參數(shù),就像任何其他常規(guī)對象一樣。因此此處有較大的靈活性,也是 Python 幾個(gè)最強(qiáng)大功能的基礎(chǔ)。
我們首先創(chuàng)建第一個(gè)示例,一個(gè)什么都不做的裝飾器:
def turn_off(func): return lambda *args, **kwargs: None
首先注意這個(gè)turn_off()只是一個(gè)常規(guī)函數(shù)。之所以成為裝飾器,是因?yàn)樗鼘⒁粋€(gè)函數(shù)作為其唯一參數(shù)并返回另一個(gè)函數(shù)。我們可以使用turn_off()來修改其他函數(shù),例如:
>>> print("Hello") Hello >>> print = turn_off(print) >>> print("Hush") >>> # Nothing is printed
代碼行 print = turn_off(print) 用 turn_off() 裝飾器裝飾了 print 語句。實(shí)際上,它將函數(shù) print() 替換為匿名函數(shù) lambda *args, **kwargs: None 并返回 turn_off()。匿名函數(shù) lambda 除了返回 None 之外什么都不做。
要定義更多豐富的裝飾器,需要了解內(nèi)部函數(shù)。內(nèi)部函數(shù)是在另一個(gè)函數(shù)內(nèi)部定義的函數(shù),它的一種常見用途是創(chuàng)建函數(shù)工廠:
def create_multiplier(factor): def multiplier(num): return factor * num return multiplier
multiplier() 是一個(gè)內(nèi)部函數(shù),在 create_multiplier() 內(nèi)部定義。注意可以訪問 multiplier() 內(nèi)部的因子,而 multiplier()未在 create_multiplier() 外部定義:
multiplier
Traceback (most recent call last): File "", line 1, inNameError: name 'multiplier' is not defined
相反,可以使用create_multiplier()創(chuàng)建新的 multiplier 函數(shù),每個(gè)函數(shù)都基于不同的參數(shù)factor:
double = create_multiplier(factor=2) double(3)
6
quadruple = create_multiplier(factor=4) quadruple(7)
28
同樣,可以使用內(nèi)部函數(shù)來創(chuàng)建裝飾器。裝飾器是一個(gè)返回函數(shù)的函數(shù):
def triple(func): def wrapper_triple(*args, **kwargs): print(f"Tripled {func.__name__!r}") value = func(*args, **kwargs) return value * 3 return wrapper_triple
triple() 是一個(gè)裝飾器,因?yàn)樗且粋€(gè)期望函數(shù) func() 作為其唯一參數(shù)并返回另一個(gè)函數(shù) wrapper_triple() 的函數(shù)。注意 triple() 本身的結(jié)構(gòu):
第 1 行開始了triple() 的定義,并期望一個(gè)函數(shù)作為參數(shù)。
第 2 到 5 行定義了內(nèi)部函數(shù)wrapper_triple()。
第 6 行返回wrapper_triple()。
這是種定義裝飾器的一般模式(注意內(nèi)部函數(shù)的部分):
第 2 行開始wrapper_triple() 的定義。此函數(shù)將替換triple() 修飾的任何函數(shù)。參數(shù)是*args 和**kwargs,用于收集傳遞給函數(shù)的任何位置參數(shù)和關(guān)鍵字參數(shù)。我們可以靈活地在任何函數(shù)上使用triple()。
第 3 行打印出修飾函數(shù)的名稱,并指出已對其應(yīng)用了triple()。
第 4 行調(diào)用func(),triple() 修飾的函數(shù)。它傳遞傳遞給wrapper_triple() 的所有參數(shù)。
第 5 行將func() 的返回值增加三倍并將其返回。
接下來的代碼中,knock() 是一個(gè)返回單詞 Penny 的函數(shù),將其傳給triple() 函數(shù),并看看輸出結(jié)果是什么。
>>> def knock(): ... return "Penny! " >>> knock = triple(knock) >>> result = knock() Tripled 'knock' >>> result 'Penny! Penny! Penny! '
我們都知道,文本字符串與數(shù)字相乘,是字符串的一種重復(fù)形式,因此字符串 'Penny' 重復(fù)了 3 次。可以認(rèn)為,裝飾發(fā)生在knock = triple(knock)。
上述方法雖然實(shí)現(xiàn)了裝飾器的功能,但似乎有點(diǎn)笨拙。PEP 318 引入了一種更方便的語法來應(yīng)用裝飾器。下面的 knock() 定義與上面的定義相同,但裝飾器用法不同。
>>> @triple ... def knock(): ... return "Penny! " ... >>> result = knock() Tripled 'knock' >>> result 'Penny! Penny! Penny! '
@ 符號(hào)用于應(yīng)用裝飾器,@triple 表示 triple() 應(yīng)用于緊隨其后定義的函數(shù)。
Python 標(biāo)準(zhǔn)庫中定義的裝飾器方法之一是:@functools.wraps。這在定義你自己的裝飾器時(shí)非常有用。前面說過,裝飾器是用另一個(gè)函數(shù)替換了一個(gè)函數(shù),會(huì)給你的函數(shù)帶來一個(gè)微妙的變化:
knock
@triple 裝飾了 knock(),然后被 wrapper_triple() 內(nèi)部函數(shù)替換,被裝飾的函數(shù)的名字會(huì)變成裝飾器函數(shù),除了名稱,還有文檔字符串和其他元數(shù)據(jù)都將會(huì)被替換。但有時(shí),我們并不總是想將被修飾的函數(shù)的所有信息都被修改了。此時(shí) @functools.wraps 正好解決了這個(gè)問題,如下所示:
import functools def triple(func): @functools.wraps(func) def wrapper_triple(*args, **kwargs): print(f"Tripled {func.__name__!r}") value = func(*args, **kwargs) return value * 3 return wrapper_triple
使用 @triple 的這個(gè)新定義保留元數(shù)據(jù):
@triple def knock(): return "Penny! " knock
注意knock() 即使在被裝飾之后,也同樣保留了它的原有函數(shù)名稱。當(dāng)定義裝飾器時(shí),使用 @functools.wraps 是一種不錯(cuò)的選擇,可以為大多數(shù)裝飾器使用的如下模板:
import functools def decorator(func): @functools.wraps(func) def wrapper_decorator(*args, **kwargs): # Do something before value = func(*args, **kwargs) # Do something after return value return wrapper_decorator
在本節(jié)中,云朵君將和大家一起學(xué)習(xí)如何擴(kuò)展 Python 計(jì)時(shí)器,并以裝飾器的形式使用它。接下來我們從頭開始創(chuàng)建 Python 計(jì)時(shí)器裝飾器。
根據(jù)上面的模板,我們只需要決定在調(diào)用裝飾函數(shù)之前和之后要做什么。這與進(jìn)入和退出上下文管理器時(shí)的注意事項(xiàng)類似。在調(diào)用修飾函數(shù)之前啟動(dòng) Python 計(jì)時(shí)器,并在調(diào)用完成后停止 Python 計(jì)時(shí)器??梢园慈缦路绞蕉x @timer 裝飾器:
import functools import time def timer(func): @functools.wraps(func) def wrapper_timer(*args, **kwargs): tic = time.perf_counter() value = func(*args, **kwargs) toc = time.perf_counter() elapsed_time = toc - tic print(f"Elapsed time: {elapsed_time:0.4f} seconds") return value return wrapper_timer
可以按如下方式應(yīng)用 @timer:
@timer def download_data(): 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) download_data() # Python Timer Functions: Three Ways to Monitor Your Code
[ ... ] Elapsed time: 0.5414 seconds
回想一下,還可以將裝飾器應(yīng)用于先前定義的下載數(shù)據(jù)的函數(shù):
requests.get = requests.get(source_url, headers=headers)
使用裝飾器的一個(gè)優(yōu)點(diǎn)是只需要應(yīng)用一次,并且每次都會(huì)對函數(shù)計(jì)時(shí):
data = requests.get(0)
Elapsed time: 0.5512 seconds
雖然@timer 順利完成了對目標(biāo)函數(shù)的定時(shí)。但從某種意義上說,你又回到了原點(diǎn),因?yàn)樵撗b飾器 @timer 失去了前面定義的類 Timer 的靈活性或便利性。換句話說,我們需要將 Timer 類表現(xiàn)得像一個(gè)裝飾器。
現(xiàn)在我們似乎已經(jīng)將裝飾器用作應(yīng)用于其他函數(shù)的函數(shù),但其實(shí)不然,因?yàn)檠b飾器必須是可調(diào)用的。Python中有許多可調(diào)用的類型,可以通過在其類中定義特殊的.__call__()方法來使自己的對象可調(diào)用。以下函數(shù)和類的行為類似:
def square(num): return num ** 2 square(4)
16
class Squarer: def __call__(self, num): return num ** 2 square = Squarer() square(4)
16
這里,square 是一個(gè)可調(diào)用的實(shí)例,可以對數(shù)字求平方,就像square()第一個(gè)示例中的函數(shù)一樣。
我們現(xiàn)在向現(xiàn)有Timer類添加裝飾器功能,首先需要 import functools。
# timer.py import functools # ... @dataclass class Timer: # The rest of the code is unchanged def __call__(self, func): """Support using Timer as a decorator""" @functools.wraps(func) def wrapper_timer(*args, **kwargs): with self: return func(*args, **kwargs) return wrapper_timer
在之前定義的上下文管理器 Timer ,給我們帶來了不少便利。而這里使用的裝飾器,似乎更加方便。
@Timer(text="Downloaded the tutorial in {:.2f} seconds") def download_data(): 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) download_data() # Python Timer Functions: Three Ways to Monitor Your Code
[ ... ] Downloaded the tutorial in 0.72 seconds
有一種更直接的方法可以將 Python 計(jì)時(shí)器變成裝飾器。其實(shí)上下文管理器和裝飾器之間的一些相似之處:它們通常都用于在執(zhí)行某些給定代碼之前和之后執(zhí)行某些操作。
基于這些相似之處,在 python 標(biāo)準(zhǔn)庫中定義了一個(gè)名為 ContextDecorator 的 mixin 類,它可以簡單地通過繼承 ContextDecorator 來為上下文管理器類添加裝飾器函數(shù)。
from contextlib import ContextDecorator # ... @dataclass class Timer(ContextDecorator): # Implementation of Timer is unchanged
當(dāng)以這種方式使用 ContextDecorator 時(shí),無需自己實(shí)現(xiàn) .__call__(),因此我們可以大膽地將其從 Timer 類中刪除。
接下來,再最后一次重改 download_data.py 示例,使用 Python 計(jì)時(shí)器作為裝飾器:
# download_data.py import requests from timer import Timer @Timer() 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()
我們與之前的寫法進(jìn)行比較,唯一的區(qū)別是第 3 行的 Timer 的導(dǎo)入和第 4 行的 @Timer() 的應(yīng)用。使用裝飾器的一個(gè)顯著優(yōu)勢是它們通常很容易調(diào)用。
但是,裝飾器仍然適用于整個(gè)函數(shù)。這意味著代碼除了記錄了下載數(shù)據(jù)所需的時(shí)間外,還考慮了保存數(shù)據(jù)所需的時(shí)間。運(yùn)行腳本:
$ python download_data.py # Python Timer Functions: Three Ways to Monitor Your Code
[ ... ] Elapsed time: 0.69 seconds
從上面打印出來的結(jié)果可以看到,代碼記錄了下載數(shù)據(jù)和保持?jǐn)?shù)據(jù)一共所需的時(shí)間。
當(dāng)使用 Timer 作為裝飾器時(shí),會(huì)看到與使用上下文管理器類似的優(yōu)勢:
省時(shí)省力:只需要一行額外的代碼即可為函數(shù)的執(zhí)行計(jì)時(shí)。
可讀性:當(dāng)添加裝飾器時(shí),可以更清楚地注意到代碼會(huì)對函數(shù)計(jì)時(shí)。
一致性:只需要在定義函數(shù)時(shí)添加裝飾器即可。每次調(diào)用時(shí),代碼都會(huì)始終如一地計(jì)時(shí)。
然而,裝飾器不如上下文管理器靈活,只能將它們應(yīng)用于完整函數(shù)。
這里展開下面的代碼塊以查看 Python 計(jì)時(shí)器timer.py的完整源代碼。
上下滑動(dòng)查看更多源碼
# timer.py import time from contextlib import ContextDecorator from dataclasses import dataclass, field from typing import Any, Callable, ClassVar, Dict, Optional class TimerError(Exception): """A custom exception used to report errors in use of Timer class""" @dataclass class Timer(ContextDecorator): """Time your code using a class, context manager, or decorator""" 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: """Initialization: add timer to dict of timers""" if self.name: 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 def __enter__(self) -> "Timer": """Start a new timer as a context manager""" self.start() return self def __exit__(self, *exc_info: Any) -> None: """Stop the context manager timer""" self.stop()
可以自己使用代碼,方法是將其保存到一個(gè)名為的文件中timer.py并將其導(dǎo)入:
from timer import Timer
PyPI 上也提供了 Timer,因此更簡單的選擇是使用 pip 安裝它:
pip install codetiming
注意,PyPI 上的包名稱是codetiming,安裝包和導(dǎo)入時(shí)都需要使用此名稱Timer:
from codetiming import Timer
除了名稱和一些附加功能之外,codetiming.Timer 與 timer.Timer 完全一樣??偠灾?,可以通過三種不同的方式使用 Timer:
1. 作為一個(gè)類:
t = Timer(name="class") t.start() # Do something t.stop()
2. 作為上下文管理器:
with Timer(name="context manager"): # Do something
3. 作為裝飾器:
@Timer(name="decorator") def stuff(): # Do something
這種 Python 計(jì)時(shí)器主要用于監(jiān)控代碼在單個(gè)關(guān)鍵代碼塊或函數(shù)上所花費(fèi)的時(shí)間。
Python定時(shí)器裝飾器已經(jīng)學(xué)習(xí)完畢了,接下來是總結(jié)了一些其他的 Python 定時(shí)器函數(shù),如果你對其不太感興趣,可以直接跳到最后。
使用 Python 對代碼進(jìn)行計(jì)時(shí)有很多選擇。這里我們學(xué)習(xí)了如何創(chuàng)建一個(gè)靈活方便的類,可以通過多種不同的方式使用該類。對 PyPI 的快速搜索發(fā)現(xiàn),已經(jīng)有許多項(xiàng)目提供 Python 計(jì)時(shí)器解決方案。
在本節(jié)中,我們首先了解有關(guān)標(biāo)準(zhǔn)庫中用于測量時(shí)間的不同函數(shù)的更多信息,包括為什么 perf_counter() 更好,然后探索優(yōu)化代碼的替代方案。
在本文之前,包括前面介紹python定時(shí)器的文章中,我們一直在使用 perf_counter() 來進(jìn)行實(shí)際的時(shí)間測量,但是 Python 的時(shí)間庫附帶了幾個(gè)其他也可以測量時(shí)間的函數(shù)。這里有一些:
time()
perf_counter_ns()
monotonic()
process_time()
擁有多個(gè)函數(shù)的一個(gè)原因是 Python 將時(shí)間表示為浮點(diǎn)數(shù)。浮點(diǎn)數(shù)本質(zhì)上是不準(zhǔn)確的。之前可能已經(jīng)看到過這樣的結(jié)果:
>>> 0.1 + 0.1 + 0.1 0.30000000000000004 >>> 0.1 + 0.1 + 0.1 == 0.3 False
Python 的 Float 遵循 IEEE 754 浮點(diǎn)算術(shù)標(biāo)準(zhǔn)[5],該標(biāo)準(zhǔn)以 64 位表示所有浮點(diǎn)數(shù)。因?yàn)楦↑c(diǎn)數(shù)有無限多位數(shù),即不能用有限的位數(shù)來表達(dá)它們。
考慮time()這個(gè)函數(shù)的主要目的,是它表示的是現(xiàn)在的實(shí)際時(shí)間。它以自給定時(shí)間點(diǎn)(稱為紀(jì)元)以來的秒數(shù)來表示函數(shù)。time()返回的數(shù)字很大,這意味著可用的數(shù)字較少,因而分辨率會(huì)受到影響。簡而言之, time()無法測量納秒級(jí)差異:
>>> import time >>> t = time.time() >>> t 1564342757.0654016 >>> t + 1e-9 1564342757.0654016 >>> t == t + 1e-9 True
一納秒是十億分之一秒。上面代碼中,將納秒添加到參數(shù) t ,他并不會(huì)影響結(jié)果。與 time() 不同的是,perf_counter() 使用一些未定義的時(shí)間點(diǎn)作為它的紀(jì)元,它可以使用更小的數(shù)字,從而獲得更好的分辨率:
>>> import time >>> p = time.perf_counter() >>> p 11370.015653846 >>> p + 1e-9 11370.015653847 >>> p == p + 1e-9 False
眾所周知,將時(shí)間表示為浮點(diǎn)數(shù)是非常具有挑戰(zhàn)的一件事,因此 Python 3.7 引入了一個(gè)新選項(xiàng):每個(gè)時(shí)間測量函數(shù)現(xiàn)在都有一個(gè)相應(yīng)的 _ns 函數(shù),它以 int 形式返回納秒數(shù),而不是以浮點(diǎn)數(shù)形式返回秒數(shù)。例如,time() 現(xiàn)在有一個(gè)名為 time_ns() 的納秒對應(yīng)項(xiàng):
import time time.time_ns()
1564342792866601283
整數(shù)在 Python 中是無界的,因此 time_ns() 可以為所有永恒提供納秒級(jí)分辨率。同樣,perf_counter_ns() 是 perf_counter() 的納秒版本:
>>> import time >>> time.perf_counter() 13580.153084446 >>> time.perf_counter_ns() 13580765666638
我們注意到,因?yàn)?perf_counter() 已經(jīng)提供納秒級(jí)分辨率,所以使用 perf_counter_ns() 的優(yōu)勢較少。
注意: perf_counter_ns() 僅在 Python 3.7 及更高版本中可用。在 Timer 類中使用了 perf_counter()。這樣,也可以在較舊的 Python 版本上使用 Timer。
有兩個(gè)函數(shù)time不測量time.sleep時(shí)間:process_time()和thread_time()。通常希望Timer能夠測量代碼所花費(fèi)的全部時(shí)間,因此這兩個(gè)函數(shù)并不常用。而函數(shù) monotonic(),顧名思義,它是一個(gè)單調(diào)計(jì)時(shí)器,一個(gè)永遠(yuǎn)不會(huì)向后移動(dòng)的 Python 計(jì)時(shí)器。
除了 time() 之外,所有這些函數(shù)都是單調(diào)的,如果調(diào)整了系統(tǒng)時(shí)間,它也隨之倒退。在某些系統(tǒng)上,monotonic() 與 perf_counter() 的功能相同,可以互換使用。我們可以使用 time.get_clock_info() 獲取有關(guān) Python 計(jì)時(shí)器函數(shù)的更多信息:
>>> import time >>> time.get_clock_info("monotonic") namespace(adjustable=False, implementation='clock_gettime(CLOCK_MONOTONIC)', monotonic=True, resolution=1e-09) >>> time.get_clock_info("perf_counter") namespace(adjustable=False, implementation='clock_gettime(CLOCK_MONOTONIC)', monotonic=True, resolution=1e-09)
注意,不同系統(tǒng)上的結(jié)果可能會(huì)有所不同。
PEP 418 描述了引入這些功能的一些基本原理。它包括以下簡短描述:
time.monotonic(): 超時(shí)和調(diào)度,不受系統(tǒng)時(shí)鐘更新影響
time.perf_counter():基準(zhǔn)測試,短期內(nèi)最精確的時(shí)鐘
time.process_time():分析進(jìn)程的CPU時(shí)間
在實(shí)際工作中,通常會(huì)想優(yōu)化代碼進(jìn)一步提升代碼性能,例如想知道將列表轉(zhuǎn)換為集合的最有效方法。下面我們使用函數(shù) set() 和直接花括號(hào)定義集合 {...} 進(jìn)行比較,看看這兩種方法哪個(gè)性能更優(yōu),此時(shí)需要使用 Python 計(jì)時(shí)器來比較兩者的運(yùn)行速度。
>>> from timer import Timer >>> numbers = [7, 6, 1, 4, 1, 8, 0, 6] >>> with Timer(text="{:.8f}"): ... set(numbers) ... {0, 1, 4, 6, 7, 8} 0.00007373 >>> with Timer(text="{:.8f}"): ... {*numbers} ... {0, 1, 4, 6, 7, 8} 0.00006204
該測試結(jié)果表明直接花括號(hào)定義集合可能會(huì)稍微快一些,但其實(shí)這些結(jié)果非常不確定。如果重新運(yùn)行代碼,可能會(huì)得到截然不同的結(jié)果。因?yàn)檫@會(huì)受計(jì)算機(jī)的性能和計(jì)算機(jī)運(yùn)行狀態(tài)所影響:例如當(dāng)計(jì)算機(jī)忙于其他任務(wù)時(shí),就會(huì)影響我們程序的結(jié)果。
更好的方法是多次重復(fù)運(yùn)行相同過程,并獲取平均耗時(shí),就能夠更加精確地測量目標(biāo)程序的性能大小。因此可以使用 timeit 標(biāo)準(zhǔn)庫,它旨在精確測量小代碼片段的執(zhí)行時(shí)間。雖然可以從 Python 導(dǎo)入和調(diào)用 timeit.timeit() 作為常規(guī)函數(shù),但使用命令行界面通常更方便??梢园慈缦路绞綄@兩種變體進(jìn)行計(jì)時(shí):
$ python -m timeit --setup "nums = [7, 6, 1, 4, 1, 8, 0, 6]" "set(nums)" 2000000 loops, best of 5: 163 nsec per loop $ python -m timeit --setup "nums = [7, 6, 1, 4, 1, 8, 0, 6]" "{*nums}" 2000000 loops, best of 5: 121 nsec per loop
timeit 自動(dòng)多次調(diào)用代碼以平均噪聲測量。timeit 的結(jié)果證實(shí) {*nums} 量比 set(nums) 快。
注意:在下載文件或訪問數(shù)據(jù)庫的代碼上使用 timeit 時(shí)要小心。由于 timeit 會(huì)自動(dòng)多次調(diào)用程序,因此可能會(huì)無意中向服務(wù)器發(fā)送請求!
最后,IPython 交互式 shell 和 Jupyter Notebook 使用 %timeit 魔術(shù)命令對此功能提供了額外支持:
In [1]: numbers = [7, 6, 1, 4, 1, 8, 0, 6] In [2]: %timeit set(numbers) 171 ns ± 0.748 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each) In [3]: %timeit {*numbers} 147 ns ± 2.62 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
同樣,測量結(jié)果表明直接花括號(hào)定義集合更快。在 Jupyter Notebooks 中,還可以使用 %%timeit cell-magic 來測量運(yùn)行整個(gè)單元格的時(shí)間。
timeit 非常適合對特定代碼片段進(jìn)行基準(zhǔn)測試。但使用它來檢查程序的所有部分并找出哪些部分花費(fèi)的時(shí)間最多會(huì)非常麻煩。此時(shí)我們想到可以使用分析器。
cProfile 是一個(gè)分析器,可以隨時(shí)從標(biāo)準(zhǔn)庫中訪問它??梢酝ㄟ^多種方式使用它,盡管將其用作命令行工具通常是最直接的:
$ python -m cProfile -o download_data.prof download_data.py
此命令在打開分析器的情況下運(yùn)行 download_data.py。將 cProfile 的輸出保存在 download_data.prof 中,由 -o 選項(xiàng)指定。輸出數(shù)據(jù)是二進(jìn)制格式,需要專門的程序才能理解。同樣,Python 在標(biāo)準(zhǔn)庫中有一個(gè)選項(xiàng) pstats!它可以在 .prof 文件上運(yùn)行 pstats 模塊會(huì)打開一個(gè)交互式配置文件統(tǒng)計(jì)瀏覽器。
$ python -m pstats download_data.prof Welcome to the profile statistics browser. download_data.prof% help ...
要使用 pstats,請?jiān)谔崾痉骆I入命令。通常你會(huì)使用 sort 和 stats 命令,strip 可以獲得更清晰的輸出:
download_data.prof% strip download_data.prof% sort cumtime download_data.prof% stats 10 ...
此輸出顯示總運(yùn)行時(shí)間為 0.586 秒。它還列出了代碼花費(fèi)最多時(shí)間的十個(gè)函數(shù)。這里按累積時(shí)間 ( cumtime) 排序,這意味著當(dāng)給定函數(shù)調(diào)用另一個(gè)函數(shù)時(shí),代碼會(huì)計(jì)算時(shí)間。
總時(shí)間 ( tottime) 列表示代碼在函數(shù)中花費(fèi)了多少時(shí)間,不包括在子函數(shù)中的時(shí)間。要查找代碼花費(fèi)最多時(shí)間的位置,需要發(fā)出另一個(gè)sort命令:
download_data.prof% sort tottime download_data.prof% stats 10 ...
可以使用 pstats了解代碼大部分時(shí)間花在哪里,然后嘗試優(yōu)化我們發(fā)現(xiàn)的任何瓶頸。還可以使用該工具更好地理解代碼的結(jié)構(gòu)。例如,被調(diào)用者和調(diào)用者命令將顯示給定函數(shù)調(diào)用和調(diào)用的函數(shù)。
還可以研究某些函數(shù)。通過使用短語 timer 過濾結(jié)果來檢查 Timer 導(dǎo)致的開銷:
download_data.prof% stats timer ...
完成調(diào)查后,使用 quit 離開 pstats 瀏覽器。
如需更加深入了解更強(qiáng)大的配置文件數(shù)據(jù)接口,可以查看 KCacheGrind[8]。它使用自己的數(shù)據(jù)格式,也可以使用 pyprof2calltree[9] 從 cProfile 轉(zhuǎn)換數(shù)據(jù):
$ pyprof2calltree -k -i download_data.prof
該命令將轉(zhuǎn)換 download_data.prof 并打開 KCacheGrind 來分析數(shù)據(jù)。
這里為代碼計(jì)時(shí)的最后一個(gè)選項(xiàng)是 line_profiler[10]。cProfile 可以告訴我們代碼在哪些函數(shù)中花費(fèi)的時(shí)間最多,但它不會(huì)深入顯示該函數(shù)中的哪些行最慢,此時(shí)就需要 line_profiler 。
注意:還可以分析代碼的內(nèi)存消耗。這超出了本教程的范圍,如果你需要監(jiān)控程序的內(nèi)存消耗,可以查看 memory-profiler[11] 。
行分析需要時(shí)間,并且會(huì)為我們的運(yùn)行時(shí)增加相當(dāng)多的開銷。正常的工作流程是首先使用 cProfile 來確定要調(diào)查的函數(shù),然后在這些函數(shù)上運(yùn)行 line_profiler。line_profiler 不是標(biāo)準(zhǔn)庫的一部分,因此應(yīng)該首先按照安裝說明[12]進(jìn)行設(shè)置。
在運(yùn)行分析器之前,需要告訴它要分析哪些函數(shù)??梢酝ㄟ^在源代碼中添加 @profile 裝飾器來實(shí)現(xiàn)。例如,要分析 Timer.stop(),在 timer.py 中添加以下內(nèi)容:
@profile def stop(self) -> float: # 其余部分不變
注意,不需要導(dǎo)入profile配置文件,它會(huì)在運(yùn)行分析器時(shí)自動(dòng)添加到全局命名空間中。不過,我們需要在完成分析后刪除該行。否則,會(huì)拋出一個(gè) NameError 異常。
接下來,使用 kernprof 運(yùn)行分析器,它是 line_profiler 包的一部分:
$ kernprof -l download_data.py
此命令自動(dòng)將探查器數(shù)據(jù)保存在名為 download_data.py.lprof 的文件中??梢允褂?line_profiler 查看這些結(jié)果:
$ python -m line_profiler download_data.py.lprof Timer unit: 1e-06 s Total time: 1.6e-05 s File: /home/realpython/timer.py Function: stop at line 35 # Hits Time PrHit %Time Line Contents ===================================== ...
首先,注意本報(bào)告中的時(shí)間單位是微秒(1e-06 s)。通常,最容易查看的數(shù)字是 %Time,它告訴我們代碼在每一行的函數(shù)中花費(fèi)的總時(shí)間的百分比。
到此,關(guān)于“如何用裝飾器擴(kuò)展Python計(jì)時(shí)器”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請繼續(xù)關(guān)注創(chuàng)新互聯(lián)網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)砀鄬?shí)用的文章!