首頁 > 軟體

詳解利用裝飾器擴充套件Python計時器

2022-06-30 18:03:59

介紹

在本文中,雲朵君將和大家一起了解裝飾器的工作原理,如何將我們之前定義的定時器類 Timer 擴充套件為裝飾器,以及如何簡化計時功能。最後對 Python 定時器系列文章做個小結。

這是我們手把手教你實現 Python 定時器的第三篇文章。前兩篇:分別是​手把手教你實現一個 Python 計時器,和用上下文管理器擴充套件 Python 計時器​,使得我們的 Timer 類方便用、美觀實用。

但我們並不滿足於此,仍然有一個用例可以進一步簡化它。假設我們需要跟蹤程式碼庫中一個給定函數所花費的時間。使用上下文管理器,基本上有兩種不同的選擇:

1. 每次呼叫函數時使用 Timer:

with Timer("some_name"):
    do_something()

當我們在一個py檔案裡多次呼叫函數 do_something(),那麼這將會變得非常繁瑣並且難以維護。

2. 將程式碼包裝在上下文管理器中的函數中:

def do_something():
    with Timer("some_name"):
        ...

Timer 只需要在一個地方新增,但這會為do_something()的整個定義增加一個縮排級別。

更好的解決方案是使用 Timer 作為裝飾器。裝飾器是用於修改函數和類行為的強大構造。

理解 Python 中的裝飾器

裝飾器是包裝另一個函數以修改其行為的函數。你可能會有疑問,這怎麼實現呢?其實函數是 Python 中的first-class 物件,換句話說,函數可以以變數的形式傳遞給其他函數的引數,就像任何其他常規物件一樣。因此此處有較大的靈活性,也是 Python 幾個最強大功能的基礎。

我們首先建立第一個範例,一個什麼都不做的裝飾器:

def turn_off(func):
    return lambda *args, **kwargs: None

首先注意這個turn_off()只是一個常規函數。之所以成為裝飾器,是因為它將一個函數作為其唯一引數並返回另一個函數。我們可以使用turn_off()來修改其他函數,例如:

>>> print("Hello")
Hello

>>> print = turn_off(print)
>>> print("Hush")
>>> # Nothing is printed

程式碼行 print = turn_off(print) 用 turn_off() 裝飾器裝飾了 print 語句。實際上,它將函數 print() 替換為匿名函數 lambda *args, **kwargs: None 並返回 turn_off()匿名函數 lambda 除了返回 None 之外什麼都不做。

要定義更多豐富的裝飾器,需要了解內部函數內部函數是在另一個函數內部定義的函數,它的一種常見用途是建立函數工廠:

def create_multiplier(factor):
    def multiplier(num):
        return factor * num
    return multiplier

multiplier() 是一個內部函數,在 create_multiplier() 內部定義。注意可以存取 multiplier() 內部的因子,而 multiplier()未在 create_multiplier() 外部定義:

multiplier

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'multiplier' is not defined

相反,可以使用create_multiplier()建立新的 multiplier 函數,每個函數都基於不同的引數factor:

double = create_multiplier(factor=2)
double(3)

6

quadruple = create_multiplier(factor=4)
quadruple(7)

28 

同樣,可以使用內部函數來建立裝飾器。裝飾器是一個返回函數的函數:

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() 是一個裝飾器,因為它是一個期望函數 func() 作為其唯一引數並返回另一個函數 wrapper_triple() 的函數。注意 triple() 本身的結構:

  • 第 1 行開始了 triple() 的定義,並期望一個函數作為引數。
  • 第 2 到 5 行定義了內部函數 wrapper_triple()
  • 第 6 行返回 wrapper_triple()

這是種定義裝飾器的一般模式(注意內部函數的部分):

  • 第 2 行開始 wrapper_triple() 的定義。此函數將替換 triple() 修飾的任何函數。引數是 *args 和 **kwargs,用於收集傳遞給函數的任何位置引數和關鍵字引數。我們可以靈活地在任何函數上使用 triple()
  • 第 3 行列印出修飾函數的名稱,並指出已對其應用了 triple()
  • 第 4 行呼叫 func()triple() 修飾的函數。它傳遞傳遞給 wrapper_triple() 的所有引數。
  • 第 5 行將 func() 的返回值增加三倍並將其返回。

