欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

詳解利用裝飾器擴(kuò)展Python計(jì)時(shí)器

 更新時(shí)間:2022年06月30日 14:41:32   作者:云朵君  
在本文中,云朵君將和大家一起了解裝飾器的工作原理,如何將我們之前定義的定時(shí)器類?Timer?擴(kuò)展為裝飾器,以及如何簡(jiǎn)化計(jì)時(shí)功能,感興趣的可以了解一下

介紹

在本文中,云朵君將和大家一起了解裝飾器的工作原理,如何將我們之前定義的定時(shí)器類 Timer 擴(kuò)展為裝飾器,以及如何簡(jiǎn)化計(jì)時(shí)功能。最后對(duì) Python 定時(shí)器系列文章做個(gè)小結(jié)。

這是我們手把手教你實(shí)現(xiàn) Python 定時(shí)器的第三篇文章。前兩篇:分別是?手把手教你實(shí)現(xiàn)一個(gè) Python 計(jì)時(shí)器,和用上下文管理器擴(kuò)展 Python 計(jì)時(shí)器?,使得我們的 Timer 類方便用、美觀實(shí)用。

但我們并不滿足于此,仍然有一個(gè)用例可以進(jìn)一步簡(jiǎn)化它。假設(shè)我們需要跟蹤代碼庫(kù)中一個(gè)給定函數(shù)所花費(fèi)的時(shí)間。使用上下文管理器,基本上有兩種不同的選擇:

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

with?Timer("some_name"):
????do_something()

當(dāng)我們?cè)谝粋€(gè)py文件里多次調(diào)用函數(shù) do_something(),那么這將會(huì)變得非常繁瑣并且難以維護(hù)。

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

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

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

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

理解 Python 中的裝飾器

裝飾器是包裝另一個(gè)函數(shù)以修改其行為的函數(shù)。你可能會(huì)有疑問(wèn),這怎么實(shí)現(xiàn)呢?其實(shí)函數(shù)是 Python 中的first-class 對(duì)象,換句話說(shuō),函數(shù)可以以變量的形式傳遞給其他函數(shù)的參數(shù),就像任何其他常規(guī)對(duì)象一樣。因此此處有較大的靈活性,也是 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()來(lái)修改其他函數(shù),例如:

>>>?print("Hello")
Hello

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

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

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

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

multiplier() 是一個(gè)內(nèi)部函數(shù),在 create_multiplier() 內(nèi)部定義。注意可以訪問(wèn) 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ù),每個(gè)函數(shù)都基于不同的參數(shù)factor:

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

6

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

28 

