How to use decorators in Python

Subscribe to my newsletter and never miss my upcoming articles

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, you're actually executing 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
    else:
        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

No Comments Yet