用于时间递归函数的 Python 装饰器
Posted
技术标签:
【中文标题】用于时间递归函数的 Python 装饰器【英文标题】:Python decorator to time recursive functions 【发布时间】:2019-04-13 09:44:18 【问题描述】:我有一个简单的装饰器来跟踪函数调用的运行时间:
def timed(f):
def caller(*args):
start = time.time()
res = f(*args)
end = time.time()
return res, end - start
return caller
可以如下使用,返回函数结果和执行时间的元组。
@timed
def test(n):
for _ in range(n):
pass
return 0
print(test(900)) # prints (0, 2.69e-05)
足够简单。但现在我想将其应用于递归函数。正如预期的那样,将上述包装器应用于递归函数会生成嵌套元组,其中包含每个递归调用的时间。
@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)
编写装饰器以便正确处理递归的优雅方式是什么?显然,如果是定时函数,您可以包装调用:
@timed
def wrapper():
return rec(3)
这将给出结果和时间的元组,但我希望所有这些都由装饰器处理,这样调用者就不必担心为每次调用定义一个新函数。想法?
【问题讨论】:
你可能可以通过使用sys._getframe
来实现你想要的,但这是CPython 的一个实现细节,所以它并不是抽象python 语言的一部分。而且它可能会很慢。
【参考方案1】:
虽然不是整体解决递归与装饰器集成的问题,但仅针对时间问题,我已经验证了时间元组的最后一个元素是整体运行时间,因为这是时间从最上面的递归调用。因此,如果你有
@timed
def rec():
...
在给定原始函数定义的情况下获得整体运行时,您可以简单地做
rec()[1]
另一方面,获取调用结果需要通过嵌套元组进行回避:
def get(tup):
if isinstance(tup, tuple):
return get(tup[0])
else:
return tup
这可能太复杂而无法简单地获取函数的结果。
【讨论】:
【参考方案2】:这里的问题并不是装饰器。问题是 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
【讨论】:
是的,我猜因为每次调用函数时都要调用装饰器的点,所以它本质上不适合递归。【参考方案3】:您可以通过 *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
如果这被认为是一个糟糕的 hack,我很乐意接受您的批评。
【讨论】:
滥用timed
函数对象的属性来存储计时信息会导致问题,如果您尝试一次计时多个事物。此外,timed
实际上并不需要func
,也不是真正定时执行func
;它正在计时with
块的内容。
简而言之,不好的答案?我最近只是在学习其中的一些工具集,所以我认为这可以工作,但我想一定有我遗漏的东西。不过并没有真正考虑多个计时器,所以这是一个很好的观点。【参考方案4】:
到目前为止,我更喜欢其他答案(特别是user2357112's answer),但您也可以制作一个基于类的装饰器来检测功能是否已被激活,如果是,则绕过时间:
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
【讨论】:
不过,这存在线程安全和重入问题。例如,在一个线程中调用rec
会导致另一个线程中的调用忽略计时信息。
@user2357112:同意;这就是为什么我更喜欢你的回答。 :-) (可以获取当前线程信息,以便与threading
模块配合,但随后遇到绿色线程等问题)以上是关于用于时间递归函数的 Python 装饰器的主要内容,如果未能解决你的问题,请参考以下文章