同樣,可以使用內(nèi)部函數(shù)來(lái)創(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 行開(kāi)始了 triple() 的定義,并期望一個(gè)函數(shù)作為參數(shù)。
  • 第 2 到 5 行定義了內(nèi)部函數(shù) wrapper_triple()
  • 第 6 行返回 wrapper_triple()。

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

  • 第 2 行開(kāi)始 wrapper_triple() 的定義。此函數(shù)將替換 triple() 修飾的任何函數(shù)。參數(shù)是 *args 和 **kwargs,用于收集傳遞給函數(shù)的任何位置參數(shù)和關(guān)鍵字參數(shù)。我們可以靈活地在任何函數(shù)上使用 triple()。
  • 第 3 行打印出修飾函數(shù)的名稱,并指出已對(duì)其應(yīng)用了 triple()。
  • 第 4 行調(diào)用 func(),triple() 修飾的函數(shù)。它傳遞傳遞給 wrapper_triple() 的所有參數(shù)。
  • 第 5 行將 func() 的返回值增加三倍并將其返回。

接下來(lái)的代碼中,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 次??梢哉J(rèn)為,裝飾發(fā)生在knock = triple(knock)。

上述方法雖然實(shí)現(xiàn)了裝飾器的功能,但似乎有點(diǎn)笨拙。PEP 318 引入了一種更方便的語(yǔ)法來(lái)應(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)庫(kù)中定義的裝飾器方法之一是:@functools.wraps。這在定義你自己的裝飾器時(shí)非常有用。前面說(shuō)過(guò),裝飾器是用另一個(gè)函數(shù)替換了一個(gè)函數(shù),會(huì)給你的函數(shù)帶來(lái)一個(gè)微妙的變化:

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

@triple 裝飾了 knock(),然后被 wrapper_triple() 內(nèi)部函數(shù)替換,被裝飾的函數(shù)的名字會(huì)變成裝飾器函數(shù),除了名稱,還有文檔字符串和其他元數(shù)據(jù)都將會(huì)被替換。但有時(shí),我們并不總是想將被修飾的函數(shù)的所有信息都被修改了。此時(shí) @functools.wraps 正好解決了這個(gè)問(wèn)題,如下所示:

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
<function knock at 0x7fa3bfe5df28>

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

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

在本節(jié)中,云朵君將和大家一起學(xué)習(xí)如何擴(kuò)展 Python 計(jì)時(shí)器,并以裝飾器的形式使用它。接下來(lái)我們從頭開(kāi)始創(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í)器。可以按如下方式定義 @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 second

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

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

使用裝飾器的一個(gè)優(yōu)點(diǎn)是只需要應(yīng)用一次,并且每次都會(huì)對(duì)函數(shù)計(jì)時(shí):

data?=?requests.get(0)

Elapsed time: 0.5512 seconds

雖然@timer 順利完成了對(duì)目標(biāo)函數(shù)的定時(shí)。但從某種意義上說(shuō),你又回到了原點(diǎn),因?yàn)樵撗b飾器 @timer 失去了前面定義的類 Timer 的靈活性或便利性。換句話說(shuō),我們需要將 Timer 類表現(xiàn)得像一個(gè)裝飾器。

現(xiàn)在我們似乎已經(jīng)將裝飾器用作應(yīng)用于其他函數(shù)的函數(shù),但其實(shí)不然,因?yàn)檠b飾器必須是可調(diào)用的。Python中有許多可調(diào)用的類型,可以通過(guò)在其類中定義特殊的.__call__()方法來(lái)使自己的對(duì)象可調(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í)例,可以對(duì)數(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 ,給我們帶來(lái)了不少便利。而這里使用的裝飾器,似乎更加方便。

@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)庫(kù)中定義了一個(gè)名為 ContextDecorator 的 mixin 類,它可以簡(jiǎn)單地通過(guò)繼承 ContextDecorator 來(lái)為上下文管理器類添加裝飾器函數(shù)。

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

當(dāng)以這種方式使用 ContextDecorator 時(shí),無(wú)需自己實(shí)現(xiàn) .__call__(),因此我們可以大膽地將其從 Timer 類中刪除。

使用 Python 定時(shí)器裝飾器

接下來(lái),再最后一次重改 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()

我們與之前的寫(xiě)法進(jìn)行比較,唯一的區(qū)別是第 3 行的 Timer 的導(dǎo)入和第 4 行的 @Timer()  的應(yīng)用。使用裝飾器的一個(gè)顯著優(yōu)勢(shì)是它們通常很容易調(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

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

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

  • 省時(shí)省力: 只需要一行額外的代碼即可為函數(shù)的執(zhí)行計(jì)時(shí)。
  • 可讀性: 當(dāng)添加裝飾器時(shí),可以更清楚地注意到代碼會(huì)對(duì)函數(shù)計(jì)時(shí)。
  • 一致性: 只需要在定義函數(shù)時(shí)添加裝飾器即可。每次調(diào)用時(shí),代碼都會(huì)始終如一地計(jì)時(shí)。

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

Python 計(jì)時(shí)器代碼

這里展開(kāi)下面的代碼塊以查看 Python 計(jì)時(shí)器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()

可以自己使用代碼,方法是將其保存到一個(gè)名為的文件中timer.py并將其導(dǎo)入:

from?timer?import?Timer

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

pip?install?codetiming

注意,PyPI 上的包名稱是codetiming,安裝包和導(dǎo)入時(shí)都需要使用此名稱Timer

from?codetiming?import?Timer

除了名稱和一些附加功能之外,codetiming.Timer 與 timer.Timer 完全一樣??偠灾梢酝ㄟ^(guò)三種不同的方式使用 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í)器函數(shù)

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

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

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

