【问题标题】:Python decorator to time recursive functions用于时间递归函数的 Python 装饰器
【发布时间】: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 语言的一部分。而且它可能会很慢。

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


【解决方案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* 稍微滥用 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
    

    如果这被认为是一个糟糕的 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 兼容)。

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

    【讨论】:

    • 不过,这存在线程安全和重入问题。例如,在一个线程中调用rec 会导致另一个线程中的调用忽略计时信息。
    • @user2357112:同意;这就是为什么我更喜欢你的回答。 :-) (可以获取当前线程信息,以便与threading 模块配合,但随后遇到绿色线程等问题)
    猜你喜欢
    • 2022-01-01
    • 2015-06-16
    • 1970-01-01
    • 1970-01-01
    • 2020-01-05
    • 2020-05-20
    • 1970-01-01
    • 2012-06-01
    • 2019-08-02
    相关资源
    最近更新 更多