Python超时装饰器

时间:2016-02-18 19:20:24

标签: python python-2.7 python-decorators

我正在使用here提到的代码解决方案 我是装饰员的新手,并且不明白为什么这个解决方案如果我想写下面这样的东西不起作用:

@timeout(10)
def main_func():
    nested_func()
    while True:
        continue

@timeout(5)
def nested_func():
   print "finished doing nothing"

=>结果将完全没有超时。我们将陷入无休止的循环 但是,如果我从@timeout删除nested_func注释,则会出现超时错误 出于某种原因,我们不能同时在函数和嵌套函数上使用装饰器,任何想法为什么以及如何纠正它起作用,假设包含函数超时总是必须大于嵌套超时。

3 个答案:

答案 0 :(得分:2)

这是signal模块的计时功能的限制,您链接的装饰器使用这些功能。这是相关的piece of the documentation(我强调添加):

  

signal.alarm(time)

     

如果时间非零,则此函数请求在SIGALRM秒内将time信号发送到进程。 取消之前安排的任何警报(任何时候都只能安排一个警报)。返回的值是之前设置的警报之前的秒数。如果time为零,则不会安排任何警报,并且会取消任何计划的警报。如果返回值为零,则当前没有安排警报。 (参见Unix手册页报警(2)。)可用性:Unix。

所以,您所看到的是,当您nested_func被调用时,它的计时器会取消外部功能的计时器。

您可以更新装饰器以注意alarm调用的返回值(这将是上一次警报(如果有)到期之前的时间)。获得正确的细节有点复杂,因为内部计时器需要跟踪其功能运行的时间,因此它可以修改前一个计时器剩余的时间。这是一个未经测试的装饰版本,我认为它大部分都是正确的(但我并不完全确定它适用于所有异常情况):

import time
import signal

class TimeoutError(Exception):
    def __init__(self, value = "Timed Out"):
        self.value = value
    def __str__(self):
        return repr(self.value)

def timeout(seconds_before_timeout):
    def decorate(f):
        def handler(signum, frame):
            raise TimeoutError()
        def new_f(*args, **kwargs):
            old = signal.signal(signal.SIGALRM, handler)
            old_time_left = signal.alarm(seconds_before_timeout)
            if 0 < old_time_left < second_before_timeout: # never lengthen existing timer
                signal.alarm(old_time_left)
            start_time = time.time()
            try:
                result = f(*args, **kwargs)
            finally:
                if old_time_left > 0: # deduct f's run time from the saved timer
                    old_time_left -= time.time() - start_time
                signal.signal(signal.SIGALRM, old)
                signal.alarm(old_time_left)
            return result
        new_f.func_name = f.func_name
        return new_f
    return decorate

答案 1 :(得分:0)

Python的PyPI库中目前有一个更好的timeout decorator版本。它支持UNIX和非UNIX操作系统。提到信号的部分-专门用于UNIX。

假设您没有使用UNIX。下面是装饰器的代码片段,其中显示了可以根据需要使用的参数列表。

def timeout(seconds=None, use_signals=True, timeout_exception=TimeoutError, exception_message=None)

用于在 NON-UNIX 基本操作系统上实现。这就是我要做的:

import time
import timeout_decorator

@timeout_decorator.timeout(10, use_signals=False)
def main_func():
    nested_func()
    while True:
        continue

@timeout_decorator.timeout(5, use_signals=False)
def nested_func():
    print "finished doing nothing"

如果您注意到,我在做 use_signals = False 。仅此而已,您应该一切顺利。

答案 2 :(得分:0)

正如Blckknght所指出的,您不能将信号用于嵌套装饰器-但您可以使用multiprocerssing对其进行存档。

您可能会使用此装饰器,它支持嵌套装饰器:https://github.com/bitranox/wrapt_timeout_decorator

并且正如ABADGER1999在他的博客https://anonbadger.wordpress.com/2018/12/15/python-signal-handlers-and-exceptions/中指出的那样 使用信号和TimeoutException可能不是最好的主意-因为它可以在修饰的函数中捕获。

当然,您可以使用从基本异常类派生的您自己的异常,但是代码可能仍无法按预期工作- 请参见下一个示例-您可以在jupyter中尝试使用:https://mybinder.org/v2/gh/bitranox/wrapt_timeout_decorator/master?filepath=jupyter_test_wrapt_timeout_decorator.ipynb

import time
from wrapt_timeout_decorator import *

# caveats when using signals - the TimeoutError raised by the signal may be catched
# inside the decorated function.
# So You might use Your own Exception, derived from the base Exception Class.
# In Python-3.7.1 stdlib there are over 300 pieces of code that will catch your timeout
# if you were to base an exception on Exception. If you base your exception on BaseException,
# there are still 231 places that can potentially catch your exception.
# You should use use_signals=False if You want to make sure that the timeout is handled correctly !
# therefore the default value for use_signals = False on this decorator !

@timeout(5, use_signals=True)
def mytest(message):
    try:
        print(message)
        for i in range(1,10):
            time.sleep(1)
            print('{} seconds have passed - lets assume we read a big file here'.format(i))
    # TimeoutError is a Subclass of OSError - therefore it is catched here !
    except OSError:
        for i in range(1,10):
            time.sleep(1)
            print('Whats going on here ? - Ooops the Timeout Exception is catched by the OSError ! {}'.format(i))
    except Exception:
        # even worse !
        pass
    except:
        # the worst - and exists more then 300x in actual Python 3.7 stdlib Code !
        # so You never really can rely that You catch the TimeoutError when using Signals !
        pass


if __name__ == '__main__':
    try:
        mytest('starting')
        print('no Timeout Occured')
    except TimeoutError():
        # this will never be printed because the decorated function catches implicitly the TimeoutError !
        print('Timeout Occured')