Decorating code (Part 1)

../decorators/fabiangallina.jpg Author: Fabián Ezequiel Gallina

In this article i'm going to write about how to code decorators in our favourite language.

A decorator is basically a callable [1] that wraps another and which allows us to modify the behavior of the wrapped one. This might sound complicated at the beginning but it is really easier than it seems.

Now, without any further ado we are going to cook some tasty home-baked wrapped callables.

Ingredients

  • a callable
  • a decorator
  • coffee (optional)
  • sugar (opcional)

Our to-be-wrapped callable for example will look like this:

def yes(string='y', end='\n'):
    """outputs `string`.

    This is similar to what the unix command `yes` does.

    Default value for `string` is 'y'

    """

    print(string, sep='', end=end)

Our decorator used to wrap the callable looks like this:

def log_callable(callable):
    """Decorates callable and logs information about it.

    Logs how many times the callable was called and the params it had.

    """

    if not getattr(log_callable, 'count_dict', None):
        log_callable.count_dict = {}

    log_callable.count_dict.setdefault(
        callable.__name__, 0
    )

    def wrap(*args, **kwargs):
        callable(*args, **kwargs)
        log_callable.count_dict[callable.__name__] += 1
        message = []

        message.append(
            """called: '{0}' '{1} times'""".format(
                callable.__name__,
                log_callable.count_dict[callable.__name__]
            )
        )
        message.append(
            """Arguments: {0}""".format(
                ", ".join(map(str, args))
            )
        )
        message.append(
            """Keyword Arguments: {0}""".format(
                ", ".join(["{0}={1}".format(key, value) \
                           for (key, value) in kwargs.items()])
            )
        )

        logging.debug("; ".join(message))

    return wrap

The coffee is just to stay awake while coding late at night and the sugar can be used with the coffee, however this is not mandatory, since everybody knows that sugar can also be eaten by the spoonful.

Preparation

Once we have our callable and our decorator, we proceed to mix them up in a bowl.

With Python we have 2 totally valid ways to mix them.

The first one, sweet, with syntactic sugar:

@log_callable
def yes(string='y', end='\n'):
   [...]

The second one [2], just for diabetics:

yes = log_callable(yes)

Voilá, our callable is now decorated.

Coming up next I'll talk about the basic anatomy of a decorator so our greengrocer can't cheat us at the moment of choosing one.

How does a decorator look like

Classes and functions can be decorators. These can also receive or not arguments (apart from the original callable arguments).

So we have two big groups:

  1. Decorator functions
    1. Without arguments.
    2. With arguments.
  2. Decorator classes
    1. Without arguments.
    2. With arguments.

The decorated callable must be called explicitly if the programmer wants the decorated callable to be executed. If this doesn't happens the decorator will be prevent the execution of it.

Example:

def disable(callable):
    """Decorates callable and prevents executing it."""

    def wrap(*args, **kwargs):
        logging.debug("{0} called but its execution has been prevented".format(
            callable.__name__)
        )

    return wrap

@disable
def yes(string='y', end='\n'):
   [...]

Decorator functions without arguments

In a decorator function that doesn't receive arguments, its first and only parameter is the callable to be decorated. In the nested function is where decorated callable positional and keyword arguments are received.

This can be seen in any of previous examples.

Decorator functions with arguments

Now we'll look an example of a decorator function that receives arguments. The example will be based on our previous log_callable and will allow us to specify if we really want to count the number of calls.

log_callable with arguments example:

def log_callable(do_count):

    if not getattr(log_callable, 'count_dict', None) and do_count:
        log_callable.count_dict = {}

    if do_count:
        log_callable.count_dict.setdefault(
            callable.__name__, 0
        )

    def wrap(callable):
        def inner_wrap(*args, **kwargs):
            callable(*args, **kwargs)

            message = []

            if do_count:
                log_callable.count_dict.setdefault(
                    callable.__name__, 0
                )
                log_callable.count_dict[callable.__name__] += 1
                message.append(
                    u"""called: '{0}' '{1} times'""".format(
                        callable.__name__,
                        log_callable.count_dict[callable.__name__],
                    )
                )
            else:
                message.append(u"""called: '{0}'""".format(callable.__name__))

            message.append(u"""Arguments: {0}""".format(", ".join(args)))
            message.append(
                u"""Keyword Arguments: {0}""".format(
                    ", ".join(["{0}={1}".format(key, value) \
                               for (key, value) in kwargs.items()])
                )
            )

            logging.debug("; ".join(message))

        return inner_wrap

    return wrap

