Python decorators

Shaphil Mahmud
5 min readDec 13, 2019

@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) calls wrapper(5, 10)
  • wrapper calls print('Doing something') first.
  • then reaches this line val = func(*args, **kwargs) which is like val = 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__ block total has the value of val

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

--

--

Shaphil Mahmud

Fullstack Software Engineer. Lover of the backend. Friends with the Frontend. https://shaphil.me