← Back to the journal

April 26, 2015 · ORIGINAL ARCHIVE

Python Decorators Reuse

Original author: Matt · Preserved from the original blog.

A python decorator
is a function (or class) that returns another function usually after applying
some transformation to it. Common examples of decorators are classmethod() and
staticmethod().

Decorators have a barrage of uses ranging from memoization, profiling, access
control to function timeouts. There is a collection of these and other decorator
code pieces here.

I have found myself mostly using decorators for retry and occasionally
timeouts for network centric operations. Normally I would have a single
decorator function that deals with particular exception and does several
retries before re-raising the exception.

Here is an example.

import time
from functools import wraps
from http.client import BadStatusLine

def if_http_errors_retry(func):
    """Decorator: retry calling function func in case of http.client errors.

    Decorator will try to call, the function three times, with a ten seconds
    delay between them. If the retries get maxed out, the decorator will raise
    the http error.

    args:
        func (function): function to be decorated.

    returns:
        func (function): decorated function

    """
    @wraps(func)
    def wrapper(*args, **kwargs):
        """func wrapper"""
        error = Exception
        for _ in range(3):
            try:
                return func(*args, **kwargs)
            except BadStatusLine as err:
                error = err
                time.sleep(10)
                continue
        raise error

    return wrapper

Looking keenly at the above decorator, you will realise that it bears some
design flaws:

You can’t specify the amount of time delay: it’s hard coded

You can’t specifiy the number of retries: it’s hard coded

It’s hard to generalize the decorator for another exception without entirely
duplicating it

After noting how much my code stinks I decided to refactor it and deal with the
three issues above. The motivation for this mainly came when I saw this retry template.

def auto_retry(n=3, exc=Exception):
    for i in range(n):
        try:
            yield None
            return
            except exc, err:
                # perhaps log exception here
                continue
    raise # re-raise the exception we caught earlier

It’s not a decorator at all but it had all the three qualities that my decorator
lacked and it gave me a basis for me to start exploring.

It turns out refactoring the decorator was pretty easy.

import time
from functools import wraps
from http.client import BadStatusLine

def auto_retry(tries=3, exc=Exception, delay=5):
    def deco(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(tries):
                try:
                    return func(*args, **kwargs)
                except exc:
                    time.sleep(delay)
                    continue
            raise exc
        return wrapper
    return deco

# decorating

@auto_retry(tries=3, exc=BadStatusLine, delay=5)
def network_call():
    # some code using http.client

Voila, now I have a very flexible retry decorator that can be applied on any
type of exception, time delay and number of retries.

The key to achieving this is having a function (auto_retry) that returns a
decorator function (deco) which will in turn decorates a function (func).
Thanks to the power of closures the parameters passed to the high order
function (auto_retry) are also available to the nested functions and are
used for the control flow within them.

About this archive

This article preserves the original writing and code. Older examples may use earlier library versions.

Original path: /2015/04/26/python-decorators-code-reuse.html