DEV Community

Cover image for Python decorators - a quick introduction
Priscila Gutierres
Priscila Gutierres

Posted on

Python decorators - a quick introduction

A decorator is a function that takes a function as input and returns another function, adding functionalities or replacing the entire fuction itself.
It is applied when the function is defined.

from functools import wraps
def timethis(func):
    ''' this decorator reports execution time '''
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(func.__name__, end -start)
        return result
    return wrapper    

Enter fullscreen mode Exit fullscreen mode

According to the official documentation, @functools.wraps is a convenience function for invoking update_wrapper() as a function decorator when defining a wrapper function.
If you forget to use @wraps, the decorated function will lose its name, docstring, annotations and calling signature.
Using @wraps , we can inject code to be executed alongside a given function, without losing function metadata.
In the given example, we start counting time before executing the original function, then stopping when it returns and print the time interval.

from time import sleep

@timethis
def timer(seconds):
    ''' creat a timer'''
    time.sleep(seconds)
Enter fullscreen mode Exit fullscreen mode

Defining a more useful decorator taking arguments (mandatory or optional)

Let's code a logging decorator to our timer function

import logging
def logged(level, name = None, message = None):
     ''' add logging '''
     def decorate(func):
        logname = name if name else func.__module__
        log = logging.getLogger(logname)
        logmsg = message if message else func.__name__
        @wraps(func)
        def wrapper(*args,**kwargs):
            log.log(level, logmsg)
            return func(*args, **kwargs)
        return wrapper
     return decorate

Enter fullscreen mode Exit fullscreen mode

Decorating the function

@logged(logging.CRITICAL, 'example')
def timer(seconds):
    sleep(seconds)
Enter fullscreen mode Exit fullscreen mode

In this example, we have a decorator taking our custom arguments and then returning a decorator. This decorator then is applied to the function we want to decorate.

def logged(func = None, *, level = logging.DEBUG, name = None, message = None):
     ''' add logging '''
     if func is None:
         return partial(logged, level=level, name=name, message=message)

     def decorate(func):
        logname = name if name else func.__module__
        log = logging.getLogger(logname)
        logmsg = message if message else func.__name__
        @wraps(func)
        def wrapper(*args,**kwargs):
            log.log(level, logmsg)
            return func(*args, **kwargs)
        return wrapper
     return decorate
Enter fullscreen mode Exit fullscreen mode

To create a decorator that takes no arguments, and avoid the calling conventions between decorators without any arguments and decorators with arguments, we use partial to return a new partial object which when called behaves like func called with the positional arguments args and keyword.

Can a decorator be defined as a class?

Also, decorators can be defined as classes and can be applied to class and static methods.

In fact, a decorator can be defined as a class, like @property, with a get, a set and a del method.

There are a lot more about decorators.
To really understand how decorators work you need to understand the variable scoping, closures, mtaclasses and descriptors.
I strong recommend you to read Fluent Python and Python Cookbook.

Top comments (0)