A decorator function with arguments receives the params that are passed explicitly to the decorator. The callable is received in the first nested function and finally the decorated callable arguments are received by the deeper nested function (in our case called inner_wrap)

The way to use this decorator will be as follows:

@log_callable(False)
def yes(string='y', end='\n'):
   [...]

Decorator classes without arguments

As we said before, the decorator and the callable don't need to be functions, they can be classes too.

Here is a class version of our log_callable (without arguments):

class LogCallable(object):
    """Decorates callable and logs information about it.

    Logs how many times the callable was called and the params it had.

    """

    def __init__(self, callable):
        self.callable = callable

        if not getattr(LogCallable, 'count_dict', None):
            LogCallable.count_dict = {}

        LogCallable.count_dict.setdefault(
            callable.__name__, 0
        )


    def __call__(self, *args, **kwargs):
        self.callable(*args, **kwargs)
        LogCallable.count_dict[self.callable.__name__] += 1

        message = []

        message.append(
            """called: '{0}' '{1} times'""".format(
                self.callable.__name__,
                LogCallable.count_dict[self.callable.__name__]
            )
        )
        message.append(
            """Arguments: {0}""".format(
                ", ".join(map(str, args))
            )
        )
        message.append(
            """Keyword Arguments: {0}""".format(
                ", ".join(["{0}={1}".format(key, value) \
                           for (key, value) in kwargs.items()])
            )
        )

        logging.debug("; ".join(message))

In a decorator class that doesn't receive parameters, the first param of __init__ method is the callable to be decorated. The __call__ method receives the arguments of the decorated callable.

The most interesting difference with the function version is that by using a class decorator we have avoided the need of a nested function.

The way to use this decorator is the same as we do with decorator functions:

@LogCallable
def yes(string='y', end='\n'):
    [...]

Decorator classes with arguments

Understanding the 3 previous cases it is possible to guess how a decorator class with arguments should be.

LogCallable with params example:

class LogCallable(object):
    """Decorates callable and logs information about it.

    Logs how many times the callable was called and the params it had.

    """

    def __init__(self, do_count):
        self.do_count = do_count

        if not getattr(LogCallable, 'count_dict', None) and do_count:
            LogCallable.count_dict = {}

    def __call__(self, callable):

        def wrap(*args, **kwargs):
            callable(*args, **kwargs)

            message = []

            if self.do_count:
                LogCallable.count_dict.setdefault(
                    callable.__name__, 0
                )
                LogCallable.count_dict[callable.__name__] += 1
                message.append(
                    u"""called: '{0}' '{1} times'""".format(
                        callable.__name__,
                        LogCallable.count_dict[callable.__name__],
                    )
                )
            else:
                message.append(u"""called: '{0}'""".format(callable.__name__))

            message.append(
                u"""Arguments: {0}""".format(
                    ", ".join(map(str, args))
                )
            )
            message.append(
                u"""Keyword Arguments: {0}""".format(
                    ", ".join(["{0}={1}".format(key, value) \
                               for (key, value) in kwargs.items()])
                )
            )

            logging.debug("; ".join(message))

        return wrap

In a decorator class with parameters, these are passed to the __init__ method. The decorated callable is received by the __call__ method and the arguments of it are received by the nested function (called wrap in our example).

The way to use it is exactly the same as in the case of decorator functions with params:

@LogCallable(False)
def yes(string='y', end='\n'):
    [...]

Ending

Decorators open a world of possibilities allowing us to make code simpler and more readable, it is a matter of analizing our current needs to figure out if they are what we really need. So in our probable next part we'll see more practical examples and we'll take a look to class decorators (do not confuse it with decorator classses ;-)

[1]Class or function name (simplified version: without adding it parens so it doesn't get executed :)
[2]The first way is the recommended way you should take unless you are decorating classes in Python < 2.6.

PDF version | reSt version

Help PET: Donate

blog comments powered by Disqus

Last change: Thu Sep 9 17:09:50 2010.  -  This magazine is under a Creative Commons license.