接下來的程式碼中,knock() 是一個返回單詞 Penny 的函數,將其傳給triple() 函數,並看看輸出結果是什麼。

>>> def knock():
...     return "Penny! "
>>> knock = triple(knock)
>>> result = knock()
Tripled 'knock'

>>> result
'Penny! Penny! Penny! '

我們都知道,文字字串與數位相乘,是字串的一種重複形式,因此字串 'Penny' 重複了 3 次。可以認為,裝飾發生在knock = triple(knock)

上述方法雖然實現了裝飾器的功能,但似乎有點笨拙。PEP 318 引入了一種更方便的語法來應用裝飾器。下面的 knock() 定義與上面的定義相同,但裝飾器用法不同。

>>> @triple
... def knock():
...     return "Penny! "
...
>>> result = knock()
Tripled 'knock'

>>> result
'Penny! Penny! Penny! '

@ 符號用於應用裝飾器,@triple 表示 triple() 應用於緊隨其後定義的函數。

Python 標準庫中定義的裝飾器方法之一是:@functools.wraps。這在定義你自己的裝飾器時非常有用。前面說過,裝飾器是用另一個函數替換了一個函數,會給你的函數帶來一個微妙的變化:

knock
<function triple.<locals>.wrapper_triple 
at 0x7fa3bfe5dd90>

@triple 裝飾了 knock(),然後被 wrapper_triple() 內部函數替換,被裝飾的函數的名字會變成裝飾器函數,除了名稱,還有檔案字串和其他後設資料都將會被替換。但有時,我們並不總是想將被修飾的函數的所有資訊都被修改了。此時 @functools.wraps 正好解決了這個問題,如下所示:

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 的這個新定義保留後設資料:

@triple
def knock():
    return "Penny! "
knock
<function knock at 0x7fa3bfe5df28>

注意knock() 即使在被裝飾之後,也同樣保留了它的原有函數名稱。當定義裝飾器時,使用 @functools.wraps 是一種不錯的選擇,可以為大多數裝飾器使用的如下模板:

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

建立 Python 定時器裝飾器

在本節中,雲朵君將和大家一起學習如何擴充套件 Python 計時器,並以裝飾器的形式使用它。接下來我們從頭開始建立 Python 計時器裝飾器。

根據上面的模板,我們只需要決定在呼叫裝飾函數之前和之後要做什麼。這與進入和退出上下文管理器時的注意事項類似。在呼叫修飾函數之前啟動 Python 計時器,並在呼叫完成後停止 Python 計時器。可以按如下方式定義 @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

可以按如下方式應用 @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 second

回想一下,還可以將裝飾器應用於先前定義的下載資料的函數:

requests.get = requests.get(source_url, headers=headers) 

使用裝飾器的一個優點是隻需要應用一次,並且每次都會對函數計時:

data = requests.get(0)

Elapsed time: 0.5512 seconds

雖然@timer 順利完成了對目標函數的定時。但從某種意義上說,你又回到了原點,因為該裝飾器 @timer 失去了前面定義的類 Timer 的靈活性或便利性。換句話說,我們需要將 Timer 類表現得像一個裝飾器。

現在我們似乎已經將裝飾器用作應用於其他函數的函數,但其實不然,因為裝飾器必須是可呼叫的。Python中有許多可呼叫的型別,可以通過在其類中定義特殊的.__call__()方法來使自己的物件可呼叫。以下函數和類的行為類似:

def square(num):
    return num ** 2

square(4)

16

class Squarer:
    def __call__(self, num):
        return num ** 2

square = Squarer()
square(4)

16

這裡,square 是一個可呼叫的範例,可以對數位求平方,就像square()第一個範例中的函數一樣。

我們現在向現有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 計時器變成裝飾器。其實上下文管理器和裝飾器之間的一些相似之處:它們通常都用於在執行某些給定程式碼之前和之後執行某些操作

基於這些相似之處,在 python 標準庫中定義了一個名為 ContextDecorator 的 mixin 類,它可以簡單地通過繼承 ContextDecorator 來為上下文管理器類新增裝飾器函數。

