溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

怎么利用裝飾器擴展Python計時器

發(fā)布時間:2022-07-01 10:18:11 來源:億速云 閱讀:142 作者:iii 欄目:開發(fā)技術

這篇文章主要介紹“怎么利用裝飾器擴展Python計時器”,在日常操作中,相信很多人在怎么利用裝飾器擴展Python計時器問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”怎么利用裝飾器擴展Python計時器”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!

    介紹

    假設我們需要跟蹤代碼庫中一個給定函數(shù)所花費的時間。使用上下文管理器,基本上有兩種不同的選擇:

    1. 每次調(diào)用函數(shù)時使用 Timer:

    with Timer("some_name"):
        do_something()

    當我們在一個py文件里多次調(diào)用函數(shù) do_something(),那么這將會變得非常繁瑣并且難以維護。

    2. 將代碼包裝在上下文管理器中的函數(shù)中:

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

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

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

    理解 Python 中的裝飾器

    裝飾器是包裝另一個函數(shù)以修改其行為的函數(shù)。你可能會有疑問,這怎么實現(xiàn)呢?其實函數(shù)是 Python 中的first-class 對象,換句話說,函數(shù)可以以變量的形式傳遞給其他函數(shù)的參數(shù),就像任何其他常規(guī)對象一樣。因此此處有較大的靈活性,也是 Python 幾個最強大功能的基礎。

    我們首先創(chuàng)建第一個示例,一個什么都不做的裝飾器:

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

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

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

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

    要定義更多豐富的裝飾器,需要了解內(nèi)部函數(shù)。內(nèi)部函數(shù)是在另一個函數(shù)內(nèi)部定義的函數(shù),它的一種常見用途是創(chuàng)建函數(shù)工廠:

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

    multiplier() 是一個內(nèi)部函數(shù),在 create_multiplier() 內(nèi)部定義。注意可以訪問 multiplier() 內(nèi)部的因子,而 multiplier()未在 create_multiplier() 外部定義:

    multiplier

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

    相反,可以使用create_multiplier()創(chuàng)建新的 multiplier 函數(shù),每個函數(shù)都基于不同的參數(shù)factor:

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

    6

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

    28 

    同樣,可以使用內(nèi)部函數(shù)來創(chuàng)建裝飾器。裝飾器是一個返回函數(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() 是一個裝飾器,因為它是一個期望函數(shù) func() 作為其唯一參數(shù)并返回另一個函數(shù) wrapper_triple() 的函數(shù)。注意 triple() 本身的結構:

    • 第 1 行開始了 triple() 的定義,并期望一個函數(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ù)和關鍵字參數(shù)。我們可以靈活地在任何函數(shù)上使用 triple()。

    • 第 3 行打印出修飾函數(shù)的名稱,并指出已對其應用了 triple()。

    • 第 4 行調(diào)用 func(),triple() 修飾的函數(shù)。它傳遞傳遞給 wrapper_triple() 的所有參數(shù)。

    • 第 5 行將 func() 的返回值增加三倍并將其返回。

    接下來的代碼中,knock() 是一個返回單詞 Penny 的函數(shù),將其傳給triple() 函數(shù),并看看輸出結果是什么。

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

    我們都知道,文本字符串與數(shù)字相乘,是字符串的一種重復形式,因此字符串 'Penny' 重復了 3 次??梢哉J為,裝飾發(fā)生在knock = triple(knock)。

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

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

    @ 符號用于應用裝飾器,@triple 表示 triple() 應用于緊隨其后定義的函數(shù)。

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

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

    @triple 裝飾了 knock(),然后被 wrapper_triple() 內(nèi)部函數(shù)替換,被裝飾的函數(shù)的名字會變成裝飾器函數(shù),除了名稱,還有文檔字符串和其他元數(shù)據(jù)都將會被替換。但有時,我們并不總是想將被修飾的函數(shù)的所有信息都被修改了。此時 @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 的這個新定義保留元數(shù)據(jù):

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

    注意knock() 即使在被裝飾之后,也同樣保留了它的原有函數(shù)名稱。當定義裝飾器時,使用 @functools.wraps 是一種不錯的選擇,可以為大多數(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

    創(chuàng)建 Python 定時器裝飾器

    在本節(jié)中,云朵君將和大家一起學習如何擴展 Python 計時器,并以裝飾器的形式使用它。接下來我們從頭開始創(chuàng)建 Python 計時器裝飾器。

    根據(jù)上面的模板,我們只需要決定在調(diào)用裝飾函數(shù)之前和之后要做什么。這與進入和退出上下文管理器時的注意事項類似。在調(diào)用修飾函數(shù)之前啟動 Python 計時器,并在調(diào)用完成后停止 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

    回想一下,還可以將裝飾器應用于先前定義的下載數(shù)據(jù)的函數(shù):

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

    使用裝飾器的一個優(yōu)點是只需要應用一次,并且每次都會對函數(shù)計時:

    data = requests.get(0)

    Elapsed time: 0.5512 seconds

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

    現(xiàn)在我們似乎已經(jīng)將裝飾器用作應用于其他函數(shù)的函數(shù),但其實不然,因為裝飾器必須是可調(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 是一個可調(diào)用的實例,可以對數(shù)字求平方,就像square()第一個示例中的函數(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 計時器變成裝飾器。其實上下文管理器和裝飾器之間的一些相似之處:它們通常都用于在執(zhí)行某些給定代碼之前和之后執(zhí)行某些操作

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

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

    當以這種方式使用 ContextDecorator 時,無需自己實現(xiàn) .__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()

    我們與之前的寫法進行比較,唯一的區(qū)別是第 3 行的 Timer 的導入和第 4 行的 @Timer()  的應用。使用裝飾器的一個顯著優(yōu)勢是它們通常很容易調(diào)用。

    但是,裝飾器仍然適用于整個函數(shù)。這意味著代碼除了記錄了下載數(shù)據(jù)所需的時間外,還考慮了保存數(shù)據(jù)所需的時間。運行腳本:

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

    [ ... ]
    Elapsed time: 0.69 seconds

    從上面打印出來的結果可以看到,代碼記錄了下載數(shù)據(jù)和保持數(shù)據(jù)一共所需的時間。

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

    • 省時省力: 只需要一行額外的代碼即可為函數(shù)的執(zhí)行計時。

    • 可讀性: 當添加裝飾器時,可以更清楚地注意到代碼會對函數(shù)計時。

    • 一致性: 只需要在定義函數(shù)時添加裝飾器即可。每次調(diào)用時,代碼都會始終如一地計時。

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

    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 計時器主要用于監(jiān)控代碼在單個關鍵代碼塊或函數(shù)上所花費的時間。

    其他 Python 定時器函數(shù)

    使用 Python 對代碼進行計時有很多選擇。這里我們學習了如何創(chuàng)建一個靈活方便的類,可以通過多種不同的方式使用該類。對 PyPI 的快速搜索發(fā)現(xiàn),已經(jīng)有許多項目提供 Python 計時器解決方案。

    在本節(jié)中,我們首先了解有關標準庫中用于測量時間的不同函數(shù)的更多信息,包括為什么 perf_counter() 更好,然后探索優(yōu)化代碼的替代方案。

    使用替代 Python 計時器函數(shù)

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

    • time()

    • perf_counter_ns()

    • monotonic()

    • process_time()

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

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

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

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

    >>> import time
    >>> t = time.time()
    >>> t
    1564342757.0654016
    
    >>> t + 1e-9
    1564342757.0654016
    
    >>> t == t + 1e-9
    True

    一納秒是十億分之一秒。上面代碼中,將納秒添加到參數(shù) t ,他并不會影響結果。與 time() 不同的是,perf_counter() 使用一些未定義的時間點作為它的紀元,它可以使用更小的數(shù)字,從而獲得更好的分辨率:

    >>> import time
    >>> p = time.perf_counter()
    >>> p
    11370.015653846
    
    >>> p + 1e-9
    11370.015653847
    
    >>> p == p + 1e-9
    False

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

    import time
    time.time_ns()

    1564342792866601283

    整數(shù)在 Python 中是無界的,因此 time_ns() 可以為所有永恒提供納秒級分辨率。同樣,perf_counter_ns() 是 perf_counter() 的納秒版本:

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

    我們注意到,因為 perf_counter() 已經(jīng)提供納秒級分辨率,所以使用 perf_counter_ns() 的優(yōu)勢較少。

    注意: perf_counter_ns() 僅在 Python 3.7 及更高版本中可用。在 Timer 類中使用了 perf_counter()。這樣,也可以在較舊的 Python 版本上使用 Timer。

    有兩個函數(shù)time不測量time.sleep時間:process_time()thread_time()。通常希望Timer能夠測量代碼所花費的全部時間,因此這兩個函數(shù)并不常用。而函數(shù) monotonic(),顧名思義,它是一個單調(diào)計時器,一個永遠不會向后移動的 Python 計時器。

    除了 time() 之外,所有這些函數(shù)都是單調(diào)的,如果調(diào)整了系統(tǒng)時間,它也隨之倒退。在某些系統(tǒng)上,monotonic() 與 perf_counter() 的功能相同,可以互換使用。我們可以使用 time.get_clock_info() 獲取有關 Python 計時器函數(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)上的結果可能會有所不同。

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

    • time.monotonic():  超時和調(diào)度,不受系統(tǒng)時鐘更新影響

    • time.perf_counter():基準測試,短期內(nèi)最精確的時鐘

    • time.process_time():分析進程的CPU時間

    估計運行時間timeit

    在實際工作中,通常會想優(yōu)化代碼進一步提升代碼性能,例如想知道將列表轉(zhuǎn)換為集合的最有效方法。下面我們使用函數(shù) set() 和直接花括號定義集合 {...} 進行比較,看看這兩種方法哪個性能更優(yōu),此時需要使用 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

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

    更好的方法是多次重復運行相同過程,并獲取平均耗時,就能夠更加精確地測量目標程序的性能大小。因此可以使用 timeit 標準庫,它旨在精確測量小代碼片段的執(zhí)行時間。雖然可以從 Python 導入和調(diào)用 timeit.timeit() 作為常規(guī)函數(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 自動多次調(diào)用代碼以平均噪聲測量。timeit 的結果證實 {*nums} 量比 set(nums) 快。

    注意:在下載文件或訪問數(shù)據(jù)庫的代碼上使用 timeit 時要小心。由于 timeit 會自動多次調(diào)用程序,因此可能會無意中向服務器發(fā)送請求!

    最后,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 選項指定。輸出數(shù)據(jù)是二進制格式,需要專門的程序才能理解。同樣,Python 在標準庫中有一個選項 pstats!它可以在 .prof 文件上運行 pstats 模塊會打開一個交互式配置文件統(tǒng)計瀏覽器。

    $ 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 秒。它還列出了代碼花費最多時間的十個函數(shù)。這里按累積時間 ( cumtime) 排序,這意味著當給定函數(shù)調(diào)用另一個函數(shù)時,代碼會計算時間。

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

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

    可以使用 pstats了解代碼大部分時間花在哪里,然后嘗試優(yōu)化我們發(fā)現(xiàn)的任何瓶頸。還可以使用該工具更好地理解代碼的結構。例如,被調(diào)用者和調(diào)用者命令將顯示給定函數(shù)調(diào)用和調(diào)用的函數(shù)。

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

    download_data.prof% stats timer
    ...

    完成調(diào)查后,使用 quit 離開 pstats 瀏覽器。

    如需更加深入了解更強大的配置文件數(shù)據(jù)接口,可以查看 KCacheGrind[8]。它使用自己的數(shù)據(jù)格式,也可以使用 pyprof2calltree 從 cProfile 轉(zhuǎn)換數(shù)據(jù):

    $ pyprof2calltree -k -i download_data.prof

    該命令將轉(zhuǎn)換 download_data.prof 并打開 KCacheGrind 來分析數(shù)據(jù)。

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

    注意:還可以分析代碼的內(nèi)存消耗。這超出了本教程的范圍,如果你需要監(jiān)控程序的內(nèi)存消耗,可以查看 memory-profiler。

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

    在運行分析器之前,需要告訴它要分析哪些函數(shù)??梢酝ㄟ^在源代碼中添加 @profile 裝飾器來實現(xiàn)。例如,要分析 Timer.stop(),在 timer.py 中添加以下內(nèi)容:

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

    注意,不需要導入profile配置文件,它會在運行分析器時自動添加到全局命名空間中。不過,我們需要在完成分析后刪除該行。否則,會拋出一個 NameError 異常。

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

    $ kernprof -l download_data.py

    此命令自動將探查器數(shù)據(jù)保存在名為 download_data.py.lprof 的文件中??梢允褂?nbsp;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)。通常,最容易查看的數(shù)字是 %Time,它告訴我們代碼在每一行的函數(shù)中花費的總時間的百分比。

    到此,關于“怎么利用裝飾器擴展Python計時器”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續(xù)學習更多相關知識,請繼續(xù)關注億速云網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬嵱玫奈恼拢?/p>

    向AI問一下細節(jié)

    免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。

    AI