Python Timer:三种监控你程序运行时间的方式

很多开发者知道Python是一个开发效率很高的编程语言,纯Python程序对比其它编译型语言如:C、Rust、或Java可能运行很慢。通过这篇教程,你将看到如何使用Python timer去监控你程序运行的速度。


Python Timers

首先,看一下本篇教程中会使用到的代码。随后你将添加Python Timer到程序中去监控它们的性能。你也将看到测量运行时间的一些简单方式。

Python Timer Functions

如果你看过Python内置的time模块,那么你应该知道测量时间的一些方法:

monotonic()

perf_counter()

process_time()

time()

Python3.7介绍了一些新方法,如thread_time(),还有上面方法的纳秒版本,以_ns后缀结尾。例如:perf_counter_ns()就是perf_counter()的纳秒版本。后面你会学习使用这些方法。注意,文档中perf_counter()的说明:

返回性能计数器的值(以小数秒为单位),即具有最高可用分辨率的时钟,以测量短持续时间。

你将使用perf_counter()方法去创建一个Python timer,然后去和其它的Python timer方法对比,知道为什么通常perf_counter()是最好的选择。


实例:下载

为了方便比较,你可以将不同的方法应用到相同的逻辑代码块中。

下面我们将使用realpython-reader包去Real Python上下载最新的教程。如果你的系统没有安装realpython-reader包,你可以使用pip进行安装。

pip install realpython-reader


安装完成后,你可以导入包reader。

新建程序文件latest_tutorial.py。代码由只有一个方法,从Real Python下载最新的教程,然后打印出来。

from reader import feed





def main():

    """Download and print the latest tutorial from Real Python"""

    tutorial = feed.get_article(0)

    print(tutorial)





if __name__ == "__main__":

    main()


get_article()方法中参数0代表最新的一篇教程,1代表上一篇,以此类推。

当你运行这个例子,可能输出如下类似的内容

# Using Pandas and Python to Explore Your Dataset





Do you have a large dataset that's full of interesting insights, but you're

not sure where to start exploring it? Has your boss asked you to generate some

statistics from it, but they're not so easy to extract? These are precisely

the use cases where **Pandas** and Python can help you! With these tools,

you'll be able to slice a large dataset down into manageable parts and glean

insight from that information.





**In this tutorial, you 'll learn how to:**

......剩下下面文章内容......


代码可能需要一点时间运行,这取决于你的网络情况。所以它很适合使用Python timer来监控运行效率。


第一种Python Timer

添加系统计算器time.perf_counter()到这个例子中。它非常适合这段代码的计时。

perf_counter()以秒为单位返回某个未指定时刻的值。所以单次调用几乎没什么用,但在不同时刻两次调用你可以统计出花费的秒数:

>>> import time

>>> time.perf_counter()

23.3523745

>>> time.perf_counter() # a few seconds later

33.8999726

>>> 33.89 - 23.35

10.54


现在将Python timer添加到代码中:

from reader import feed

import time





def main():

    """Download and print the latest tutorial from Real Python"""

    tic = time.perf_counter()

    tutorial = feed.get_article(0)

    toc = time.perf_counter()



    print(f"Download the tutorial in {toc - tic:.2f} seconds.")



    print(tutorial)





if __name__ == "__main__":

    main()


我们在下载前后都调用了一次perf_counter(),然后计算时间差。

注意:打印中的f-string,它是python3.6以后可使用的一种格式化字符串方法。:.2f表示保留2位有效数字,处理的对象是toc -tic。

运行这个程序:

Download the tutorial in 4.36 seconds.

# Python args and kwargs: Demystified





Sometimes, when you look at a function definition in Python, you might see

that it takes two strange arguments: **`*args`** and **`**kwargs`**. If you've

ever wondered what these peculiar variables are, or why your IDE defines them

in `main()`, then this course is for you! You'll learn how to use args and

kwargs in Python to add more flexibility to your functions.

......剩下下面文章内容......


这就是基本的计时代码,接下来将会把计时器以class,context manager,decorator的方式实现,以方便使用。


Python Timer Class