from contextlib import ContextDecorator
# ...
@dataclass
class Timer(ContextDecorator):
    # Implementation of Timer is unchanged

當以這種方式使用 ContextDecorator 時,無需自己實現 .__call__(),因此我們可以大膽地將其從 Timer 類中刪除。

使用 Python 定時器裝飾器

接下來,再最後一次重改 download_data.py 範例,使用 Python 計時器作為裝飾器:

# 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()

我們與之前的寫法進行比較,唯一的區別是第 3 行的 Timer 的匯入和第 4 行的 @Timer()  的應用。使用裝飾器的一個顯著優勢是它們通常很容易呼叫

但是,裝飾器仍然適用於整個函數。這意味著程式碼除了記錄了下載資料所需的時間外,還考慮了儲存資料所需的時間。執行指令碼:

$ python download_data.py
# Python Timer Functions: Three Ways to Monitor Your Code

[ ... ]
Elapsed time: 0.69 seconds

從上面列印出來的結果可以看到,程式碼記錄了下載資料和保持資料一共所需的時間。

當使用 Timer 作為裝飾器時,會看到與使用上下文管理器類似的優勢:

  • 省時省力: 只需要一行額外的程式碼即可為函數的執行計時。
  • 可讀性: 當新增裝飾器時,可以更清楚地注意到程式碼會對函數計時。
  • 一致性: 只需要在定義函數時新增裝飾器即可。每次呼叫時,程式碼都會始終如一地計時。

然而,裝飾器不如上下文管理器靈活,只能將它們應用於完整函數。

Python 計時器程式碼

這裡展開下面的程式碼塊以檢視 Python 計時器timer.py的完整原始碼。

# 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()

可以自己使用程式碼,方法是將其儲存到一個名為的檔案中timer.py並將其匯入:

from timer import Timer

PyPI 上也提供了 Timer,因此更簡單的選擇是使用 pip 安裝它:

pip install codetiming

注意,PyPI 上的包名稱是codetiming,安裝包和匯入時都需要使用此名稱Timer

from codetiming import Timer

除了名稱和一些附加功能之外,codetiming.Timer 與 timer.Timer 完全一樣。總而言之,可以通過三種不同的方式使用 Timer

1. 作為一個

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 計時器主要用於監控程式碼在單個關鍵程式碼塊或函數上所花費的時間。

其他 Python 定時器函數

使用 Python 對程式碼進行計時有很多選擇。這裡我們學習瞭如何建立一個靈活方便的類,可以通過多種不同的方式使用該類。對 PyPI 的快速搜尋發現,已經有許多專案提供 Python 計時器解決方案。

在本節中,我們首先了解有關標準庫中用於測量時間的不同函數的更多資訊,包括為什麼 perf_counter() 更好,然後探索優化程式碼的替代方案。

使用替代 Python 計時器函數

在本文之前,包括前面介紹python定時器的文章中,我們一直在使用 perf_counter() 來進行實際的時間測量,但是 Python 的時間庫附帶了幾個其他也可以測量時間的函數。這裡有一些:

  • time()

  • perf_counter_ns()
  • monotonic()
  • process_time()

擁有多個函數的一個原因是 Python 將時間表示為浮點數。浮點數本質上是不準確的。之前可能已經看到過這樣的結果:

>>> 0.1 + 0.1 + 0.1
0.30000000000000004

>>> 0.1 + 0.1 + 0.1 == 0.3
False

Python 的 Float 遵循 IEEE 754 浮點算術標準,該標準以 64 位表示所有浮點數。因為浮點數有無限多位數,即不能用有限的位數來表達它們。

考慮time()這個函數的主要目的,是它表示的是現在的實際時間。它以自給定時間點(稱為紀元)以來的秒數來表示函數。time()返回的數位很大,這意味著可用的數位較少,因而解析度會受到影響。簡而言之, time()無法測量納秒級差異:

>>> import time
>>> t = time.time()
>>> t
1564342757.0654016

>>> t + 1e-9
1564342757.0654016

>>> t == t + 1e-9
True

