Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does the @timeout(timelimit) decorator work?

Tags:

python

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
like image 257
sakurashinken Avatar asked Aug 05 '15 01:08

sakurashinken


People also ask

What is the point of decorators?

Decorators are a very powerful and useful tool in Python since it allows programmers to modify the behaviour of a function or class. Decorators allow us to wrap another function in order to extend the behaviour of the wrapped function, without permanently modifying it.

How to do a timeout in Python?

Use multiprocessing to timeout a Python function print('Starting function inc_forever()...') print(next(counter))def return_zero(): print('Starting function return_zero()...')

When should you use a decorator?

You'll use a decorator when you need to change the behavior of a function without modifying the function itself. A few good examples are when you want to add logging, test performance, perform caching, verify permissions, and so on. You can also use one when you need to run the same code on multiple functions.


1 Answers

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
like image 153
Russia Must Remove Putin Avatar answered Oct 25 '22 22:10

Russia Must Remove Putin