回归一下上面的例子,在执行下载教程之前,你需要一个变量(tic)去存储python timer的值。之后再记录python timer值(toc),这样就能达到计时的目的了。接下来创建类也是用perf_counter()做同样的事情,但是可读性更好。

创建timer.py:

import time





class TimerError(Exception):

    """A custom exception used to report errors in use of Timer class"""





class Timer:

    def __init__(self):

        self._start_time = None



    def start(self):

        if self._start_time:

            raise TimeoutError("Timer is running.")



        self._start_time = time.perf_counter()



    def stop(self):

        if not self._start_time:

            raise TimeoutError("Timer is not running.")



        elapsed_time = time.perf_counter() - self._start_time

        self._start_time = None



        print(f"Elapsed time: {elapsed_time:.4f} seconds.")


_start_time为None代表timer没有启动,timer启动记录初始时刻,停止时记录结束时刻,计时后将_start_time改为未启动状态。

另外:_start_time下划线开头是python的惯例,代表内部属性,不应在外边使用。

使用:

>>> from timer import Timer

>>> t = Timer()

>>> t.start()

>>> t.stop() # A few seconds later

Elapsed time: 7.5385 seconds.


对比上面直接使用perf_counter()方法,可以看到代码变得更简洁、清晰,可读性更强。


用Python Timer Class计时

修改latest_tutorial.py程序

from reader import feed

from timer import Timer





def main():

    """Download and print the latest tutorial from Real Python"""

    t = Timer()

    t.start()

    tutorial = feed.get_article(0)

    t.stop()



    print(tutorial)





if __name__ == "__main__":

    main()


运行这段程序:

Elapsed time: 3.7284 seconds.

......


增加更多的灵活性

在上面代码中,历时信息是硬编码在stop方法中,这不够灵活,可以使用参数,并提供合适的默认值。

def __init__(self, text="Elapsed time: {:0.4f} seconds"):

    self._start_time = None

    self.text = text


然后,在stop()中,使用format()渲染值

def stop(self):

    if not self._start_time:

        raise TimeoutError("Timer is not running.")



    elapsed_time = time.perf_counter() - self._start_time

    self._start_time = None



    print(self.text.format(elapsed_time))


修改timer.py后,就可以灵活的使用历时信息了

>>> from timer import Timer

>>> t = Timer(text="You waited {:.1f} seconds.")

>>> t.start()

>>> t.stop()

You waited 6.5 seconds.


接下来,如果不想打印信息到控制台,而是想保存测量信息到数据库,那么应该返回stop()的elapsed_time值。当调用代码时可以选择忽略保存测量信息,也可以忽略返回信息。或许你还想整合信息到logging中。为了支持logging或者其它的输出方式,我们应该修改直接调用print(),而是接收一个我们指定的logging方法。这和上面指定text信息类似:

import time





class TimerError(Exception):

    """A custom exception used to report errors in use of Timer class"""





class Timer:

    def __init__(self, text="Elapsed time: {:0.4f} seconds", logger=print):

        self._start_time = None

        self.text = text

        self.logger = logger



    def start(self):

        if self._start_time:

            raise TimeoutError("Timer is running.")



        self._start_time = time.perf_counter()



    def stop(self):

        if not self._start_time:

            raise TimeoutError("Timer is not running.")



        elapsed_time = time.perf_counter() - self._start_time

        self._start_time = None



        if self.logger:

            self.logger(self.text.format(elapsed_time))



        return elapsed_time


上面又新增了一个实体变量self.logger,它应该接收一个字符串类型的方法名。除了print,还可以使用如:logging.info,文件对象的write方法等。if判断是当logger=None时,关闭输出操作。

测试这个两个功能:

>>> from timer import Timer

>>> import logging

>>> t = Timer(logger=logging.warning)

>>> t.start()

>>> t.stop()

WARNING:root:Elapsed time: 5.6231 seconds

5.623065699999998



>>> t = Timer(logger=None)

>>> t.start()

>>> value = t.stop()

>>> value

7.919924000000002


最后一点改进,如果你在循环中调用一个慢的方法,你可以在用字典保存具名计时器以方便追踪每个计时器。

如:

from reader import feed

from timer import Timer





