繁体   English   中英

Python 装饰器到时间递归函数

[英]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兼容)。

请注意,要真正正常工作,最外层的调用应该使用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

您可以通过* 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

如果这被认为是一个糟糕的黑客,我会很乐意接受你的批评。

虽然它不是整合递归与装饰器问题的整体解决方案,但对于时间问题,我已经验证了时间元组的最后一个元素是整体运行时间,因为这是从上层开始的时间 - 最近的递归调用。 因此,如果你有

@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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM