首頁 > 後端開發 > Python教學 > 手把手教你用裝飾器擴充 Python 計時器

手把手教你用裝飾器擴充 Python 計時器

王林
發布: 2023-04-13 20:46:01
轉載
1762 人瀏覽過

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

手把手教你用裝飾器擴充 Python 計時器

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

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 seconds
登入後複製

回想一下,还可以将装饰器应用于先前定义的下载数据的函数:

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 定时器函数,如果你对其不太感兴趣,可以直接跳到最后。

其他 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 浮点算术标准[5],该标准以 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[9] 从 cProfile 转换数据:

$ pyprof2calltree -k -i download_data.prof
登入後複製

该命令将转换 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
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 計時器的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:51cto.com
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板