python timeout 装饰器,python装饰器计算函数运行时间

  python timeout 装饰器,python装饰器计算函数运行时间

  在本文中,云云君将与大家分享装饰器的工作原理,如何将我们之前定义的timer类Timer扩展到装饰器中,以及如何简化计时功能。有兴趣的可以看看。

  00-1010简介了解Python中的装饰器使用Python计时器创建Python计时器装饰器Python计时器代码其他Python计时器函数使用替代Python计时器函数估计运行时间timeit使用探查器查找瓶颈代码摘要

  

目录

 

  在本文中,云云君将与大家分享decorator的工作原理,如何将我们之前定义的Timer类扩展到decorator,以及如何简化计时函数。最后对Python定时器系列文章做个总结。

  这是我们教你如何实现Python定时器的第三篇文章。前两部分:教你手工实现一个Python定时器,使用上下文管理器扩展Python定时器,使得我们的定时器类简单易用,美观实用。

  但我们对此并不满意。仍然有一个用例可以进一步简化它。假设我们需要跟踪代码库中给定函数所花费的时间。使用上下文管理器,基本上有两种不同的选项:

  1.每次调用函数时使用Timer:

  withTimer(某个名称):

  做某事()

  当我们在一个py文件中多次调用函数do_something()时,会变得非常繁琐,难以维护。

  2.将代码包装在上下文管理器中的函数中:

  defdo_something():

  withTimer(某个名称):

  .

  Timer只需要加在一个地方,但是这样会给do_something()的整个定义增加一个缩进层次。

  一个更好的解决方案是使用Timer 作为装饰器.装饰器,它们是修改函数和类行为的强大构造。

  

介绍

 

  装饰器是一个包装另一个函数来修改其行为的函数。你可能想知道,这是如何实现的?实际上,函数是Python中的一级对象。换句话说,一个函数可以以变量的形式传递给其他函数的参数,就像其他任何常规对象一样。所以这里有很大的灵活性,也是Python最强大的几个功能的基础。

  让我们首先创建第一个例子,一个什么也不做的装饰器:

  defturn_off(func):

  returnlambda*args,* * kwargs:None

  首先注意这个turn_off()只是一个常规函数。它之所以成为装饰器,是因为它将一个函数作为唯一的参数,并返回另一个函数。我们可以使用turn_off()来修改其他函数,比如:

  打印(“你好”)

  你好

  print=turn_off(打印)

  打印(“嘘”)

  #没有打印

  print=turn_off(print)一行用turn_off()修饰符修饰print语句。实际上,它用匿名函数lambda *args,* * kwargs: None替换了函数print(),并返回turn_off()。匿名函数nbs

  p;lambda 除了返回 None 之外什么都不做。

  要定义更多丰富的装饰器,需要了解内部函数内部函数是在另一个函数内部定义的函数,它的一种常见用途是创建函数工厂:

  

def create_multiplier(factor):

 

  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)

 

  

6

 

  

 

  

quadruple = create_multiplier(factor=4)

 

  

28

 

  

 

  同样,可以使用内部函数来创建装饰器。装饰器是一个返回函数的函数:

  

def triple(func):

 

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

 

  我们都知道,文本字符串与数字相乘,是字符串的一种重复形式,因此字符串 'Penny'重复了 3 次。可以认为,装饰发生在knock = triple(knock)

  上述方法虽然实现了装饰器的功能,但似乎有点笨拙。PEP 318 引入了一种更方便的语法来应用装饰器。下面的knock()定义与上面的定义相同,但装饰器用法不同。

  

>>> @triple

 

  @符号用于应用装饰器,@triple表示triple()应用于紧随其后定义的函数。

  Python 标准库中定义的装饰器方法之一是:@functools.wraps。这在定义你自己的装饰器时非常有用。前面说过,装饰器是用另一个函数替换了一个函数,会给你的函数带来一个微妙的变化:

  

knock

 

  

<function triple.<locals>.wrapper_triple

 

  @triple装饰了knock(),然后被wrapper_triple()内部函数替换,被装饰的函数的名字会变成装饰器函数,除了名称,还有文档字符串和其他元数据都将会被替换。但有时,我们并不总是想将被修饰的函数的所有信息都被修改了。此时@functools.wraps正好解决了这个问题,如下所示:

  

import functools

 

  使用@triple的这个新定义保留元数据:

  

@triple

 

  

<function knock at 0x7fa3bfe5df28>

 

  注意knock()即使在被装饰之后,也同样保留了它的原有函数名称。当定义装饰器时,使用@functools.wraps是一种不错的选择,可以为大多数装饰器使用的如下模板:

  

import functools

 

  

 

  

