用于时间递归函数的 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* 稍微滥用 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 模块配合,但随后遇到绿色线程等问题)

以上是关于用于时间递归函数的 Python 装饰器的主要内容,如果未能解决你的问题,请参考以下文章

python--递归函数匿名函数嵌套函数高阶函数装饰器生成器迭代器

使用 python 装饰器调用递归函数的次数

进阶学Python:Python的递归函数和装饰器

python函数装饰器内置函数json及模块

python基础第19回 多层,有参装饰器 递归 二分法

递归函数生成器装饰器