This content originally appeared on Level Up Coding - Medium and was authored by Sandipan Dutta
This post will show how we can use python decorators effectively.
Decorators in Python are a powerful design pattern that allows us to apply new functionality without modifying the existing structure of a Python object. As we saw in the first post of this series, we can pass classes as an argument, or it can be returned from a function, like that a Python function is also a first-class citizen, and we can perform all these operations on functions as well.
So far in this series
- Python Classes, Objects, and MRO
- Dataclasses
- Decorators (this post)
Deep Dive on Python Functions
Before we can understand what a decorator is, we need to know more about python function and how it works.
>>> def print_hello(name: str) -> None:
... print("Hello " + name)
...
>>> print_hello("Spiderman")
Hello Spiderman
>>>
In this example, we can see that the function print_hello() prints “Hello <input>” where the input is the argument that we pass to the function, and ultimately it returns None(as we are not returning anything).
There is a concept called Side effects, which basically means if a function has changed anything outside of its function definition, like changing the value of the arguments that have passed, changing the value of a global variable, or printing something in the stdout. In this case, our function doesn't return anything but it prints in the stdout, so it has side effects.
Python functions are objects too: First-class objects
As I stated in my last article, python functions are objects too, first-class objects, which means we can use functions, in python, like any other objects(int, float, string, list, tuple, etc.)we can pass them to another function as an argument, or can be returned as a value.
def greet(func: callable):
print(func.__name__)
greet(print_hello)
-------output------------
print_hello
In the above function, we are basically passing another function as its argument and then just printing its name. We can do this as well,
def print_hello(name:str) -> None:
print('Hello ' + name)
def greet(func: callable) -> None:
func("Thor")
greet(print_hello)
------------output--------------
Hello Thor
Notice that we don't use () after print_hello as we don't want to execute that, we want to pass the function, and inside greet we are actually calling the function. So basically we passed the print_hello as an argument to another function, just like any other object.
We can return functions from other functions too.
def talk(name: str) -> Callable[[], None]:
def greet() -> None:
print("Hi " + name)
#Notice no () here
return greet
f = talk("Batman")
print(f)
f()
----------output----------
<function talk.<locals>.greet at 0x000001BBCB7CB5B0>
Hi Batman
Here we are not passing a function to another function, but we are receiving another function from talk() and then we are executing it, which finally prints Hi Batman .
Inner functions
We can define a function inside another function too.
def outer_func() -> ():
message = 'Hi'
def inner_func():
print(message)
return inner_func
outer_func()() # try with single () and see what happens!
-------output----------
Hi
Here we can call the outer_func() but we can’t explicitly call inner_func() nor do we have access to any variables that are declared inside that function.
Decorators
Now we have revised the important concepts about python functions we can finally get into the topic we are here for, Python Decorators. A decorator is a callable that returns a callable. Basically, a decorator takes in a function, adds some functionality, and returns it.
Let's create a simple decorator.
First, we need to create a wrapper that will be inside a function, it will look similar to the above function, a function inside another one.
def my_decorator(func):
def wrapper():
print("Inside wrapper()")
func()
return wrapper
Here we are taking a function as an argument for the my_decorator() function. Inside my_decorator() we are defining another function that will be executing the passed function. Now to use this decorator we need to create another function and assign that to a variable and give it to my_decorator().
def say_something():
print("Hi!")
talk = my_decorator(say_something)
talk()
-------output----------
Inside wrapper()
Hi!
So what really happened here?
Basically, we are decorating the say_something() using my_decorator() and then storing the returned function as talk, but generally, we keep the returned function name the same as the original function, i.e. say_something()
For ease of use, we have an option to rewrite the whole thing like below.
@my_decorator
def say_something():
print("Hi!")
say_something()
-------output----------
Inside wrapper()
Hi!
The output remains the same.
Decorators with arguments
Now you might be thinking that we can use this decorator with any functions, but that is not true, if your decorator is not equipped to handle arguments that might be passed to the decorated function then it will surely fail, as you can see below.
@my_decorator
def say_something(name: str):
print(f"Hi {name}!")
say_something("Iron man")
-------output----------
Traceback (most recent call last):
File "C:\Users\SANDIPAN DUTTA\PycharmProjects\decorators\Decorators.py", line 69, in <module>
say_something("Iron man")
TypeError: my_decorator.<locals>.wrapper() takes 0 positional arguments but 1 was given
This is because the inner functionwrapper() does not take any argument.
To overcome this, we can use*args and **kwargs so that it can accept any number of arguments.
def my_decorator(func):
def wrapper(*args, **kwargs):
print("Inside wrapper()")
func(*args, **kwargs)
return wrapper
@my_decorator
def greet(name: str):
print(f"Hi {name}!")
greet("Iron man")
-------output----------
Inside wrapper()
Hi Iron man!
There’s still one problem that remains, the return value of the decorated function will be ignored in the current approach.
@my_decorator
def greet(name: str):
return f"Hi {name}!"
var = greet("Iron man")
print(var)
-------output----------
Inside wrapper()
None
This is because the wrapper() is not actually returning anything, to make this work we need to return the value of the decorated function.
def my_decorator(func):
def wrapper(*args, **kwargs):
print("Inside wrapper()")
return func(*args, **kwargs)
return wrapper
@my_decorator
def greet(name: str):
return f"Hi {name}!"
var = greet("Iron man")
print(var)
-------output----------
Inside wrapper()
Hi Iron man!
Real world example
Here is an example of a decorator that decides weather a user is logged in or in a flask application using session.
def login_required(f):
@wraps(f)
def wrap(*args, **kwargs):
if 'logged_in' in session:
return f(*args, **kwargs)
else:
flash("You need to login first")
return redirect(url_for('login_page'))
return wrap
@app.route("/logout/")
@login_required
def logout():
session.clear()
flash("You have been logged out!")
gc.collect()
return redirect(url_for('dashboard'))
Conclusion
In this tutorial, we learned, how to create a python decorator for our own custom requirements. In the next one, we will learn about Metaclasses. See you in the next one.
Advanced Python: Decorators was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.
This content originally appeared on Level Up Coding - Medium and was authored by Sandipan Dutta
Sandipan Dutta | Sciencx (2022-10-04T15:48:39+00:00) Advanced Python: Decorators. Retrieved from https://www.scien.cx/2022/10/04/advanced-python-decorators/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.