在本文之前,包括前面介紹python定時(shí)器的文章中,我們一直在使用 perf_counter() 來(lái)進(jìn)行實(shí)際的時(shí)間測(cè)量,但是 Python 的時(shí)間庫(kù)附帶了幾個(gè)其他也可以測(cè)量時(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)看到過(guò)這樣的結(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),該標(biāo)準(zhǔn)以 64 位表示所有浮點(diǎn)數(shù)。因?yàn)楦↑c(diǎn)數(shù)有無(wú)限多位數(shù),即不能用有限的位數(shù)來(lái)表達(dá)它們。

考慮time()這個(gè)函數(shù)的主要目的,是它表示的是現(xiàn)在的實(shí)際時(shí)間。它以自給定時(shí)間點(diǎn)(稱為紀(jì)元)以來(lái)的秒數(shù)來(lái)表示函數(shù)。time()返回的數(shù)字很大,這意味著可用的數(shù)字較少,因而分辨率會(huì)受到影響。簡(jiǎn)而言之, time()無(wú)法測(cè)量納秒級(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í)間測(cè)量函數(shù)現(xiàn)在都有一個(gè)相應(yīng)的 _ns 函數(shù),它以 int 形式返回納秒數(shù),而不是以浮點(diǎn)數(shù)形式返回秒數(shù)。例如,time() 現(xiàn)在有一個(gè)名為 time_ns() 的納秒對(duì)應(yīng)項(xiàng):

import?time
time.time_ns()

1564342792866601283

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

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

>>>?time.perf_counter_ns()
13580765666638

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

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

有兩個(gè)函數(shù)time不測(cè)量time.sleep時(shí)間:process_time()thread_time()。通常希望Timer能夠測(cè)量代碼所花費(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 描述了引入這些功能的一些基本原理。它包括以下簡(jiǎn)短描述:

  • time.monotonic():  超時(shí)和調(diào)度,不受系統(tǒng)時(shí)鐘更新影響
  • time.perf_counter():基準(zhǔn)測(cè)試,短期內(nèi)最精確的時(shí)鐘
  • time.process_time():分析進(jìn)程的CPU時(shí)間

估計(jì)運(yùn)行時(shí)間timeit

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

該測(cè)試結(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)行相同過(guò)程,并獲取平均耗時(shí),就能夠更加精確地測(cè)量目標(biāo)程序的性能大小。因此可以使用 timeit 標(biāo)準(zhǔn)庫(kù),它旨在精確測(cè)量小代碼片段的執(zhí)行時(shí)間。雖然可以從 Python 導(dǎo)入和調(diào)用 timeit.timeit() 作為常規(guī)函數(shù),但使用命令行界面通常更方便。可以按如下方式對(duì)這兩種變體進(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)用代碼以平均噪聲測(cè)量。timeit 的結(jié)果證實(shí) {*nums} 量比 set(nums) 快。

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

最后,IPython 交互式 shell 和 Jupyter Notebook 使用 %timeit 魔術(shù)命令對(duì)此功能提供了額外支持:

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)

同樣,測(cè)量結(jié)果表明直接花括號(hào)定義集合更快。在 Jupyter Notebooks 中,還可以使用 %%timeit cell-magic 來(lái)測(cè)量運(yùn)行整個(gè)單元格的時(shí)間。

使用 Profiler 查找代碼中的Bottlenecks

timeit 非常適合對(duì)特定代碼片段進(jìn)行基準(zhǔn)測(cè)試。但使用它來(lái)檢查程序的所有部分并找出哪些部分花費(fèi)的時(shí)間最多會(huì)非常麻煩。此時(shí)我們想到可以使用分析器。

