How to use decorators in Python

How to use decorators in Python

ยท

4 min read

You may have already found a decorator in some Python code. For example, the following is commonly used to create static methods within a class:

class Person:
    @staticmethod
    def some_static_method():
        pass

But what does really do a decorator and how to use it in order to improve your coding experience in Python?

The concept โ˜‘๏ธ

A decorator is a function that takes another function as an argument, executes it within an internal function, and returns the internal one.

A very confused man

This concept is clarified in the example below ๐Ÿ˜…:

def decorator(func):
    def do_something(*args, **kwargs):
        # do what you need to do here
        print("I'm doing something here...")
        # execute the function, returning its result
        return func(*args, **kwargs)
    return do_something

@decorator
def my_function(*args):
    print("Hi there! I'm a function :)")

What is happening there?

Every time you call my_function, the program actually executes do_something, the inner function of the decorator! It allows you to perform some action before running the decorated functions themselves.

Ok, but... Please, give me some better examples ๐Ÿคทโ€โ™‚๏ธ

So, here we go!

Counting time

Let's suppose that you want to know how long a function takes to run. A simple way to do it is by creating a decorator that acts as a stopwatch:

import time

# decorator
def stopwatch(func):
    # inner function
    def run(*args, **kwargs):
        # start counting
        start = time.time()
        # run function
        response = func(*args, **kwargs)
        # stop counting
        end = time.time()
        # print the timing
        print(f"Timing for running {func.__name__}: {end - start}")
        # return the executed function value
        return response
    # it runs every time you call a function decorated by @stopwatch
    return run

Let's test it with some function:

@stopwatch
def test():
    print('Test me!')

test()

The output will be something like this:

Test me!
Timing for running test: 5.364418029785156e-05

Storing values

In this example, we'll be greeting some people, but we don't wanna greet someone more than a single time. Thus we'll use a decorator for storing the names that were already passed as arguments for our greeting function, avoiding that kind of problem.

def just_one_time(func):
    # list of greeted people
    names_list = []

    # inner function
    def check_name(*args, **kwargs):
        name = args[0]
        # Program stops if name already exists in list
        if name in names_list:
            raise Exception(f"{name} was already greeted!")
        # Else, list is updated and the decorated function runs
        else:
            names_list.append(name)
            return func(*args, **kwargs)

    return check_name

@just_one_time
def greet(name):
    print(f"Welcome {name}!")

greet('George')
greet('Samantha')
greet('Samantha') # Exception here

The above code will be executed until we try to greet Samantha twice. ๐Ÿ˜‰

Decorators with custom arguments ๐Ÿคฒ๐Ÿป

It would be nice to pass arguments to the decorators because that way we could make them respond according to the needs of each function.

Wait... We can do it! ๐Ÿ˜ƒ

Little girl celebrating

All we need to do is wrapping our old decorator inside a new function. See how it works:

# new decorator
def decorator(*args, **kwargs):
    # our old decorator
    def inner_wrapper(func):
        # main function
        def inner(*args, **kwargs):
            # do something
            return func(*args, **kwargs)
        return inner

    # get the function and run inner_wrapper
    if len(args) == 1 and callable(args[0]):
        func = args[0]
        return inner_wrapper(func)

    # use custom arguments
    print(args, kwargs)

    return inner_wrapper

Now, we can use our new decorator this way:

@decorator('simple argument', key_argument='some value')
def do_something():
    ...

Keep it in mind ๐Ÿง 

In the example above, our decorator runs along with the rest of the code (a single time), while its inner function runs every time we call do_something().

Bonus

Another cool fact about decorators is that you can use more than one for each function:

@first
@second
def my_function():
    ...

Conclusion

  • Feel free to use decorators when you intend to prepare the way for a function every time it's called.
  • Decorators offer a good way to control what happens when you call a method.
  • Nothing prevents you from using them in class methods, as we see at the start of this article. ๐Ÿ˜‰

I hope this brief explanation has been helpful to you!

Buy me a coffee