创建 Python 定时器装饰器

 

  在本节中,云朵君将和大家一起学习如何扩展 Python 计时器,并以装饰器的形式使用它。接下来我们从头开始创建 Python 计时器装饰器。

  根据上面的模板,我们只需要决定在调用装饰函数之前和之后要做什么。这与进入和退出上下文管理器时的注意事项类似。在调用修饰函数之前启动 Python 计时器,并在调用完成后停止 Python 计时器。可以按如下方式定义@timer装饰器:

  

import functools

 

  可以按如下方式应用@timer

  

@timer

 

  

[ ... ]
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):

 

  

16

 

  

 

  

class Squarer:

 

  

16

 

  

 

  这里,square是一个可调用的实例,可以对数字求平方,就像square()第一个示例中的函数一样。

  我们现在向现有Timer类添加装饰器功能,首先需要import functools。

  

# timer.py

 

  在之前定义的上下文管理器Timer,给我们带来了不少便利。而这里使用的装饰器,似乎更加方便。

  

@Timer(text="Downloaded the tutorial in {:.2f} seconds")

 

  

[ ... ]
Downloaded the tutorial in 0.72 seconds

 

  

 

  有一种更直接的方法可以将 Python 计时器变成装饰器。其实上下文管理器和装饰器之间的一些相似之处:它们通常都用于在执行某些给定代码之前和之后执行某些操作

  基于这些相似之处,在 python 标准库中定义了一个名为ContextDecoratormixin类,它可以简单地通过继承ContextDecorator来为上下文管理器类添加装饰器函数。

  

from contextlib import ContextDecorator

 

  当以这种方式使用ContextDecorator时,无需自己实现.__call__(),因此我们可以大胆地将其从 Timer 类中删除。

  

 

  

使用 Python 定时器装饰器

 

  接下来,再最后一次重改download_data.py示例,使用 Python 计时器作为装饰器:

  

# download_data.py

 

  我们与之前的写法进行比较,唯一的区别是第 3 行的 Timer 的导入和第 4 行的@Timer()的应用。使用装饰器的一个显着优势是它们通常很容易调用

  但是,装饰器仍然适用于整个函数。这意味着代码除了记录了下载数据所需的时间外,还考虑了保存数据所需的时间。运行脚本:

  

$ python download_data.py

 

  

[ ... ]
Elapsed time: 0.69 seconds

 

  

 

  从上面打印出来的结果可以看到,代码记录了下载数据和保持数据一共所需的时间。

  当使用 Timer 作为装饰器时,会看到与使用上下文管理器类似的优势:

  

  • 省时省力:只需要一行额外的代码即可为函数的执行计时。

  • 可读性:当添加装饰器时,可以更清楚地注意到代码会对函数计时。

  • 一致性:只需要在定义函数时添加装饰器即可。每次调用时,代码都会始终如一地计时。

 

  然而,装饰器不如上下文管理器灵活,只能将它们应用于完整函数。

  

 

  

Python 计时器代码

 

  这里展开下面的代码块以查看 Python 计时器timer.py的完整源代码。

  

# timer.py

 

  可以自己使用代码,方法是将其保存到一个名为的文件中timer.py并将其导入:

  

from timer import Timer

 

  PyPI 上也提供了 Timer,因此更简单的选择是使用 pip 安装它:

  

pip install codetiming

 

  注意,PyPI 上的包名称是codetiming,安装包和导入时都需要使用此名称Timer

  

from codetiming import Timer

 

  除了名称和一些附加功能之外,codetiming.Timertimer.Timer完全一样。总而言之,可以通过三种不同的方式使用Timer

  1. 作为一个

  

t = Timer(name="class")

 

  2. 作为上下文管理器

  

with Timer(name="context manager"):

 

  3. 作为装饰器

  

@Timer(name="decorator")

 

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

 

  Python 的 Float 遵循IEEE 754 浮点算术标准,该标准以 64 位表示所有浮点数。因为浮点数有无限多位数,即不能用有限的位数来表达它们。

  考虑time()这个函数的主要目的,是它表示的是现在的实际时间。它以自给定时间点(称为纪元)以来的秒数来表示函数。time()返回的数字很大,这意味着可用的数字较少,因而分辨率会受到影响。简而言之,time()无法测量纳秒级差异:

  

>>> import time

 

  一纳秒是十亿分之一秒。上面代码中,将纳秒添加到参数 t ,他并不会影响结果。与time()不同的是,perf_counter()使用一些未定义的时间点作为它的纪元,它可以使用更小的数字,从而获得更好的分辨率:

  

>>> import time

 

  众所周知,将时间表示为浮点数是非常具有挑战的一件事,因此 Python 3.7 引入了一个新选项:每个时间测量函数现在都有一个相应的_ns函数,它以int形式返回纳秒数,而不是以浮点数形式返回秒数。例如,time()现在有一个名为time_ns()的纳秒对应项:

  

