说说在 Python 中如何实现输出指定函数运行时长的装饰器
假设我们需要一个可以输出某个函数运行时长的装饰器。
1 基础实现
一种可能的定义方式为:
这里利用函数装饰器,在 clock(func) 函数内部定义了一个 clock(*args) 函数,定义好后直接返回。内部利用 perf_count() 函数实现计算函数运行时长。每调用一次 perf_counter(),Python 就会记录一个时间点,类似于在秒表上按下开始计时键;当第二次调用该函数时,会计算与第一个时间点的时间长,类似于在秒表上按下结束计时键1。
func.__name__
会返回入参函数的名称。repr(arg) 会返回一个 arg的 string 格式2。通过一系列转换,我们就可以得到一个以逗号作为分隔符的入参字符串。
内部函数最后以这样的一种格式 [时长] 运行函数名(多个入参字符串) -> 输出结果
输出函数运行报告。其中的 %0.8fs 表示小数保留8位,然后再转换为字符串。
而外部函数最后返回这个 clocked(*args) 函数。
接着我们使用这个 clock 装饰器,来输出以下两个函数的运行报告:
睡眠函数;
斐波那契函数。
运行结果:
这里使用 @装饰函数名
这样的语法来包装我们需要运行的函数。
实际上等价于:
所以从写法上来讲,第一种方式更加简洁。
如果输出 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 采用的是面向函数的编程方式。
Luciano Ramalho (作者),安道,吴珂 (译者).流畅的Python[M].人民邮电出版社,2017:319-322.