[英]Python decorator to time recursive functions
我有一个简单的装饰器来跟踪 function 调用的运行时间:
def timed(f):
def caller(*args):
start = time.time()
res = f(*args)
end = time.time()
return res, end - start
return caller
这可以如下使用,并返回 function 结果和执行时间的元组。
@timed
def test(n):
for _ in range(n):
pass
return 0
print(test(900)) # prints (0, 2.69e-05)
很简单。 但现在我想将其应用于递归函数。 正如预期的那样,将上述包装器应用于递归 function 会生成带有每个递归调用时间的嵌套元组。
@timed
def rec(n):
if n:
return rec(n - 1)
else:
return 0
print(rec(3)) # Prints ((((0, 1.90e-06), 8.10e-06), 1.28e-05), 1.90e-05)
编写装饰器以便正确处理递归的优雅方式是什么? 显然,如果是定时 function,您可以结束通话:
@timed
def wrapper():
return rec(3)
这将给出结果和时间的元组,但我希望所有这些都由装饰器处理,以便调用者不必担心为每次调用定义一个新的 function。 想法?
这里的问题不是装饰者。 问题是rec
需要rec
成为一种行为方式的函数,但是你希望rec
是一个行为不同的函数。 没有干净的方法来协调单个rec
函数。
最干净的选择是停止要求rec
同时做两件事。 而不是使用装饰符号,将timed(rec)
分配给另一个名称:
def rec(n):
...
timed_rec = timed(rec)
如果您不想要两个名称,则需要编写rec
以了解装饰的rec
将返回的实际值。 例如,
@timed
def rec(n):
if n:
val, runtime = rec(n-1)
return val
else:
return 0
到目前为止,我更喜欢其他答案(特别是user2357112的答案 ),但您也可以创建一个基于类的装饰器来检测该功能是否已被激活,如果是,则绕过时间:
import time
class fancy_timed(object):
def __init__(self, f):
self.f = f
self.active = False
def __call__(self, *args):
if self.active:
return self.f(*args)
start = time.time()
self.active = True
res = self.f(*args)
end = time.time()
self.active = False
return res, end - start
@fancy_timed
def rec(n):
if n:
time.sleep(0.01)
return rec(n - 1)
else:
return 0
print(rec(3))
(用(object)
编写的类,以便与py2k和py3k兼容)。
请注意,要真正正常工作,最外层的调用应该使用try
和finally
。 这是__call__
幻想版本:
def __call__(self, *args):
if self.active:
return self.f(*args)
try:
start = time.time()
self.active = True
res = self.f(*args)
end = time.time()
return res, end - start
finally:
self.active = False
您可以通过* ahem *以不同的方式构建您的计时器*滥用contextmanager
和function attribute
...
from contextlib import contextmanager
import time
@contextmanager
def timed(func):
timed.start = time.time()
try:
yield func
finally:
timed.duration = time.time() - timed.start
def test(n):
for _ in range(n):
pass
return n
def rec(n):
if n:
time.sleep(0.05) # extra delay to notice the difference
return rec(n - 1)
else:
return n
with timed(rec) as r:
print(t(10))
print(t(20))
print(timed.duration)
with timed(test) as t:
print(t(555555))
print(t(666666))
print(timed.duration)
结果:
# recursive
0
0
1.5130000114440918
# non-recursive
555555
666666
0.053999900817871094
如果这被认为是一个糟糕的黑客,我会很乐意接受你的批评。
虽然它不是整合递归与装饰器问题的整体解决方案,但对于时间问题,我已经验证了时间元组的最后一个元素是整体运行时间,因为这是从上层开始的时间 - 最近的递归调用。 因此,如果你有
@timed
def rec():
...
给出你可以简单做的原始函数定义来获得整个运行时
rec()[1]
另一方面,获取调用的结果将需要通过嵌套元组重用:
def get(tup):
if isinstance(tup, tuple):
return get(tup[0])
else:
return tup
这可能太复杂了,无法简单地获得您的函数的结果。
在尝试分析简单的quicksort
实现时,我遇到了同样的问题。
主要问题是装饰器在每个 function 调用上执行,我们需要一些可以保留 state 的东西,所以我们可以在最后总结所有调用。 装饰器不是工作的正确工具
然而,一种想法是滥用函数是对象并且可以具有属性的事实。 下面用一个简单的装饰器对此进行探讨。 必须了解的是,通过使用装饰器的 sintax 糖 ( @
),function 将始终累积其时序。
from typing import Any, Callable
from time import perf_counter
class timeit:
def __init__(self, func: Callable) -> None:
self.func = func
self.timed = []
def __call__(self, *args: Any, **kwds: Any) -> Any:
start = perf_counter()
res = self.func(*args, **kwds)
end = perf_counter()
self.timed.append(end - start)
return res
# usage
@timeit
def rec(n):
...
if __name__ == "__main__":
result = rec(4) # rec result
print(f"Took {rec.timed:.2f} seconds")
# Out: Took 3.39 seconds
result = rec(4) # rec result
# timings between calls are accumulated
# Out: Took 6.78 seconds
这给我们带来了一个受@r.ook启发的解决方案,下面是一个简单的上下文管理器,它存储每个运行时间并在最后打印其总和( __exit__
)。 请注意,因为对于每个时间我们都需要一个with
语句,所以这不会累积不同的运行。
from typing import Any, Callable
from time import perf_counter
class timeit:
def __init__(self, func: Callable) -> None:
self.func = func
self.timed = []
def __call__(self, *args: Any, **kwds: Any) -> Any:
start = perf_counter()
res = self.func(*args, **kwds)
end = perf_counter()
self.timed.append(end - start)
return res
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, exc_traceback):
# TODO: report `exc_*` if an exception get raised
print(f"Took {sum(self.timed):.2f} seconds")
return
# usage
def rec(n):
...
if __name__ == "__main__":
with timeit(rec) as f:
result = f(a) # rec result
# Out: Took 3.39 seconds
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.