简体   繁体   English

递归Python函数上的值修饰器

[英]Value-modifying decorator on recursive Python function

I'm trying to make a timing decorator in Python. 我正在尝试在Python中创建一个计时装饰器。 It works like this: 它是这样的:

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

This works for simple functions. 这适用于简单的功能。 But when I try it on a function like non-memoized Fibonacci 但是当我在非记忆斐波那契等函数上尝试时

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

it fails, because as it recurses: 它失败,因为随着递归:

  • fib(0) and fib(1) return 1 fib(0)fib(1)返回1
  • this is changed to { 'result': 1, 'time': 0.001 } 更改为{ 'result': 1, 'time': 0.001 }
  • fib(2) tries to add fib(0) + fib(1) , which is fib(2)尝试添加fib(0) + fib(1) ,即

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

How can I get a decorator to only decorate the final return value of a function? 我怎样才能得到一个装饰器只装饰一个函数的最终返回值? I don't want to have to modify the fib function at all. 我根本不需要修改fib函数。

If you use the function_timer function as a decorator, it will time each invocation of your fib function which you don't want. 如果使用function_timer函数作为装饰器,它将为您不需要的fib函数的每次调用计时。 You probably want to time the cumulative time it takes for all the Fibonacci functions to execute. 您可能想计时所有Fibonacci函数执行所需的累积时间。

As suggested by @minitech, you can simply wrap the fibonacci function via your function_timer function. 如@minitech所建议,您可以通过function_timer函数简单地包装fibonacci函数。

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

The second line above will only return once all recursion is complete, and end_time - start_time will accurately reflect the execution time. 上面的第二行仅在所有递归完成后才返回,并且end_time - start_time将准确反映执行时间。

You may not realize this, but a decorator f wraps a function g like so: f(g(x)) . 您可能没有意识到这一点,但是装饰器f封装了函数g如下所示: f(g(x)) In your case you don't want to wrap g with f for every call. 在您的情况下,您不想为每个调用都用f包装g You only want to it once, and so a decorator is not very useful here. 您只想要一次,因此装饰器在这里不是很有用。

timed_fib = function_timer(fib)

Trying to have fib provide one behavior to recursive calls and another to other users of the function is a recipe for pain. 试图让fib为递归调用提供一种行为,为函数的其他用户提供另一种行为是痛苦的秘诀。 Just don't replace fib . 只是不要替换fib Use another name for the timed function. 使用其他名称作为计时功能。 If you want, you can rename fib to _fib and have the timed version be fib : 如果需要,可以将fib重命名为_fib ,并将定时版本设置为fib

fib = function_timer(_fib)

There be hacks ahead. 未来会有黑客入侵。 Proceed with caution. 请谨慎操作。

Since cPython gives us access to frame introspection abilities, you can use that to try to figure out if a given call is recursive or not. 由于cPython使我们能够访问框架自省功能,因此您可以使用它来尝试确定给定的调用是否递归。 Basically: if we're calling f , we can peek at the next frame up on the stack; 基本上:如果我们调用f ,我们可以窥视堆栈中的下一帧; if it's got the same __name__ as f , it's probably the result of a recursive call. 如果它得到了相同的__name__f ,它可能是一个递归调用的结果。 Probably. 大概。

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

So then: 因此:

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

Disclaimer section: this is fragile. 免责声明部分:这很脆弱。 Don't do this in anything resembling production code - or if you must, come up with a way more vigorous test than if f.__name__ == sys._getframe(1).f_code.co_name: . 不要以任何类似于生产代码的方式执行此操作-或(如果必须)提供比if f.__name__ == sys._getframe(1).f_code.co_name:更为严格的测试方法 And definitely don't expect this to be portable to all implementations of python. 绝对不要期望这可以移植到python的所有实现中。

All that said, diving into stack frames is basically the only way to go about figuring if a given function call is recursive. 综上所述,进入堆栈框架基本上是判断给定函数调用是否递归的唯一方法。 Or, well, there is technically another way by annotating the function's __dict__ , but that's an order of magnitude more fragile, so this is as good as it gets. 或者,好吧,从技术上讲,还有另一种方法来对函数的__dict__进行注释,但这是一个更脆弱的数量级,因此就可以了。 (Or simply don't use a decorator, as the other answers say) (或者干脆不使用装饰器,如其他答案所述)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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