def main():

    t = Timer(text="Downloaded 10 tutorials in {:.2f} seconds.")

    t.start()

    for tutorial_num in range(10):

        tutorial = feed.get_article(tutorial_num)

        print(tutorial)

    t.stop()





if __name__ == "__main__":

    main()


这个代码会下载最新的10条教程,运行结果

......

Downloaded 10 tutorials in 5.16 seconds.


我们需要改进的一点就是print,在循环中print会执行多次。虽然print运行的时间与下载的时间相比几乎可以忽略不记,但具名计时器可以很好的解决这个问题,我们来实现它。

首先,在Timer类中定义类变量.timers,这意味着Timer所有实体对象共享这个变量。它可以通过类直接访问,也可以使用类实例对象。

class Timer:

    timers = dict()


>>> from timer import Timer

>>> Timer.timers

{}

>>> t = Timer()

>>> t.timers

{}

>>> Timer.timers is t.timers

True


可以看到它们访问都是同一对象。

然后在Timer中添加name参数,相同name计时器叠加得到最后执行时间。

import time





class TimerError(Exception):

    """A custom exception used to report errors in use of Timer class"""





class Timer:

    timers = dict()



    def __init__(self, name=None, text="Elapsed time: {:0.4f} seconds", logger=print):

        self._start_time = None

        self.name = name

        self.text = text

        self.logger = logger



        if name:

            self.timers.setdefault(name, 0)



    def start(self):

        if self._start_time:

            raise TimeoutError("Timer is running.")



        self._start_time = time.perf_counter()



    def stop(self):

        if not self._start_time:

            raise TimeoutError("Timer is not running.")



        elapsed_time = time.perf_counter() - self._start_time

        self._start_time = None



        if self.logger:

            self.logger(self.text.format(elapsed_time))

        if self.name:

            self.timers[self.name] += elapsed_time



        return elapsed_time


给Timer指定一个名称,它每一次的计时都会累加保存在timers中。

>>> from timer import Timer

>>> t = Timer("accumulate")

>>> t.start()

>>> t.stop()

Elapsed time: 3.5549 seconds

3.554935799999999

>>>

>>> t.start()

>>> t.stop()

Elapsed time: 3.0968 seconds

3.0967951000000014

>>>

>>> Timer.timers

{'accumulate': 6.6517309000000004}


现在回到latest_tutorials.py文件中,给Timer添加一个名称

from reader import feed

from timer import Timer





def main():

    name = "download"

    t = Timer(name, logger=None)

    for tutorial_num in range(10):

        t.start()

        tutorial = feed.get_article(tutorial_num)

        t.stop()

        print(tutorial)

    download_time = Timer.timers[name]

    print(f"Downloaded 10 tutorials in {download_time:.2f} seconds")





if __name__ == "__main__":

    main()


这样就是下载所用的真实时间了。

总结使用Class Timer的好处:

可读性:代码阅读起来更自然

一致性:将属性和行为封装成参数使用起来更容易

灵活性:使用带有默认值的参数,代码可重用


A Python Timer Context Manager

计时器类对比刚开始的计时器方法,已经变得很强大。但是步骤仍然有点繁琐:

1. 实例化类

2. 在你想监控的代码块前调用.start()

3. 在代码块结束处调用.stop()

Python还有一种在代码块之前、之后调用函数的构造方式:context manager。

上下文管理在python2时就已经引进了,关键字with,语法:

with EXPRESSION as VARIABLE:

    BLOCK


EXPRESSION是python表达式,返回一个上下文管理器,绑定到VARIABLE,BLOCK是正常的python代码块。上下文管理器保证在BLOCK之前或BLOCK之后调用一些代码,即使BLOCK抛出异常。

常使用上下文管理处理一些资源,如:files、locks、和数据库连接。它可以在资源使用完毕后释放资源。

with open("timer.py") as fp:

    print(fp.read())


要使用context manager,必须实现context manager协议,它包含两点:

1. 在进入上下文前调用__enter__()

2. 在推出上下文后调用__exit__()

简而言之,要实现自己的context manager,需要创建class,并实现__enter__()和__exit__()特殊方法。

