Introduction
A decorator in Python is a function that takes another function as its argument, and returns yet another function. Decorators can be extremely useful as they allow the extension of an existing function, without any modification to the original function source code.
A decorator gives a function a new behavior without changing the function itself. A decorator is used to add functionality to a function or a class. In other words, python decorators wrap another function and extends the behavior of the wrapped function, without permanently modifying it.
Prerequisite
Before we learn the concept of decorators we must know a few things about functions.
- In Python everything is an object and can be referenced using a variable name. Yes, even functions are objects with attributes.
- We can have multiple variables reference the same function object(definition)
Let’s see how a decorator works and start with an aside to introduce the logging concept.
For the sake of this specific example, you’re potentially already using logging in your Python code. If you don’t, or if you use the standard logging module, let me introduce you to a fantastic and easy to use new logging module called Loguru.
Loguru is simple to configure and use, and requires minimal setup code to start logging. The Python standard logging module is powerful and flexible, but can be difficult for beginners to configure. Loguru gives us the best of both worlds: you can start simple, and even have the bandwidth to drop back to standard Python logging for more complex logging scenarios.
As the name suggests Decorators are there to decorate other functions and give them new look and feel, exactly like a gift box.
A simple example for a decorator:
def outer(f):
def inner():
msg = f()
return msg.upper()
return inner
def func():
return 'hello! Peter'
func = outer(func)
print(func())
output: HELLO! PETER
In the above case outer() is our enclosing function and inner() is our nested function. Observe when we called the outer() function, we passed in another function named func() as the argument. This function is now available in the outer() function’s scope as f. And again we reassigned the func with the returned object of the outer() i.e. inner. As we have learnt earlier now func holds the reference to the function inner(). In the next line func() was called and it returned ‘HELLO! PETER’ as expected. Go through the code again to understand better.
However, Python has a better way to implement this, we will use @ symbol before the decorator function. This is nothing but syntactic sugar. Let’s see how it’s done
def outer(f):
def inner():
msg = f()
return msg.upper()
return inner
@outer
def func():
return 'hello! Peter'
func()
output: HELLO! PETER
We stacked the outer() above the function that needs to be decorated, this is the same as that we used before but more Pythonic. Let’s take another example where we want to find out the greater number in each tuple from a list of tuples.
def outer(func):
def inner(args):
return [func(var[0],var[1]) for var in args]
return inner
@outer
def func(a,b):
return a if a>b else b
print(func([(1,4),(5,3)]))
output: [4, 5]
Here, we could pass two variables to the func(). In this way, we were able to add new functionality to our function func(). func() originally could take only two arguments but now it can take a list of tuples.
Decorator as callable
Decorators allow you to define reusable building blocks that can change or extend the behavior of other functions. And they let you do that without permanently modifying the wrapped function itself. The function’s behavior changes only when it’s decorated.
Now what does the implementation of a simple decorator look like? In basic terms, a decorator is a callable that takes a callable as input and returns another callable.
The following function has that property and could be considered the simplest decorator one could possibly write:
def null _ decorator(func):
return func
As you can see, null _ decorator is a callable (it’s a function), it takes another callable as its input, and it returns the same input callable without modifying it.
Let’s use it to decorate (or wrap) another function:
def greet():
return 'Hello!'
greet = null_decorator(greet)
>>> greet()
'Hello!'
In this example we have defined a greet function and then immediately decorated it by running it through the null_decorator function. We know this doesn’t look very useful yet (It means we specifically designed the null decorator to be useless, right?) but in a moment it’ll clarify how Python’s decorator syntax works.
Instead of explicitly calling null_decorator on greet and then reassigning the greet variable, you can use Python’s @ syntax for decorating a function in one step:
@null_decorator
def greet():
return 'Hello!'
>>> greet()
'Hello!'
Putting an @null_decorator line in front of the function definition is the same as defining the function first and then running through the decorator. Using the @ syntax is just syntactic sugar, and a shortcut for this commonly used pattern.
Note that using the @ syntax decorates the function immediately at definition time. This makes it difficult to access the undecorated original without brittle hacks. Therefore you might choose to decorate some functions manually in order to retain the ability to call the undecorated function as well.
Python Decorators – Key Takeaways
1.Decorators define reusable building blocks you can apply to a callable to modify its behavior without permanently modifying the callable itself.
- The @ syntax is just a shorthand for calling the decorator on an input function. Multiple decorators on a single function are applied bottom to top (decorator stacking).
- As a debugging best practice, use the functools.wraps helper in your own decorators to carry over metadata from the undecorated callable to the decorated one.