Python decorators
@decorators explained
In Python, decorators are functions that take other functions as parameters and then decorate them. By decorating the decorator could enhance the functionality of the function. It can do so by modifying the passed-in function or not.
Let’s see some examples to get an idea of how they work.
The following does not do anything but illustrates the idea of a decorator
def decorate(func):
def wrapper():
print(f'decorating function: {func.__name__}')
func()
print('This could be some functionality')
return wrapper
def say():
print('Hello')
@decorate
def greet():
print('Welcome stranger')
if __name__ == "__main__":
greet()
say = decorate(say)
say()
Here, decorate
is a decorator which takes a function as an argument, defines an inner function wrapper
that does all the work, and returns it. wrapper
performs the additional tasks you want your func
to have and calls it if necessary. One thing to notice here is the difference between how say
and greet
is called. The call greet()
is just a syntactic sugar for doing essentially the same thing done with say
. Thus greet()
is equivalent to doing greet = decorate(greet)
first and then greet()
.
This is a pretty useless decorator so it may not be very clear what a decorator is or why you might want one.
Let’s move on to the popular timer example. Let’s say you want to measure the run time of a function. Obviously what you can do is get the time before you call the function then get the time again after your function call ends and do the simple math to get the execution time. Like this
start = current_time()
my_function()
end = current_time()
run_time = end - start
Now imagine you have to do this for hundreds of functions. It will get tedious before you reach a hundred. What you can do however is create a decorator for this purpose. Here is the famous timer decorator
import time
def timer(func):
def wrapper():
start = time.time()
func()
end = time.time()
print(f'runtime: {end - start}')
return wrapper
@timer
def long_loop():
# 10^8 loop, > 1 sec
for i in range(100000000):
pass
if __name__ == "__main__":
long_loop()
Neat huh? You are doing the same thing but not inside the __main__
block. Next time you want to measure the run time performance of a function, just add @timer
it before it.
Now let’s see another example. This decorator keeps track of how many times your function was called.
def count(wrapped):
def inner(*args, **kwargs):
inner.counter += 1
return wrapped(*args, **kwargs)
inner.counter = 0
return inner
@count
def func():
pass
if __name__ == "__main__":
print(func.counter)
for i in range(100):
func()
print(func.counter)
The inner.counter
part may seem weird at first but remember that functions in Python are objects and like all objects, they too can have properties. So after declaring the inner
function you add a counter
property to it and assign it the value 0. The inner
function is not called until func()
is called inside the loop. When func()
is called, counter
is incremented. Before that func = inner
so calling func.counter
is like calling inner.counter
.
Now let’s say you want to return a value from the function you want to decorate. In that case, you just have to call the passed-in function inside your inner
or wrapper
function and return the results. Here’s an example
"""
Return values from decorated functions
"""
def deco(func):
def wrapper(*args, **kwargs):
print('Doing something')
val = func(*args, **kwargs)
print('Now doing something else')
return val
return wrapper
@deco
def add(x, y):
return x + y
def sub(x, y):
return x - y
if __name__ == "__main__":
# the following call is equivalent to deco(add)(5, 10)
# add is replaced by deco(add) in actual call
total = add(5, 10)
print(total)
diff = deco(sub)
result = diff(5, 10)
print(result)
diff2 = deco(sub)(12, 13)
print(diff2)
Here, inside the wrapper
function you save the results of the passed-in function func
in a variable val
and return it. Notice how the wrapper
function makes use of *args
and **kwargs
. Remember how the call to add()
breaks down to add = deco(add)
? At this point add
is just another name for wrapper
since that is what deco
returns. So the call add(5, 10)
is equivalent to wrapper(5, 10)
. If we didn’t have *args, **kwargs
in the signature of wrapper
it would fail to receive parameters 5 and 10. So to break it down,
add(5, 10)
callswrapper(5, 10)
wrapper
callsprint('Doing something')
first.- then reaches this line
val = func(*args, **kwargs)
which is likeval = add(*args, **kwargs)
- the positional values 5 and 10 are passed to
add
through*args
- then the statement
print(‘Now doing something else’)
is executed - and then the value
val
is returned thus in__main__
blocktotal
has the value ofval
I hope this explanation was detailed enough in portraying a better understanding of how decorators work.
Now let’s see another example. If you work with the web, you may have seen the login_required
decorator. This is just a simpler version to give you an idea
def login_required(api_func):
def wrapper(login):
if login:
print('login successful')
api_func()
else:
print('403 unauthorized')
return wrapper
def check():
print('GET /api/check')
@login_required
def profile():
print('GET /api/user/profile')
if __name__ == "__main__":
check()
api = login_required(check)
api(True)
print()
api(False)
print()
profile(True)
print()
profile(False)
To make things simple we pass in a boolean value login
to the wrapper
based on which we decide if the call to the API should be allowed or not.
What if you want to pass in parameters to the decorator itself? Here’s an example of how you can do that.
def skipIf(conditional, message):
def dec(wrapped):
def inner(*args, **kwargs):
if not conditional:
return wrapped(*args, **kwargs)
else:
print(message)
return inner
return dec
@skipIf(True, 'Test ommitted')
def skip_test():
print("will not print")
def add_test():
print("testing method")
@skipIf(False, "N/A")
def run(a, b):
print(a, b)
if __name__ == "__main__":
skip_test()
add_test = skipIf(False, 'Add test ommitted')(add_test)
add_test()
run('Name', 'Required')
This is a simpler version of the skipif
decorator from the pytest
framework. This one includes a few other examples from the PyCon2014 talk on decorators. I’ll link all the sources at the end of this post.
Let’s have a look at one last example. This one is similar to the skipIf
example but may look familiar to you if you ever used Flask. This is a simpler reproduction of the @app.route()
decorator.
def get_details_url(path, obj):
replacements = ('<int>', )
for replacement in replacements:
if replacement in path:
path = path.replace(replacement, str(obj))
return path
def route(path):
def wrapper(wrapped):
def inner(*args, **kwargs):
request = { 'path': '', 'user': 0}
# assuming the first item in args is the request
if len(args) > 0:
request = args[0]
details_path = get_details_url(path, request['user'])
print(details_path, request['path'])
if request['path'] == details_path:
return wrapped(*args, **kwargs)
else:
print('Path mismatch!')
return inner
return wrapper
@route('/api/users/<int>')
def users(request):
user = request['user']
path = request['path']
print('User details')
print(f'User: {user}, Path: {path}')
if __name__ == "__main__":
request = {
'path': '/api/users/1',
'user': 1
}
users(request)
There are no validation or error checks. I just tried to get as close as possible with as little as possible.
I hope this helps. There might be unintentional mistakes in the post. If you happen to find one I would be happy if you pointed that out by commenting.
Sources
- PyCon2014 talk by Colton Myers on Decorators — https://youtu.be/9oyr0mocZTg
- Slides — https://speakerdeck.com/pycon2014/decorators-a-powerful-weapon-in-your-python-arsenal-by-colton-myers
- Python 101 by Michael Driscoll — https://leanpub.com/python_101