Decorators
In python, functions are first class objects. This mean that you can pass them as arugments into other functions. To give an example of this, let's create the most basic function imaginable:
def funk():
print("called funk")
Consider what happens when you run the below lines of code:
funk()
print(funk)
You will get two things:
>>> called funk
>>> <function funk at 0x7f0554f21800>
The first one is the result of the function and is what we typically would want. The second is the address in memory at which our funk function is stored. This means it is an object just like anything else in python and can be called as an argument in another function.
Let's call it within a second function to show what we are looking at:
def funk():
print("called funk")
# funk2 takes a function as an argument
def funk2(f): # the function is passed as f
f() # and called with f()
funk2(funk)
We are now passing funk
as an arugment into funk2
. Do you think this will work?
>>> called funk
Of course it does. We are passing the function funk
into funk2
as f
and then calling it with the line f()
.
Wrapper Functions
Now we are going to create a wrapper function:
def f1(func):
def wrapper():
print("started")
func()
print("ended")
return wrapper
def f2():
print("hello")
Ideally, our output will first print started
, then call whatever function is passed into f1
, and finally print ended
.
So what happens when we call it like so:
f1(f2)
We get:
>>>
That's right, nothing! Think about what we wrote here. The retun value of f1 function is the wrapper function, not the result of that function.
So to get anything out of it we will need to do something a little bit silly:
f1(f2)()
We could save this as a new function like so:
x = f1(f2)
x()
So what is x
? It is a new function where in we passed f2
into f1
. So here is where the decorator comes in.
We can write this like so:
def f1(func):
def wrapper():
print("started")
func()
print("ended")
return wrapper
@f1
def f2():
print("hello")
f2()
Args and Kwargs
That's all well and good but what if we want f2
to have an argument? As it is currently written, f1
cannot accept a function with arguments as an argument. But if we add the following it can:
def f1(func):
def wrapper(*args, **kwargs):
print("started")
func(*args, **kwargs)
print("ended")
return wrapper
@f1
def f2(text):
print(text)
f2("say hi")
Example: How long does a function take to run?
So basically, a decorator allows us to inject different functions into a larger function. When would this be useful? What if we wanted to see how long it takes for a function to run?
import time
# the decorator function
def timer(func):
def wrap():
before = time.time()
func()
print("function took", time.time() - before, "seconds")
return wrap
# the decorated function
@timer
def run():
time.sleep(2)
run()