Python Decorators

by Jarrett Retz November 15th, 2020
python programming decorator

Introduction

Python decorators have a mysterious presence. This is true for a developer that has some Python knowledge, and definitely the case for someone new to programming.

The first time I used a decorator in Python was with the Flask​ web framework, and I kept looking at the line of code like it didn't belong.

Now that I have created a few functions using the decorator syntax I feel more comfortable with seeing it in Python.

Why Use a Decorator?

Using a decorator is (so I've heard) a common pattern in programming languages. A decorator can be used as a wrapper, around another function, to modify the inputs before they are passed into the function. Additionally, the outputs of a function can be modified before they are returned.

The decorator's usefulness appears to come from the need to add to a function without modifying the core purpose (or source code) of the function. Some use cases include:

  • logging
  • type checking
  • data validation
  • gather metadata

Decorators can be reused. So it may be easier, and cleaner, to use a decorator function instead of trying to implement a logging function inside of your existing or future functions.

Decorators in Class Definitions

This scenario is a bit beyond a beginner's level. However, conceptually, it demonstrates how a decorator can be useful.

In Python, you can define classes using the class keyword. Classes have both properties and methods. Below, we define a basic class named Person.

class Person:
    def __init__(self, first, last):
        self.name = first + " " + last

    def __repr__(self):
        return self.name

The class is initialized with a first name and last name. The __init__ method initializes the class and sets the property name. The __repr__ method defines the method that will be called when we pass a Person object to the print() function.

>>> me = Person("Jarrett", "Retz")
>>> print(me)
# Jarrett Retz

Above, I simply created an object named me. Then I passed the object to the print function and the name property was printed as the output.

Classmethod

A class method receives the class as implicit first argument, just like an instance method receives the instance. Python Docs

We can use a classmethod to modify the inputs to the class before initialization. Take for example the case where we didn't factor in people who submit middle names to the person object. This would cause us to rewrite the class __init__ method!

Instead of doing that, we can extend the class to be initialized the original way, or initialized using our new classmethod.

class Person:
    def __init__(self, first, last):
        self.name = first + " " + last

    def __repr__(self):
        return self.name

    @classmethod # decorator
    def parse_name(cls, name):
        name_list = name.split(' ')
        first = name_list[0]
        last = name_list[-1]
        return cls(first, last)

In the code above, we added the classmethod parse_name. Inside of a class, decorators can be used to define classmethods or staticmethods with the syntax @staticmethod and @classmethod.

Using the decorator @classmethod we are given access to the Person class in the first argument: cls.

Let's now use this new method to create the me object again.

>>> me = Person.parse_name("Jarrett Danger Retz")
>>> print(me)
# Jarrett Retz

The parse_name function allows us to initialize the class using a second option that parses the name string for the first and last words (first and last name).

The decorator @classmethod allows us to extend the ways that we initialize the Person class.

Decorators Outside Classes

Next, let's extend a function result with a decorator.

First, let's define the function add in a Python file.

# example.py

def add(num1, num2):
    return num1 + num2

It adds two numbers together. However, we want the result to be returned so a human can interpret the results.

Let's create a decorator function in the same file to do this.

# example.py

def recite_answer(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return "The answer is " + str(result)
    return wrapper

def add(num1, num2):
    return num1 + num2



print(add(4, 5))

# Output: 
# 9

We created the decorator function recite_answer that:

  • Takes a function as the only argument
  • Defines an inner function wrapper that accepts all the arguments that it is passed (*args, **kwargs)
  • Calls the function it was passed with the arguments.
  • Adds the string "The answer is " to the result.
  • Returns the inner function wrapper

However, the recite_answer function has no effect on the add function definition. We need to use the decorator @ syntax.

# example.py

def recite_answer(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return "The answer is " + str(result)
    return wrapper

@recite_answer
def add(num1, num2):
    return num1 + num2


print(add(4, 5))

# Output
# The answer is 9

This seems wildly different than our use of decorators in the class definition. Nonetheless, these are two ways that decorators are used in Python.

Finding good examples and explanations for decorators can be difficult for beginners. I would say that it's an intermediate-to-advanced topic. Don't be discouraged if you don't understand it at first.

After using it in a library, or defining your own Python class, you will start to see how they work.


Have a thought about the article?

Send JRTS a message!

We'll use this email to respond to your message.

Contact