class Greeter:

    def __init__(self, name):

        self.name = name



    def __enter__(self):

        print(f"Hello, {self.name}")



    def __exit__(self, exc_type, exc_val, exc_tb):

        print(f"See you later, {self.name}")


Greeter是一个上下文管理器,因为它实现了上下文管理器协议。

>>> from greeter import Greeter

>>> with Greeter("Nick"):

...     print("Doing stuff...")

...

Hello, Nick

Doing stuff...

See you later, Nick


__enter__()是在之前调用,__exit__()是在之后调用。这个例子中并没有用到上下文管理器,因此并不需要as关联到一个name上。

修改一下,关联上下文管理器

class Greeter:

    def __init__(self, name):

        self.name = name



    def __enter__(self):

        print(f"Hello, {self.name}")

        return self



    def __exit__(self, exc_type, exc_val, exc_tb):

        print(f"See you later, {self.name}")


返回的self将可以关联到as name。

>>> from greeter import Greeter

>>> with Greeter("Nick") as grt:

...     print(f"{grt.name} is doing stuff...")

...

Hello, Nick

Nick is doing stuff...

See you later, Nick


创建上下文计时器

我们已经看到上下文管理器是如何工作的了。计时器每次开始需要调用start(),结束需要调用stop(),上下文管理器可以自动调用。实现context manager协议只需要实现__enter__()和__exit__(),分别是启动和停止定时器。

def __enter__(self):

    self.start()

    return self



def __exit__(self, exc_type, exc_val, exc_tb):

    self.stop()


现在可以使用上下文管理Timer

>>> from timer import Timer

>>> import time

>>> with Timer():

...     time.sleep(.7)

...

Elapsed time: 0.7126 seconds


修改上面latest_tutorial.py文件,使用context manager的方式来计时

from timer import Timer

from reader import feed





def main():

    with Timer():

        tutorial = feed.get_article(0)

    print(tutorial)





if __name__ == '__main__':

    main()


Python Timer 装饰器

Timer 类已经非常方便了,但如果想要监控一个方法运行时间,还有一种更精简的方法。当然,Timer也可以监控方法:

with Timer("some_name"):

    do_something()


or


def do_something():

    with Timer("some_name"):

        ...


这两种方式都要改动do_something()方法,不麻烦但也不方便,更好的方式是使用装饰器。装饰器可以修改函数和类的行为。

装饰器的通用模板:

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


与Timer Context Manager类似,需要被装饰的方法调用之前开启定时器,调用结束了停止定时器。

import functools

import time

from reader import feed





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:.4f} seconds.")

        return value

    return wrapper_timer





@timer

def latest_tutorial():

    tutorial = feed.get_article(0)

    print(tutorial)





if __name__ == '__main__':

    latest_tutorial()


这个timer装饰器可以正常工作,但它与最开始的方式无异,远没有Timer类强大、灵活。幸好,python中类也可用作装饰器。我们知道装饰器必须是可调用的,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


可以看到Squarer类实现了__call__方法,它的实例square是可调用的,也就是调用__call__方法。

那么在我们Timer类中只需要实现__call__方法即可:

def __call__(self, func):

    @functools.wraps(func)

    def wrapper_timer(*args, **kwargs):

        with self:

            return func(*args, **kwargs)

    return wrapper_timer


__call__方法实际上是调用上面已经实现的context manager方法,现在可以使用类作为装饰器使用了

>>> @Timer(text="下载教程使用 {:.2f} 秒")

... def latest_tutorial():

...     tutorial = feed.get_article(0)

...     print(tutorial)



...>>> latest_tutorial()


除了实现自己的__call__特殊方法外,python还有一个标准库ContextDecorator。它可以在context manager环境中添加装饰器功能,只需要将类继承至ContextDecorator即可。

from contextlib import ContextDecorator



class Timer(ContextDecorator):

    # 不再需要__call__


总结

上面提供了不同的方法创建计时器:

使用Timer类是非常灵活的,完成可以控制如何、什么时候去调用计时器

使用context manager监控代码块,方便直接,with语法具有优秀的阅读性

装饰器是非常简洁的,使用@Timer()可快速监控方法运行时间


 

展开阅读全文