一納秒是十億分之一秒。上面程式碼中,將納秒新增到引數 t ,他並不會影響結果。與 time() 不同的是,perf_counter() 使用一些未定義的時間點作為它的紀元,它可以使用更小的數位,從而獲得更好的解析度:

>>> import time
>>> p = time.perf_counter()
>>> p
11370.015653846

>>> p + 1e-9
11370.015653847

>>> p == p + 1e-9
False

眾所周知,將時間表示為浮點數是非常具有挑戰的一件事,因此 Python 3.7 引入了一個新選項:每個時間測量函數現在都有一個相應的 _ns 函數,它以 int 形式返回納秒數,而不是以浮點數形式返回秒數。例如,time() 現在有一個名為 time_ns() 的納秒對應項:

import time
time.time_ns()

1564342792866601283

整數在 Python 中是無界的,因此 time_ns() 可以為所有永恆提供納秒級解析度。同樣,perf_counter_ns() 是 perf_counter() 的納秒版本:

>>> import time
>>> time.perf_counter()
13580.153084446

>>> time.perf_counter_ns()
13580765666638

我們注意到,因為 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 計時器函數的更多資訊:

>>> 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)

注意,不同系統上的結果可能會有所不同。

PEP 418 描述了引入這些功能的一些基本原理。它包括以下簡短描述:

  • time.monotonic():  超時和排程,不受系統時鐘更新影響
  • time.perf_counter():基準測試,短期內最精確的時鐘
  • time.process_time():分析程序的CPU時間

估計執行時間timeit

在實際工作中,通常會想優化程式碼進一步提升程式碼效能,例如想知道將列表轉換為集合的最有效方法。下面我們使用函數 set() 和直接花括號定義集合 {...} 進行比較,看看這兩種方法哪個效能更優,此時需要使用 Python 計時器來比較兩者的執行速度。

>>> 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

該測試結果表明直接花括號定義集合可能會稍微快一些,但其實這些結果非常不確定。如果重新執行程式碼,可能會得到截然不同的結果。因為這會受計算機的效能和計算機執行狀態所影響:例如當計算機忙於其他任務時,就會影響我們程式的結果。

更好的方法是多次重複執行相同過程,並獲取平均耗時,就能夠更加精確地測量目標程式的效能大小。因此可以使用 timeit 標準庫,它旨在精確測量小程式碼片段的執行時間。雖然可以從 Python 匯入和呼叫 timeit.timeit() 作為常規函數,但使用命令列介面通常更方便。可以按如下方式對這兩種變體進行計時:

$ 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 自動多次呼叫程式碼以平均噪聲測量。timeit 的結果證實 {*nums} 量比 set(nums) 快。

注意:在下載檔案或存取資料庫的程式碼上使用 timeit 時要小心。由於 timeit 會自動多次呼叫程式,因此可能會無意中向伺服器傳送請求!

最後,IPython 互動式 shell 和 Jupyter Notebook 使用 %timeit 魔術命令對此功能提供了額外支援:

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)

同樣,測量結果表明直接花括號定義集合更快。在 Jupyter Notebooks 中,還可以使用 %%timeit cell-magic 來測量執行整個單元格的時間。

使用 Profiler 查詢程式碼中的Bottlenecks

timeit 非常適合對特定程式碼片段進行基準測試。但使用它來檢查程式的所有部分並找出哪些部分花費的時間最多會非常麻煩。此時我們想到可以使用分析器

cProfile 是一個分析器,可以隨時從標準庫中存取它。可以通過多種方式使用它,儘管將其用作命令列工具通常是最直接的:

$ python -m cProfile -o download_data.prof download_data.py

此命令在開啟分析器的情況下執行 download_data.py。將 cProfile 的輸出儲存在 download_data.prof 中,由 -o 選項指定。輸出資料是二進位制格式,需要專門的程式才能理解。同樣,Python 在標準庫中有一個選項 pstats!它可以在 .prof 檔案上執行 pstats 模組會開啟一個互動式組態檔統計瀏覽器。

$ python -m pstats download_data.prof
Welcome to the profile statistics browser.
download_data.prof% help

...

要使用 pstats,請在提示符下鍵入命令。通常你會使用 sort 和 stats 命令,strip 可以獲得更清晰的輸出:

