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.