递归Python函数的值修改装饰器

时间:2014-07-19 04:55:32

标签: python recursion decorator python-decorators

我正在尝试用Python制作时序装饰器。它的工作原理如下:

def function_timer(f):
  """
  Creates an function timer wrapper around a function.
  This is meant to be used as a decorator (see PEP 318).

  Input:
    f :: any function

  Output:
    A function that, when called, executes f with any arguments, while timing
    the execution. The return value of this function depends on the return
    value of f. If f returns
      None ::
        then this function returns None
      a dictionary d ::
        then this function returns a new dictionary with all the contents of
        d, but with an additional key 'time' equal to the execution time of
        the function. If the key 'time' is taken in the original dictionary,
        then 'time_' will be tried, then 'time__', etc., until a free key is
        found. The original dictionary will be shallow-copied, not modified.
      some non-dict value x:
        then this function returns a dictionary whose 'result' key gives x,
        and whose 'time' key gives the execution time of the function.
  """
  def timed_function(*args, **kwargs):
    start_time = time.time()
    print args, kwargs
    result = f(*args, **kwargs)
    end_time = time.time()
    total_time = end_time - start_time

    if result is None:
      return None
    elif type(result) >= dict:
      result = result.copy()
      key = 'time'
      while key in result:
        key += '_'
      result[key] = total_time
    else:
      return { 'result': result, 'time': total_time }

  return timed_function

这适用于简单的功能。但是当我尝试使用非记忆的Fibonacci

这样的函数时
@function_timer
def fib(n):
  if n < 2:
    return 1
  else:
    return fib(n - 2) + fib(n - 1)

它失败了,因为它在递归:

  • fib(0)fib(1)返回1
  • 此更改为{ 'result': 1, 'time': 0.001 }
  • fib(2)尝试添加fib(0) + fib(1),即

    { 'result': 1, 'time': 0.001 } + { 'result': 1, 'time': 0.001 }
    
  • 这失败了。

如何让装饰器只装饰函数的 final 返回值?我根本不想修改fib功能。

3 个答案:

答案 0 :(得分:2)

如果您使用function_timer函数作为装饰器,它将为您不需要的fib函数的每次调用计时。您可能希望计算所有Fibonacci函数执行所需的累积时间。

正如@minitech所建议的那样,您只需通过function_timer函数包装斐波那契函数。

start_time = time.time()
result = fib(*args, **kwargs)
end_time = time.time()

上面的第二行只会在所有递归完成后返回,end_time - start_time将准确反映执行时间。

你可能没有意识到这一点,但装饰者f包裹了一个函数g,如下所示:f(g(x))。在您的情况下,您不希望g包含f每次通话。你只需要一次,所以装饰器在这里不是很有用。

答案 1 :(得分:1)

timed_fib = function_timer(fib)

试图让fib为递归调用提供一种行为而向函数的其他用户提供另一种行为是一种痛苦的处方。只是不要替换fib。使用另一个名称作为定时功能。如果需要,您可以将fib重命名为_fib,并将时间版本设为fib

fib = function_timer(_fib)

答案 2 :(得分:1)

前面有黑客攻击。谨慎行事。

由于cPython使我们能够访问帧内省能力,你可以使用它来试图判断给定的调用是否是递归的。基本上:如果我们正在调用f,我们可以查看堆栈中的下一帧;如果它与__name__具有相同的f,则可能是递归调用的结果。可能。

import sys

def function_timer(f):
   def timed_function(*args, **kwargs):
     if f.__name__ == sys._getframe(1).f_code.co_name:
         # recursive call!  Probably!
         return f(*args, **kwargs)
     # no changes to your function below here
     start_time = time.time()
     result = f(*args, **kwargs)
     end_time = time.time()
     total_time = end_time - start_time

     if result is None:
       return None
     elif type(result) >= dict:
       result = result.copy()
       key = 'time'
       while key in result:
         key += '_'
       result[key] = total_time
     else:
       return { 'result': result, 'time': total_time }

   return timed_function

那么:

@function_timer
def fib(n):
    if n < 2:
        return 1
    else:
        return fib(n - 2) + fib(n - 1)

fib(2)
Out[60]: {'result': 2, 'time': 2.86102294921875e-06}

fib(20)
Out[61]: {'result': 10946, 'time': 0.03364205360412598}

免责声明部分:这很脆弱。不要在类似于生产代码的任何事情中执行此操作 - 或者如果必须,请提出方式if f.__name__ == sys._getframe(1).f_code.co_name:更有力的测试。绝对不要指望这可以移植到python的所有实现。

所有这一切,潜入堆栈帧基本上是计算给定函数调用是否递归的唯一方法。或者,从技术上讲,通过注释函数的__dict__还有另一种方法,但这是一个更加脆弱的数量级,所以这就好了。 (或者只是不要使用装饰器,正如其他答案所说)