8. Decorators#

This is likely the simplest syntactic sugar you’ll see today, but maybe one with some of the furthest reaching consequences. Let’s say you have a bit of code that looks like this:

def f(): ...
f = g(f)

So g is a function that takes a function and (hopefully) returns a function, probably a very similar one since you are giving it the same name as the old “f”. In Python 2.5, we gained the ability to write this instead:

@g
def f(): ...

That’s it. The thing after the @ “decorates” (or transforms) the function you are defining and the output is saved with the name f.

def bad_decorator(func):
    print(f"You don't need {func.__name__}!")
    return 2


@bad_decorator
def f(x):
    return x**2
You don't need f!
f
2

Okay, so that’s useless (well, except for the printout, which could be good for logging). What could this be used for? Turns out, almost anything. Having a syntax for “modifying” a function (it also works on methods and classes, too) is fantastic, and lets you think in a different way.

There are several decorators in builtins, property, classmethod, and staticmethod. For example:

class BagOfFunctions:
    @staticmethod
    def f(x):
        return x**2

What’s missing from the above function? Self! It’s static, it doesn’t need an instance, or even the current class.

BagOfFunctions().f(2)
4
BagOfFunctions.f(2)
4

The decorator took our method and added the correct handling to it so it works with or without an instance.

If the thing after the @ is called, this is called a decorator factory; it’s exactly the same as above, just slightly more unusual in structure to what you normally see:

def f...
f = g()(f)

# same as

@g()
def f...

You can also nest decorators.

You could have a rate decorator, which causes a function to wait after completing so that it always takes the same amount of time. You could have a logging decorator, which prints to a log every time the wrapped function is called. There are quite a few decorators in the standard library; we’ll see more later, but here are a couple interesting ones:

8.1. Examples#

8.1.1. Least Recently Used Caching (LRU)#

This is all you need to implement a cache based on the input arguments. When you call this again with recently used arguments (the cache size is adjustable), it pulls from a cache instead of rerunning the function.

import functools
import time


@functools.lru_cache
def slow(x: int) -> int:
    time.sleep(2)
    return x
slow(4)
4
slow(4)
4

8.1.2. Single Dispatch#

Another magical decorator is functools.singledispatch, which lets you simulate type based dispatch (but only on the first argument) from other languages:

@functools.singledispatch
def square(x):
    print("Not implemented")


@square.register
def square_int(x: int) -> int:
    return x**2


@square.register
def square_str(x: str) -> str:
    return f"{x}²"
square(2)
4
square("x")
'x²'

8.1.3. Other functools decorators#

There’s also @functools.total_ordering, which when applied to a class, fills in the missing comparison operators from the ones that are already there (==, !=, <, <=, >, >= can be computed from just two functions)

And @functools.wraps is a decorator that helps you write decorators that wrap functions. Also see decorator and the newer, fancier wrapt libraries on PyPI.

8.1.4. Dataclasses#

Another use case we’ve briefly seen is dataclasses from Python 3.7:

from dataclasses import dataclass


@dataclass
class Vector:
    x: float
    y: float

This @dataclass is taking the class you pass in, converting the class annotations to instance variables, making an __init__, __repr__, and much more. When you are viewing a class as data + functionality, this is a very natural way to work.

Vector(1, y=2)
Vector(x=1, y=2)

If you need Dataclasses in 3.6, there’s a pip install dataclasses backport, and this was based on the popular attrs library, which is much more powerful and can do all sorts of tricks, like validate and transform values.

8.1.5. Third party: Click#

Click is a package that lets you write command line interfaces using decorators on functions:

import click

@click.command()
@click.option('--count', default=1, help='Number of greetings.')
@click.option('--name', prompt='Your name',
              help='The person to greet.')
def hello(count, name):
    """Simple program that greets NAME for a total of COUNT times."""
    for x in range(count):
        click.echo('Hello %s!' % name)

if __name__ == '__main__':
    hello()

We’ll see more decorators, don’t worry!