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
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.
Use multiprocessing to timeout a Python function print('Starting function inc_forever()...') print(next(counter))def return_zero(): print('Starting function return_zero()...')
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.
How does the @timeout(timelimit) decorator work?
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.
from functools import wraps
import errno
import os
import signal
class TimeoutError(Exception):
pass
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
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!
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With