cProfile 是一個(gè)分析器,可以隨時(shí)從標(biāo)準(zhǔn)庫(kù)中訪問(wèn)它??梢酝ㄟ^(guò)多種方式使用它,盡管將其用作命令行工具通常是最直接的:

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

此命令在打開(kāi)分析器的情況下運(yùn)行 download_data.py。將 cProfile 的輸出保存在 download_data.prof 中,由 -o 選項(xiàng)指定。輸出數(shù)據(jù)是二進(jìn)制格式,需要專門的程序才能理解。同樣,Python 在標(biāo)準(zhǔn)庫(kù)中有一個(gè)選項(xiàng) pstats!它可以在 .prof 文件上運(yùn)行 pstats 模塊會(huì)打開(kāi)一個(gè)交互式配置文件統(tǒng)計(jì)瀏覽器。

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

...

要使用 pstats,請(qǐng)?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ù)。通過(guò)使用短語(yǔ) timer 過(guò)濾結(jié)果來(lái)檢查 Timer 導(dǎo)致的開(kāi)銷:

download_data.prof%?stats?timer
...

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

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

$?pyprof2calltree?-k?-i?download_data.prof

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

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

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

行分析需要時(shí)間,并且會(huì)為我們的運(yùn)行時(shí)增加相當(dāng)多的開(kāi)銷。正常的工作流程是首先使用 cProfile 來(lái)確定要調(diào)查的函數(shù),然后在這些函數(shù)上運(yùn)行 line_profiler。line_profiler 不是標(biāo)準(zhǔn)庫(kù)的一部分,因此應(yīng)該首先按照安裝說(shuō)明進(jìn)行設(shè)置。

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

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

注意,不需要導(dǎo)入profile配置文件,它會(huì)在運(yùn)行分析器時(shí)自動(dòng)添加到全局命名空間中。不過(guò),我們需要在完成分析后刪除該行。否則,會(huì)拋出一個(gè) NameError 異常。

接下來(lái),使用 kernprof 運(yùn)行分析器,它是 line_profiler 包的一部分:

$?kernprof?-l?download_data.py

此命令自動(dòng)將探查器數(shù)據(jù)保存在名為 download_data.py.lprof 的文件中??梢允褂?nbsp;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í)間的百分比。

總結(jié)

在本文中,我們嘗試了幾種不同的方法來(lái)將 Python 計(jì)時(shí)器添加到代碼中:

  • 使用了一個(gè)來(lái)保持狀態(tài)并添加一個(gè)用戶友好的界面。類非常靈活,直接使用 Timer 可以讓您完全控制如何以及何時(shí)調(diào)用計(jì)時(shí)器。
  • 使用上下文管理器向代碼塊添加功能,并在必要時(shí)進(jìn)行清理。上下文管理器使用起來(lái)很簡(jiǎn)單,使用 with Timer() 添加可以幫助您在視覺(jué)上更清楚地區(qū)分您的代碼。
  • 使用裝飾器向函數(shù)添加行為。裝飾器簡(jiǎn)潔而引人注目,使用 @Timer() 是監(jiān)控代碼運(yùn)行時(shí)的快速方法。

我們還了解了為什么在對(duì)代碼進(jìn)行基準(zhǔn)測(cè)試時(shí)應(yīng)該更喜歡time.perf_counter()而不是 time.time(),以及在優(yōu)化代碼時(shí)還有哪些其他有用的替代方法。

現(xiàn)在我們可以在自己的代碼中添加Python計(jì)時(shí)器函數(shù)了!在日志中跟蹤程序的運(yùn)行速度將有助于監(jiān)視腳本。對(duì)于類、上下文管理器和裝飾器一起工作的其他用例

