说说在 Python 中如何实现输出指定函数运行时长的装饰器

467 阅读2分钟

假设我们需要一个可以输出某个函数运行时长的装饰器。

1 基础实现

一种可能的定义方式为:

import time


def clock(func):
    def clocked(*args):
        t0 = time.perf_counter()
        result = func(*args)
        elapsed = time.perf_counter() - t0
        name = func.__name__
        arg_str = ', '.join(repr(arg) for arg in args)
        logging.info('[%0.8fs] %s(%s) -> %r', elapsed, name, arg_str, result)
        return result

    return clocked

这里利用函数装饰器,在 clock(func) 函数内部定义了一个 clock(*args) 函数,定义好后直接返回。内部利用 perf_count() 函数实现计算函数运行时长。每调用一次 perf_counter(),Python 就会记录一个时间点,类似于在秒表上按下开始计时键;当第二次调用该函数时,会计算与第一个时间点的时间长,类似于在秒表上按下结束计时键1

func.__name__ 会返回入参函数的名称。repr(arg) 会返回一个 arg的 string 格式2。通过一系列转换,我们就可以得到一个以逗号作为分隔符的入参字符串。

内部函数最后以这样的一种格式 [时长] 运行函数名(多个入参字符串) -> 输出结果 输出函数运行报告。其中的 %0.8fs 表示小数保留8位,然后再转换为字符串。

而外部函数最后返回这个 clocked(*args) 函数。

接着我们使用这个 clock 装饰器,来输出以下两个函数的运行报告:

  1. 睡眠函数;
  2. 斐波那契函数。
import time
from course_6.clock_decorator import clock


@clock
def snooze(seconds):
    time.sleep(seconds)


@clock
def factorial(n):
    return 1 if n < 2 else n * factorial(n - 1)


if __name__ == '__main__':
    logging.info('snooze(.123) -> %s', snooze(.123))
    logging.info('factorial(6) -> %s', factorial(6))

运行结果:

这里使用 @装饰函数名 这样的语法来包装我们需要运行的函数。

@clock
def factorial(n):
    return 1 if n < 2 else n * factorial(n - 1)

实际上等价于:

def factorial(n):
    return 1 if n < 2 else n * factorial(n - 1)

factorial = clock(factorial)

所以从写法上来讲,第一种方式更加简洁。

如果输出 logging.info('factorial.__name__ -> %s',factorial.__name__) 就会得到 factorial.__name__ -> clocked。这就说明了 factorial 实际上是 clocked 函数,也就是说factorial 函数已经被装饰为 clocked 函数。所以每次调用 factorial 函数,本质上就是调用 clocked 函数。

2 优化

前面说了,如果输出 logging.info('factorial.__name__ -> %s',factorial.__name__) 就会得到 factorial.__name__ -> clocked。也就是说,装饰函数 clock(func) 把 factorial(n) 函数给遮住了。如果我们不想被装饰函数的 __name__ 属性被遮住,可以这样做:

@functools.wraps 也是一个装饰器,它可以把 func 中的相关属性复制到 clocked 中。这样再次输出logging.info('factorial.__name__ -> %s',factorial.__name__) 就会得到 factorial.__name__ -> factorial 咯。


这实际上就是经典的装饰器设计模式,但在是实现方式上与普通的面向对象语言差别较大。普通的面向对象语言采用的是面向对象的编程方式,而 Python 采用的是面向函数的编程方式。


  1. Python3 perf_counter() 用法.
  2. Python repr() 函数.
  3. Luciano Ramalho (作者),安道,吴珂 (译者).流畅的Python[M].人民邮电出版社,2017:319-322.