12

I found this decorator that times out a function here on Stack Overflow, and I am wondering if someone could explain in detail how it works, as the code is very elegant but not clear at all. Usage is @timeout(timelimit).

from functools import wraps
import errno
import os
import signal

class TimeoutError(Exception):
    pass

def timeout(seconds=100, error_message=os.strerror(errno.ETIME)):
    def decorator(func):
        def _handle_timeout(signum, frame):
            raise TimeoutError(error_message)

        def wrapper(*args, **kwargs):
            signal.signal(signal.SIGALRM, _handle_timeout)
            signal.alarm(seconds)
            try:
                result = func(*args, **kwargs)
            finally:
                signal.alarm(0)
            return result

        return wraps(func)(wrapper)

    return decorator
CubeJockey
  • 2,209
  • 8
  • 24
  • 31
sakurashinken
  • 3,940
  • 8
  • 34
  • 67

1 Answers1

18

How does the @timeout(timelimit) decorator work?

Decorator Syntax

To be more clear, based on the example in the question, the usage is like this:

@timeout(100)
def foo(arg1, kwarg1=None):
    '''time this out!'''
    something_worth_timing_out()

The above is the decorator syntax. The below is semantically equivalent:

def foo(arg1, kwarg1=None):
    '''time this out!'''
    something_worth_timing_out()

foo = timeout(100)(foo)

Note that we name the function that wraps the original foo, "foo". That's what the decorator syntax means and does.

Necessary imports

from functools import wraps
import errno
import os
import signal

Exception to raise on Timeout

class TimeoutError(Exception):
    pass

Analysis of the function

This is what's called in the line, @timeout(timelimit). The timelimit argument will be locked into the inner functions, making those functions "closures", so-called because they close-over the data:

def timeout(seconds=100, error_message=os.strerror(errno.ETIME)):

This will return a function that takes a function as an argument, which the next line proceeds to define. This function will return a function that wraps the original function. :

    def decorator(func):

This is a function to timeout the decorated function:

        def _handle_timeout(signum, frame):
            raise TimeoutError(error_message)

And this is the actual wrapper. Before calling the wrapped function, it sets a signal that will interrupt the function if it does not finish in time with an exception:

        def wrapper(*args, **kwargs):
            signal.signal(signal.SIGALRM, _handle_timeout)
            signal.alarm(seconds)
            try:
                result = func(*args, **kwargs)
            finally:
                signal.alarm(0)

This will return the result if the function completes:

            return result

This returns the wrapper. It makes sure the wrapped function gets the attributes from the original function, like docstrings, name, function signature...

        return wraps(func)(wrapper)

and this is where the decorator is returned, from the original call, @timeout(timelimit):

    return decorator

Benefit of wraps

The wraps function allows the function that wraps the target function to get the documentation of that function, because foo no longer points at the original function:

>>> help(foo)
Help on function foo in module __main__:

foo(arg1, kwarg1=None)
    time this out!

Better usage of wraps

To further clarify, wraps returns a decorator, and is intended to be used much like this function. It would be better written like this:

def timeout(seconds=100, error_message=os.strerror(errno.ETIME)):
    def decorator(func):
        def _handle_timeout(signum, frame):
            raise TimeoutError(error_message)
        @wraps(func)
        def wrapper(*args, **kwargs):
            signal.signal(signal.SIGALRM, _handle_timeout)
            signal.alarm(seconds)
            try:
                result = func(*args, **kwargs)
            finally:
                signal.alarm(0)
            return result
        return wrapper
    return decorator
Community
  • 1
  • 1
Russia Must Remove Putin
  • 374,368
  • 89
  • 403
  • 331
  • 1)What does the python interpreter actually do to process this code? specifically, how does it execute the @timeout(timelimit) directive? 2) return wraps(func)(wrapper) is unclear. Why does it have two parenthetical bodies? 3) why does the combination of try and signal.alarm spawn multiple threads, as is necessary to execute this function? – sakurashinken Aug 05 '15 at 02:56
  • 1) see the inner function `wrapper`. It gets called when you do `foo()`. 2) see addendum to end of the answer. 3) `try` has nothing to do with it, `finally` just stops the timer if it doesn't time-out, and you have to have a thread, otherwise concurrency would be much more difficult. – Russia Must Remove Putin Aug 05 '15 at 03:02
  • I guess my fundamental confusion is what the @decorator(args) does. What would equivalent python code be? – sakurashinken Aug 05 '15 at 03:04
  • Let me clear that up, look at the very beginning of the answer. – Russia Must Remove Putin Aug 05 '15 at 03:08
  • 2
    Thank you, this was very helpful. – sakurashinken Aug 05 '15 at 18:44