以上就是詳解利用裝飾器擴(kuò)展Python計(jì)時(shí)器的詳細(xì)內(nèi)容,更多關(guān)于Python裝飾器 計(jì)時(shí)器的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Django框架實(shí)現(xiàn)逆向解析url的方法

    Django框架實(shí)現(xiàn)逆向解析url的方法

    這篇文章主要介紹了Django框架實(shí)現(xiàn)逆向解析url的方法,結(jié)合實(shí)例形式分析了Django逆向解析URL的原理、步驟、相關(guān)實(shí)現(xiàn)技巧與注意事項(xiàng),需要的朋友可以參考下
    2018-07-07
  • Python?Pandas中append方法舉例詳解

    Python?Pandas中append方法舉例詳解

    這篇文章主要給大家介紹了關(guān)于Python?Pandas中append方法的相關(guān)資料,在Pandas中append()方法用于將一個(gè)或多個(gè)DataFrame或Series添加到DataFrame中,需要的朋友可以參考下
    2023-12-12
  • python 列表,數(shù)組,矩陣兩兩轉(zhuǎn)換tolist()的實(shí)例

    python 列表,數(shù)組,矩陣兩兩轉(zhuǎn)換tolist()的實(shí)例

    下面小編就為大家分享一篇python 列表,數(shù)組,矩陣兩兩轉(zhuǎn)換tolist()的實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧
    2018-04-04
  • pycharm激活方法到2099年(激活流程)

    pycharm激活方法到2099年(激活流程)

    這篇文章主要介紹了pycharm激活方法到2099年,文末給大家提到了idea和pycharm最新版激活方法,非常不錯(cuò)對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2020-09-09
  • 跟老齊學(xué)Python之集成開(kāi)發(fā)環(huán)境(IDE)

    跟老齊學(xué)Python之集成開(kāi)發(fā)環(huán)境(IDE)

    IDE的全稱是:Integrated Development Environment,簡(jiǎn)稱IDE,也稱為Integration Design Environment、Integration Debugging Environment,翻譯成中文叫做“集成開(kāi)發(fā)環(huán)境”,在臺(tái)灣那邊叫做“整合開(kāi)發(fā)環(huán)境”。
    2014-09-09
  • Python3讀取文件的操作詳解

    Python3讀取文件的操作詳解

    說(shuō)到fileinput,可能90%的碼農(nóng)表示沒(méi)用過(guò),甚至沒(méi)有聽(tīng)說(shuō)過(guò)。但是,今天小編還是要介紹fileinput這個(gè)方法,因?yàn)樘嗡沽耍旄S小編一起學(xué)習(xí)學(xué)習(xí)吧
    2022-07-07
  • Python基于Pytorch的特征圖提取實(shí)例

    Python基于Pytorch的特征圖提取實(shí)例

    在利用深度學(xué)習(xí)進(jìn)行分類時(shí),有時(shí)需要對(duì)中間的特征圖進(jìn)行提取操作,下面這篇文章主要給大家介紹了關(guān)于Python基于Pytorch的特征圖提取的相關(guān)資料,需要的朋友可以參考下
    2022-03-03
  • django 開(kāi)發(fā)忘記密碼通過(guò)郵箱找回功能示例

    django 開(kāi)發(fā)忘記密碼通過(guò)郵箱找回功能示例

    這篇文章主要介紹了django 開(kāi)發(fā)忘記密碼通過(guò)郵箱找回功能示例,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2018-04-04
  • Python可視化Matplotlib散點(diǎn)圖scatter()用法詳解

    Python可視化Matplotlib散點(diǎn)圖scatter()用法詳解

    這篇文章主要介紹了Python可視化中Matplotlib散點(diǎn)圖scatter()的用法詳解,文中附含詳細(xì)示例代碼,有需要得朋友可以借鑒參考下,希望能夠有所幫助
    2021-09-09
  • Python 高級(jí)變量之字典和字符串詳解

    Python 高級(jí)變量之字典和字符串詳解

    這篇文章主要為大家介紹了Python 字典和字符串,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來(lái)幫助
    2021-12-12

最新評(píng)論