时间点递归函数的Python装饰器

时间:2018-11-09 19:32:28

标签: python python-3.x recursion python-decorators

我有一个简单的装饰器来跟踪函数调用的运行时间:

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)

这将给出结果和时间的元组,但我希望所有结果都由装饰器处理,以便调用者不必担心为每个调用定义一个新函数。想法?

4 个答案:

答案 0 :(得分:7)

这里的问题实际上不是装饰者。问题在于,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

答案 1 :(得分:3)

到目前为止,我更喜欢其他答案(尤其是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兼容)。

请注意,要真正正常工作,最外面的调用应使用tryfinally。这是__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

答案 2 :(得分:1)

您可以通过* ahem *稍微滥用contextmanagerfunction 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

如果这被认为是不好的破解,我很乐意接受您的批评。

答案 3 :(得分:0)

尽管这不是解决将递归与装饰器集成的问题的整体解决方案,但仅出于计时问题,我已经验证了时间元组的最后一个元素是整体运行时间,因为这是时间从最上面的递归调用。因此,如果您有

@timed
def rec():
    ...

要获得给定的原始函数定义的总体运行时间,只需执行

rec()[1]

另一方面,获取调用结果将需要在嵌套元组中重新使用:

def get(tup):
    if isinstance(tup, tuple):
        return get(tup[0])
    else:
        return tup

这可能太复杂了,以至于无法简单地获取函数的结果。