import time

 

  

1564342792866601283

 

  

 

  整数在 Python 中是无界的,因此time_ns()可以为所有永恒提供纳秒级分辨率。同样,perf_counter_ns()perf_counter()的纳秒版本:

  

>>> import time

 

  我们注意到,因为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

 

  注意,不同系统上的结果可能会有所不同。

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

  

  • time.monotonic(): 超时和调度,不受系统时钟更新影响

  • time.perf_counter():基准测试,短期内最精确的时钟

  • time.process_time():分析进程的CPU时间

 

  

 

  

估计运行时间timeit

 

  在实际工作中,通常会想优化代码进一步提升代码性能,例如想知道将列表转换为集合的最有效方法。下面我们使用函数set()和直接花括号定义集合{...}进行比较,看看这两种方法哪个性能更优,此时需要使用 Python 计时器来比较两者的运行速度。

  

>>> from timer import Timer

 

  该测试结果表明直接花括号定义集合可能会稍微快一些,但其实这些结果非常不确定。如果重新运行代码,可能会得到截然不同的结果。因为这会受计算机的性能和计算机运行状态所影响:例如当计算机忙于其他任务时,就会影响我们程序的结果。

  更好的方法是多次重复运行相同过程,并获取平均耗时,就能够更加精确地测量目标程序的性能大小。因此可以使用timeit标准库,它旨在精确测量小代码片段的执行时间。虽然可以从 Python 导入和调用timeit.timeit()作为常规函数,但使用命令行界面通常更方便。可以按如下方式对这两种变体进行计时:

  

$ python -m timeit --setup "nums = [7, 6, 1, 4, 1, 8, 0, 6]" "set(nums)"

 

  timeit自动多次调用代码以平均噪声测量。timeit的结果证实{*nums}量比set(nums)快。

  注意:在下载文件或访问数据库的代码上使用timeit时要小心。由于timeit会自动多次调用程序,因此可能会无意中向服务器发送请求!

  最后,IPython 交互式 shellJupyter Notebook使用%timeit魔术命令对此功能提供了额外支持:

  

In [1]: numbers = [7, 6, 1, 4, 1, 8, 0, 6]

 

  同样,测量结果表明直接花括号定义集合更快。在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

 

  要使用pstats,请在提示符下键入命令。通常你会使用sortstats命令,strip可以获得更清晰的输出:

  

download_data.prof% strip

 

  此输出显示总运行时间为 0.586 秒。它还列出了代码花费最多时间的十个函数。这里按累积时间 (cumtime) 排序,这意味着当给定函数调用另一个函数时,代码会计算时间。

  总时间 (tottime) 列表示代码在函数中花费了多少时间,不包括在子函数中的时间。要查找代码花费最多时间的位置,需要发出另一个sort命令:

  

download_data.prof% sort tottime

 

  可以使用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

 

  注意,不需要导入profile配置文件,它会在运行分析器时自动添加到全局命名空间中。不过,我们需要在完成分析后删除该行。否则,会抛出一个 NameError 异常。

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

  

$ kernprof -l download_data.py

 

  此命令自动将探查器数据保存在名为download_data.py.lprof的文件中。可以使用line_profiler查看这些结果:

  

$ python -m line_profiler download_data.py.lprof

 

  首先,注意本报告中的时间单位是微秒(1e-06 s)。通常,最容易查看的数字是%Time,它告诉我们代码在每一行的函数中花费的总时间的百分比。

  

 

  

总结

 

  在本文中,我们尝试了几种不同的方法来将 Python 计时器添加到代码中:

  

  • 使用了一个来保持状态并添加一个用户友好的界面。类非常灵活,直接使用 Timer 可以让您完全控制如何以及何时调用计时器。

  • 使用上下文管理器向代码块添加功能,并在必要时进行清理。上下文管理器使用起来很简单,使用with Timer()添加可以帮助您在视觉上更清楚地区分您的代码。

  • 使用装饰器向函数添加行为。装饰器简洁而引人注目,使用@Timer()是监控代码运行时的快速方法。

 

  我们还了解了为什么在对代码进行基准测试时应该更喜欢time.perf_counter()而不是time.time(),以及在优化代码时还有哪些其他有用的替代方法。

  现在我们可以在自己的代码中添加Python计时器函数了!在日志中跟踪程序的运行速度将有助于监视脚本。对于类、上下文管理器和装饰器一起工作的其他用例

  以上就是详解利用装饰器扩展Python计时器的详细内容,更多关于Python装饰器 计时器的资料请关注盛行IT软件开发工作室其它相关文章!

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: