Python Decorators

By

Learn how Python decorators work: using the @ syntax to wrap a function in another function that can run code before and after it, like a logtime example.

~~~

Decorators are a way to change, enhance or alter in any way how a function works.

You apply one with the @ symbol followed by the decorator name, right before the function definition:

@logtime
def hello():
    print('hello!')

This hello function has the logtime decorator assigned.

Python applies the decorator once, at the moment the function is defined. From then on, every call to hello() runs the wrapped version instead of the original.

How do you write a decorator?

A decorator is a function that takes a function as a parameter, wraps the function in an inner function that performs the job it has to do, and returns that inner function.

Here’s logtime, a decorator that measures how long a function takes to run:

from time import time

def logtime(func):
    def wrapper():
        start = time()
        val = func()
        print(f'{func.__name__} took {time() - start:.4f} seconds')
        return val
    return wrapper

Now calling hello() prints:

hello!
hello took 0.0000 seconds

The wrapper decides what happens around the original function. It can run code before, run code after, or even skip the call entirely. This is why decorators are a common way to add logging, timing, caching or access checks without touching the function itself.

Notice the wrapper returns the value it gets back from func(). Forget that return and every decorated function silently returns None. That’s a bug that can take a while to track down.

Handling arguments

Our wrapper takes no arguments, so it only works with functions that take none.

Accept *args and **kwargs and pass them through, and the decorator works with any function:

def logtime(func):
    def wrapper(*args, **kwargs):
        start = time()
        val = func(*args, **kwargs)
        print(f'{func.__name__} took {time() - start:.4f} seconds')
        return val
    return wrapper

One thing to watch out for

The decorated function loses its identity. Check hello.__name__ and you get 'wrapper', not 'hello'. The docstring is gone too. This confuses debuggers and any tool that inspects functions.

The fix is functools.wraps, which copies the original function’s metadata onto the wrapper:

import functools
from time import time

def logtime(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time()
        val = func(*args, **kwargs)
        print(f'{func.__name__} took {time() - start:.4f} seconds')
        return val
    return wrapper

Now hello.__name__ is 'hello' again.

My advice is to add functools.wraps to every decorator you write. It costs one line and saves you from strange debugging sessions later.

Tagged: Python · All topics
~~~

Related posts about python: