Python decorators with examples
Decorators are a complicated but extremely useful feature of Python. They are basically functions that wrap around other functions, in…
Decorators are a complicated but extremely useful feature of Python. They are basically functions that wrap around other functions, in order to modify or complement their behavior (from the outside, at least). Here is a very simple example:
from functools import wraps
def only_even(func):
"""This is the decorator. When declared with '@', this function
will be called with the decorated function as an argument.
It should return another callable, which will "take the place" of
the original function.
"""
@wraps(func) # This is optional but recommended
def new_function(*args, **kwargs):
"""Because it is declared inside another function, every time
only_even() is called, this becomes a new "pointer".
It will be called in the place of the decorated function.
"""
# Should call the original function somewhere
result = func(*args, **kwargs)
if result % 2 == 0:
return result
else:
return result - 1
return new_function
# Now comes the usage.
# The function add_two() is decorated with @only_even.
# A pointer to add_two is passed as an argument to only_even (as func).
@only_even
def add_two(n1, n2):
"""Adds two numbers."""
return n1 + n2
The function add_two
simply returns the sum of two numbers. But the decorator @only_even
modifies that behavior: it checks if the result is even — if it is, no modification is done, but if it is odd, it decreases it by 1 to make it even.
Note: That is an inappropriate name for the function, and is meant for explanation only. If you decorate a function with a decorator that changes its purpose, you should probably reflect that in its name and docstring — since you cannot “undecorate” it. In this example, a better name would be even_add_two
or something like that.
To clarify the nomenclature:
only_even
is the decorator: it’s a callable that is applied to another via the decorator operator@
. It must return a callable, which is the wrapper (although in certain cases you could return the same function).new_function
is the wrapper function. It’s the function that replaces the decorated function (in this caseadd_two()
). For this reason, it’s usually simply namedwrapper
. It is usually defined inside the decorator to make use of Python’s closures (the context inside a function).
Note how I recommended decorating it withfunctools.wraps(func)
. This decorator will simply copy the metadata (name, module, docstring) from the original function and apply it to the new one.add_two
is the decorated function.
Note: a “callable” in Python is either a function or an object that implements the __call__
method.
Another way of declaring the function above is this:
def add_two(n1, n2):
return n1 + n2
add_two = only_even(add_two)
Note how the “variable” add_two
is replaced with the return value of the decorator. That’s what happens when you decorate a function, and that’s why the return value must be a callable.
The result of this code is the following:
>>> add_two(2, 2)
4
>>> add_two(2, 3)
4
>>> add_two(2, 4)
6
>>> add_two(2, 5)
6
A note about the wrapper parameters
You may have noticed this example wrapper takes as arguments *args
and **kwargs
. This is the Python notation for “unnamed” and “named” arguments. The names args and kwargs are mere conventions: you could change them to *unnamed, **named
and it would be fine. The types of these variables are tuple and dict, respectively.
>>> add_two(2, 4) # args=(2, 4) kwargs={}
>>> add_two(n1=2, n2=4) # args=() kwargs={'n1': 2, 'n2': 4}
>>> add_two(2, n2=4) # args=(2,) kwargs={'n2': 4}
A wrapper doesn’t have to take these arguments. It could take the same parameters as the original function — it all depends on the implementation.
Multiple decorators
A function can be decorated with multiple decorators. This relatively common setting can be useful for a number of reasons, but always brings about a question: In which order do they run?
Let’s add a second decorator to our example above:
from functools import wraps
def square_result(func):
"""Returns the square of the original result."""
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs) ** 2
return wrapper
def only_even(func):
"""Rounds the result down to the nearest even number."""
@wraps(func)
def new_function(*args, **kwargs):
result = func(*args, **kwargs)
return result if result % 2 == 0 else result - 1
return new_function
@square_result
@only_even
def add_two(n1, n2):
return n1 + n2
The declaration above is the same as the following:
add_two = square_result(only_even(add_two))
Please note how the order of declaration (square_result
first, then only_even
) is the same in both. This means that only_even
is called first, because it is the innermost call. Its result is then passed to square_result
. Therefore, during declaration (i.e., before calling the function), the order of execution is bottom-up.
When the function is called, however, the wrapper for square_result
is called first, which in turn calls the wrapper for only_even
. Therefore the order of execution of the wrappers is top-down.
This can become apparent by adding a few print statements:
from functools import wraps
def square_result(func):
"""Returns the square of the original result."""
print('declaring square_result')
@wraps(func)
def wrapper(*args, **kwargs):
print('called wrapper for square_result')
return func(*args, **kwargs) ** 2
return wrapper
def only_even(func):
"""Rounds the result down to the nearest even number."""
print('declaring only_even')
@wraps(func)
def new_function(*args, **kwargs):
print('called wrapper for only_even')
result = func(*args, **kwargs)
return result if result % 2 == 0 else result - 1
return new_function
@square_result
@only_even
def add_two(n1, n2):
print(f'called add_two({n1}, {n2})')
return n1 + n2
# outputs:
# declaring only_even
# declaring square_result
print(add_two(2, 3))
# called wrapper for square_result
# called wrapper for only_even
# called add_two(2, 3)
# 16
The explanation for the result of 16 is this:
add_two
is actually a pointer tosquare_result
wrapper.square_result
wrapper callsonly_even
wrapper (new_function
).new_function
calls the original function with argumentsadd_two(2, 3)
. The result, naturally, is 5.only_even
rounds it down to 4 and returns it.square_result
squares it, making it 16.
If we were to change the order of the decorators we would get:
@only_even
@square_result
def add_two(n1, n2):
print(f'called add_two({n1}, {n2})')
return n1 + n2
print(add_two(2, 3))
# declaring square_result
# declaring only_even
# called wrapper for only_even
# called wrapper for square_result
# called add_two(2, 3)
# 24
Decorators with parameters
You may have noticed that some decorators accept parameters that further control its behavior. Here’s an example:
@power_result(3)
def add_two(n1, n2):
return n1 + n2
add_two(2, 3)
# 125
In the example above, power_result
is a decorator that takes an argument, which is the exponent of the power operation. Because it takes an argument instead the function itself, to implement this decorator, we have to make a function that returns a function that returns a function. Sound complicated? It is, until you get the hang of it. Let’s see:
def power_result(exponent):
"""Decorator that powers the result to the given exponent.
This is actually a 'decorator generator'. It will return a
decorating function.
"""
def decorating_function(func):
"""The actual decorator that gets called with the func argument."""
@wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result ** exponent
return wrapper
return decorating_function
Notice that the two outmost functions return functions — and again, because of Python closures, the arguments for the outer function are accessible from the innermost functions. Quite the nifty trick, isn’t it?
To make matters a bit more complicated, some decorators may optionally accept arguments — which means that, when declared without ()
, they simply behave as a decorator, and when called with arguments, they behave as a “decorator generator”. For example:
def power_result(exponent_or_function):
if callable(exponent_or_function):
# This means it's being called without arguments.
# Here we're making it the same as calling it with power=2,
# but the implementation could be different.
return power_result(2)(exponent_or_function)
# Everything else is the same
def decorating_function(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result ** exponent_or_function
return wrapper
return decorating_function
@power_result
def add_two(n1, n2):
return n1 + n2
add_two(2, 3)
# 25 (5 ** 2)
@power_result(4)
def double(n):
return n * 2
double(2)
# 256 (4 ** 4)
Built-in decorators
Python provides three special built-in decorators for method manipulation: @property
, @classmethod
, @staticmethod
. Because they are built-in, there is no need to import them from any module; they are always available.
@property
This decorator transforms a parameter-less class method into a read-only class attribute, which in Python is called — well, a property. Properties are accessed from a class without arguments()
, but their access triggers a function that will return its value. For example:
class A:
def __init__(self):
self.items = []
@property
def count(self):
"""Returns the number of items in the list."""
return len(self.items)
a = A()
print(a.count)
# 0
a.items.append('Bread')
print(a.count)
# 1
Every time you access a.count
, you are actually calling a function with the object being the only argument. It would be wise to make that function not modify anything, unless you have a very good reason to.
Properties can be made writeable by declaring another method that sets the value of the property. Another example:
class Person:
def __init__(self, first_name='', last_name=''):
self.first_name = first_name
self.last_name = last_name
@property
def full_name(self):
"""Joins the string to make the full name."""
return f'{self.first_name} {self.last_name}'.strip()
@full_name.setter
def full_name(self, value):
"""Sets first and last name by splitting the string."""
self.first_name, _, self.last_name = value.partition(' ')
# Also optional, and less frequently used
@full_name.deleter
def full_name(self):
self.first_name = self.last_name = ''
me = Person('Augusto', 'Men')
print(me.full_name)
# Augusto Men
del me.full_name
print(me.first_name)
# -blank-
print(me.last_name)
# -blank-
you = Person()
print(you.full_name)
# -blank-
you.full_name = 'Awesome Fellow'
print(you.first_name)
# Awesome
print(you.last_name)
# Fellow
Note: If you want to decorate a property with other decorators, you should probably make @property
the topmost one, because remember, the result is passed to the decorator above, and property returns a pseudo-function that cannot be called with arguments. See above in Multiple decorators.
@classmethod
Methods are functions that are defined inside a class and whose first parameter is self
— an instance of the class that is calling the method. However, some functions more appropriately belong to the class instead of a specific instance. We decorate these methods with @classmethod
, and Python will make the first parameter the class that’s calling it instead.
class A:
@classmethod
def name(cls, prefix='<', suffix='>'):
return f'{prefix}{cls.__name__}{suffix}'
print(A.name())
# <A>
instance = A()
print(instance.name('[', ']'))
# [A]
class B(A):
pass
print(B.name())
# <B>
As you can see, the class method can be called either by the class directly or by an instance of it, but the argument cls
always refers to the class definition. It is also inherited by subclasses.
Class methods can be overridden and they can call their super:
class C(A):
@classmethod
def name(cls, *args, **kwargs):
return super().name(*args, **kwargs) + '!'
print(C.name())
# <C>!
@staticmethod
Unlike class method, static methods are simply functions that have no ownership by a class of by an instance. They are simple functions, and are usually placed inside a class just for organization reasons. They must however be accessed via a fully-qualified name like ClassName.function()
or instance.function()
.
import sys
class D:
@staticmethod
def is_py38plus():
return sys.version_info >= (3, 8)
print(D.is_py38plus())
# True
Static methods cannot call their “super” — but they can be replaced in subclasses.
Decorated classes
Decorators can also be applied to classes, with the same rule: They must return a class — possibly the same one, in order to not invalidate the class that’s being declared. This is useful for registration processes in your application. For example:
registered_classes = []
def register(klass):
"""Class decorator that registers a class and returns it.
The name 'class' is a reserved word, so use klass instead.
"""
if klass not in registered_classes:
registered_classes.append(klass)
return klass # returns the class without modifying
@register
class A:
pass
@register
class B:
pass
for klass in registered_classes:
print(klass.__name__)
# A
# B
Some class decorators modify attributes, add / override methods, and some will even subclass it before returning.
Conclusion
Decorators are a versatile and very commonly-used feature of Python. Knowing how and when to use them can be tricky, and building your own decorators may be even trickier, but once you understand the mechanics of it, they will save you a lot of time and will be extremely useful to reduce code repetition.