Python Wrapper methods with Decorators
Hi, today I will be writing a bit about python decorators. First, for those who are not very familiar with it, I’ll explain it with my own personal experience of where I came across it the very first time. I am sure many of you have worked a bit on web development and so python, being one of the highly used languages that supports popular web frameworks(Flask & Django), tends to be the first choice for many. It’s okay if you have only seen either one, they both employ the use of decorators. In my case I have used both Flask and Django multiple times to create websites and APIs on the go when needed. Now, both of these frameworks use decorators to route incoming HTTP/HTTPS requests from the network traffic to methods that can be defined by us to perform certain tasks in response to the request
In the particular example shown below in a basic Flask code, we see that all GET requests to the URL “your-site-name.com/home” will be routed to the home method. This is defined in flask to respond to the request, which in this case will return a string that will be displayed on the Website.
1
2
3
@app.route("/home")
def home():
return "Hello, Flask!"
I won’t get into the flask details as much, but what must be seen is that the @app.route()
decorator is used to route URLs to python methods.
While this is one use case which I felt most people reading this must have seen, it isn’t the only place it’s used. A lot of people must have also seen it used when defining classes (e.g., @static methods) or in other python frameworks.
So what are decorators — python has these wonderful things called decorators which can perform method calls to wrap other methods with just the use of a single line. To understand this we will look at the concept of wrapper methods (& even wrapper classes). Many of you would have heard about it a lot in various other languages, where they use wrapper methods and classes for a variety of things.
So what are decorators —First things first what are wrapper functions, how and why they work in python with the use of decorators. In python, everything is an object made by a class. That means variables, data, methods and also classes (made by metaclasses- won’t get into it here) are all objects. Hence, just as we define instances of our own classes and move it around just like a variable, a method can be used as a variable too. Furthermore, due to this object nature it also allows us to pass methods as parameters to other methods. Hence, we can see below that we are defining a method deco
that takes a method f
as an argument. This method also has a method wrapper
defined within itself, and returns it just like a variable (rather an object). Python has these wonderful things called decorators, which can perform method calls to wrap other methods with just the use of a single line. To understand this we will look at the concept of wrapper methods (& even wrapper classes). Many of you would have heard about it a lot in various other languages, where they use wrapper methods and classes for a variety of things.
1
2
3
4
5
6
7
8
9
def deco(f):
def wrapper():
print("This function has started")
f()
print("This function has ended")
return wrapper
def func():
print("hi from func")
In this case what would happen if we called the method func
- the output would be as below:
CALL
1
func()
OUTPUT
1
hi from func
But what if we passed func
to deco
then what would the output be? Note - w
is the returned wrapper
method from deco and we then call this new method w
instead of func
CALL
1
2
w=deco(func())
w()
OUTPUT
1
2
3
This function has started
hi from func
This function has ended
As you can see, the method deco
essentially applied wrapper
around our original method func
. And so now the method executes as is but with the wrapper
method adding some few more lines of code around it, essentially wrapping our original method — hence the name wrapper functions.
So how do decorators come here?
The issues with the above style of doing things is that we do not wish to add invocation lines, i.e., we do not want to have to call both methods. In large production codes many a time we simply define methods to be called in other modules, having the code in the above fashion would mean having to remember and invoke both methods properly. This makes it very unfriendly to use.
We can simply apply the line @deco
above the method func
and it will automatically use deco
to wrap func
with the method wrapper
. This saves us from writing extra lines of code or even invoking the method. Hence, our new code will look like this :
1
2
3
4
5
6
7
8
9
10
def deco(f):
def wrapper(f):
print("This function has started")
f()
print("This function has ended")
return wrapper
@deco
def func():
print("hi from func")
CALL
1
func()
OUTPUT
1
2
3
This function has started
hi from func
This function has ended
But then a question arises what if the functions that we send to the wrapper function has parameters, should we define the wrapper
method to take in those inputs too as shown below?
1
2
3
4
5
6
7
8
9
10
11
#bad way to handle
def deco(f):
def wrapper(num1,num2):
print("This function has started")
f(num1,num2)
print("This function has ended")
return wrapper
@deco
def add(num1,num2):
print("hi from func")
The answer to that is an obvious no. Here is why — what if we want to use the wrapper in a modular fashion, where it can be used with another method that has a different signature? The script shown above is hard coded and cannot be reused, hence while it’s not wrong it is not modular enough to extend to all the methods. Before I get into the solution for this, I’d like to also point out, what if the func
method has a return value? How will the wrapper deal with the return values. The following script will take care of the above issues.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def deco(f):
def wrapper(*args,**kwargs):
print("This function has started")
ret=f(*args,**kwargs)
print("This function has ended")
return ret
return wrapper
@deco
def binary_add(num1,num2):
print("binary sum")
return num1+num2
@deco
def ternary_add(num1,num2,num3):
print("ternary sum")
return num1+num2+num3
The script above now can take any arbitrary method with any kind of function signature and use the args and kwargs of that method itself, so we don’t need to hard code it. The return value ret
is returned too. As shown above, the deco
method is now capable of accepting any method with any signature (in our case binary_add
and ternary_add
) and also returns the appropriate values. Now we can say the script’s perfect.
CALL
1
2
print(binary_add(1,1))
print(ternary_add(1,1,1))
OUTPUT
1
2
3
4
5
6
7
8
This function has started
binary sum
This function has ended
2
This function has started
ternary sum
This function has ended
3
This is essentially how the Flask framework also uses decorators to wrap the app.route()
method (route
method from the flask app
class) around the method we define. While we focus on the web service function, the wrapper class in the decorator handles the network connections and routing.
Some other use cases where we can create our own handy wrapper methods with decorators are when we want to time a function or in large projects make custom tests for every method when called.
eg :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import time
def timer(func):
def wrapper(*args, **kwargs):
start = time.time()
rv = func()
end = time.time()
total = end-start
print("\n\n\nTime To Run : ", total)
return rv
return wrapper
@timer
def test():
time.sleep(1)
test()
Well that’s all about wrapper methods with decorators for now. I hope you found this as exciting and interesting of a topic as I did. It definitely comes very handy with larger projects to simply add lines of code to a method without having to explicitly modify it, and also very useful to create abstracted modules and frameworks like Flask.
Thanks for reading this article, any questions can we asked below with your GitHub account.