download_data.prof% strip
download_data.prof% sort cumtime
download_data.prof% stats 10
...

此輸出顯示總執行時間為 0.586 秒。它還列出了程式碼花費最多時間的十個函數。這裡按累積時間 ( cumtime) 排序,這意味著當給定函數呼叫另一個函數時,程式碼會計算時間。

總時間 ( tottime) 列表示程式碼在函數中花費了多少時間,不包括在子函數中的時間。要查詢程式碼花費最多時間的位置,需要發出另一個sort命令:

download_data.prof% sort tottime
download_data.prof% stats 10
...

可以使用 pstats瞭解程式碼大部分時間花在哪裡,然後嘗試優化我們發現的任何瓶頸。還可以使用該工具更好地理解程式碼的結構。例如,被呼叫者和呼叫者命令將顯示給定函數呼叫和呼叫的函數。

還可以研究某些函數。通過使用短語 timer 過濾結果來檢查 Timer 導致的開銷:

download_data.prof% stats timer
...

完成調查後,使用 quit 離開 pstats 瀏覽器。

如需更加深入瞭解更強大的組態檔資料介面,可以檢視 KCacheGrind[8]。它使用自己的資料格式,也可以使用 pyprof2calltree 從 cProfile 轉換資料:

$ pyprof2calltree -k -i download_data.prof

該命令將轉換 download_data.prof 並開啟 KCacheGrind 來分析資料。

這裡為程式碼計時的最後一個選項是 line_profiler。cProfile 可以告訴我們程式碼在哪些函數中花費的時間最多,但它不會深入顯示該函數中的哪些行最慢,此時就需要 line_profiler 。

注意:還可以分析程式碼的記憶體消耗。這超出了本教學的範圍,如果你需要監控程式的記憶體消耗,可以檢視 memory-profiler。

行分析需要時間,並且會為我們的執行時增加相當多的開銷。正常的工作流程是首先使用 cProfile 來確定要調查的函數,然後在這些函數上執行 line_profilerline_profiler 不是標準庫的一部分,因此應該首先按照安裝說明進行設定。

在執行分析器之前,需要告訴它要分析哪些函數。可以通過在原始碼中新增 @profile 裝飾器來實現。例如,要分析 Timer.stop(),在 timer.py 中新增以下內容:

@profile
def stop(self) -> float:
    # 其餘部分不變

注意,不需要匯入profile組態檔,它會在執行分析器時自動新增到全域性名稱空間中。不過,我們需要在完成分析後刪除該行。否則,會丟擲一個 NameError 異常。

接下來,使用 kernprof 執行分析器,它是 line_profiler 包的一部分:

$ kernprof -l download_data.py

此命令自動將探查器資料儲存在名為 download_data.py.lprof 的檔案中。可以使用 line_profiler 檢視這些結果:

$ 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
=====================================
...

首先,注意本報告中的時間單位是微秒(1e-06 s)。通常,最容易檢視的數位是 %Time,它告訴我們程式碼在每一行的函數中花費的總時間的百分比。

總結

在本文中,我們嘗試了幾種不同的方法來將 Python 計時器新增到程式碼中:

  • 使用了一個來保持狀態並新增一個使用者友好的介面。類非常靈活,直接使用 Timer 可以讓您完全控制如何以及何時呼叫計時器。
  • 使用上下文管理器向程式碼塊新增功能,並在必要時進行清理。上下文管理器使用起來很簡單,使用 with Timer() 新增可以幫助您在視覺上更清楚地區分您的程式碼。
  • 使用裝飾器向函數新增行為。裝飾器簡潔而引人注目,使用 @Timer() 是監控程式碼執行時的快速方法。

我們還了解了為什麼在對程式碼進行基準測試時應該更喜歡time.perf_counter()而不是 time.time(),以及在優化程式碼時還有哪些其他有用的替代方法。

現在我們可以在自己的程式碼中新增Python計時器函數了!在紀錄檔中跟蹤程式的執行速度將有助於監視指令碼。對於類、上下文管理器和裝飾器一起工作的其他用例

以上就是詳解利用裝飾器擴充套件Python計時器的詳細內容,更多關於Python裝飾器 計時器的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com