Decorators are a powerful tool in Python for adding custom functionality to classes and functions. They are defined to classes and functions. They are defined a similar way to functions, but with the addition of a decorator keyword. In this article, we will be learning when?
to use a decorator and why
we need decorators with some few examples.
What are decorators?
A decorator is a function that is attached to another function or class. It takes one or more arguments, and its job is to modify the behavior of the original function or class.
Before we begin on how to create a decorator, we need to understand the fundamentals of functions before we dive right in. For more understanding about functions, you can visit this article Understanding functions in Python.
So let's create our functions.
def func1():
print("Called func1")
func1()
Results:
Called func1
Now rather than print func1 or calling func1, we will print func1 without he brackets and see what happens.
def func1():
print("Called func1")
print(func1)
Output:
<function func1 at 0x000002CB9EEA3E20>
You can see some random memory address, what this actually means is that: func1
is an object and since it is an object, we can pass it all round our program. To understand this better, let's create another function called func2
.
def func1():
print("Called func1")
def func2(f):
f()
func2(func1)
the function func2
has been created and it takes, and argument called f
which is called below the function. Now since func1 is an object and we can represent functions as object let us see the output below.
Called func1
You can see clearly that func1 is called, now how this works is that func1
is and object that represents the function func1()
and we can pass it through parameters we can store them in variables and so on.
The above example is the basic principle that we need to understand in python when it comes to functions.
Now we are going to see a kind of function called wrapper functions.
Let us see the example below.
def func1(func):
def wrapper():
print("Started")
func()
print("Ended")
return wrapper
Essentially, what this function is doing is that it has another function defined inside it called wrapper
, it prints out an output called started
, called the function func
as we have passed into our func1
and it will print out another value that says Ended
. This function returns the wrapper
function that has been defined inside our initial function.
Let's create another function called f
and what we want to do is always do this func1
functionality our initial function using the newly created function called f
.
def func1(func):
def wrapper():
print("Started")
func()
print("Ended")
return wrapper
def f():
print("Hello")
func1(f)()
and we have the output:
Started
Hello
Ended
This might be a little bit complicated but the reason we us parenthesis () to call our function is because the value wrapper
is a function. To understand this better, let's use the print to output the value of func1 so that you will see.
def func1(func):
def wrapper():
print("Started")
func()
print("Ended")
return wrapper
def f():
print("Hello")
print(func1(f))
and you can see the output:
<function func1.<locals>.wrapper at 0x000001C74A78ADD0>
so, this tells us that when you call func1
, you have a value that is actually equal to another function f
.
This Property is what is actually going to allow us to what is called decorate a function.
Using function aliasing, we can also have the same output as below, Let's see the example below.
def func1(func):
def wrapper():
print("Started")
func()
print("Ended")
return wrapper
def f():
print("Hello")
x = func1(f)
x()
output:
Started
Hello
Ended
What the above code actually does is that we have set x
which is a variable =
the function func1
which has the parament f
passed into it. So that is function aliasing, changing the name of function without changing its functionality.
You might be wondering why this article is on decorators and we are talking more about functions, well if you have understood what we have learned so far, then you pretty much understand decorators.
So, the code above with the line x = func1(f)
can be replaced with decorators. Let's see the code below.
def func1(func):
def wrapper():
print("Started")
func()
print("Ended")
return wrapper
@func1
def f():
print("Hello")
f()
What the code above does is to automatically write this line of code x = func1(f)
for us every time we call f
and we still have the same results.
Started
Hello
Ended
Now let's make a slide modification to our code to show you something wrong with the way we have been doing things
def func1(func):
def wrapper():
print("Started")
func()
print("Ended")
return wrapper
@func1
def f(a):
print(a)
f("Hi")
and that is: notice what happens when we add a parameter to our function, and rather than printing Hello
it will be printing a
and we can call f
with the value "Hi"
Traceback (most recent call last):
File "C:\Users\Bansikah\PycharmProjects\MyCompiler\decorator.py", line 15, in <module>
f("Hi")
TypeError: func1.<locals>.wrapper() takes 0 positional arguments but 1 was given
It give and error, notice that in our wrapper function.
def wrapper():
print("Started")
func()
print("Ended")
return wrapper
we call the function func()
without any parameter, so to fix this we will use args
and kwargs
in the parameters of our wrapper function. You must have probably seen this in your python lesson but if you haven't then i am going to write another article on that.
def func1(func):
def wrapper(*args, **kwargs):
print("Started")
func(*args, **kwargs)
print("Ended")
return wrapper
@func1
def f(a):
print(a)
f("Hi")
The reason we are using the args
and kwargs
is that we know that our wrapper function is supposed to contain some certain number of arguments and we don't know if those arguments are keyword
arguments or regular in-place
arguments all we know is that it needs to have some arguments. This actually allows us to have any number of arguments on any specific decorated function and this will work properly.
Output:
Started
Hi
Ended
And we have gotten the desired output we expected, and even if we decide to add another variable as below.
def func1(func):
def wrapper(*args, **kwargs):
print("Started")
func(*args, **kwargs)
print("Ended")
return wrapper
@func1
def f(a, b=9):
print(a, b)
f("Hi")
output:
Started
Hi 9
Ended
this continuous to work and that is what the args
and kwargs
can do for us.
The final thing we are going to talk about is returning values from decorated functions, before that I will just like to say this: if the variables or function names we are using are confusing to you, you can change them to your own names that you will understand better.
So far what we have been doing is printing the values in the decorated functions, but we would like to return those values instead? Well, this is pretty much easy to do, we are going to do this by creating another function as in the example below.
def func1(func):
def wrapper(*args, **kwargs):
print("Started")
val = func(*args, **kwargs)
print("Ended")
return val
return wrapper
@func1
def f(a, b=9):
print(a, b)
# f("Hi")
@func1
def add(x, y):
return x + y
print(add(4, 5))
What we have done in the above code is that we created a function called add
and it takes in two parameters x
and y
and also we created a variable val
in the wrapper function and returned the val
. You might be wondering that why didn't we just return the func(*args, **kwargs)
like this: return func(*args, **kwargs)
well it is better to do it this way.
output:
Started
Ended
9
So that is essentially how we can return values from a decorated function. In the above example you can see that you can use a decorator on more than one function.
For better understanding, let's see the applications of decorators in real world examples.
#Python decorators - Example 1
def before_after(func):
def wrapper(*args):
print("Before")
func(*args)
print("After")
return wrapper
class Test:
@before_after
def decorated_method(self):
print("run")
t = Test()
t.decorated_method()
output:
Before
run
After
Another example here, the timer decorator, a very common example maybe you have seen that before.
Python Decorators - Example 2
import time
def timer(func):
def wrapper():
before = time.time()
func()
print("Function took:", time.time() - before, "seconds")
return wrapper
@timer
def run():
time.sleep(2)
run()
output:
Function took: 2.0019941329956055 seconds
it measures how long a function takes to run.
And lastly the log()
decorator example.
# Python Decorators - Example 3
import datetime
def log(func):
def wrapper(*args, **kwargs):
with open("logo.txt", "a") as f:
f.write("Called function with" + " ".join([str(arg) for arg in args]) + "at" + str(datetime.datetime.now()) + "\n")
val = func(*args, **kwargs)
return val
return wrapper
@log
def run(a, b, c=9):
print(a + b + c)
run(1, 3, c=9)
output:
13
and we have this inside out log.txt
file.
Called function with1 3atCalled function with1 3at2023-06-05 01:00:47.423933
Called function with1 3at2023-06-05 01:02:38.287020
That was it about decorators in python a very advanced and broad topic in python, congrats if you understood everything and it is really going to help you in becoming an expert in python. Please do well to ask you valuable questions in the comments section, your reactions will really encourage me to continue writing articles like this. Happy Codingπ
Top comments (0)