How to Use Decorators to Validate Input

How to Use Decorators to Validate Input

Learn how to use decorators to validate user input before communicating with a device
Aquiles Carattino 2018-03-12 Decorators Python Tricks Validation Data

Python is rich in resources that can shorten the time it takes to develop new programs and simplify repetitive tasks. Decorators are one of such elements but more often than not they are not considered by less experienced developers. Adding decorators to the syntactic toolbox can be of great use in different contexts, and in this article, we are going to discuss how can they help you when communicating with a device. The example code can be found in our Github repository.

Decorators in Python are nothing more than functions that take as arguments other functions. They appear with an @ in front of them right above a function (or a method within a class). Let's quickly recap how functions work in Python and how to use them. Imagine you want to compute the average of two values. We can develop a function for it:

def average(x, y):
   avg = (x+y)/2
   return avg

We can use the function directly in our code by writing:

a = 1
b = 3
result = average(1, 3)

What you should see happening is that when you call the function average it adds both numbers and returns their mean value. Imagine that, for some reason, you want to allow only positive numbers as arguments of your function. You can expand the average function to check whether it is true or not.

def average(x, y):
   if x<0 or y<0:
      raise Exception("Both x and y have to be positive")

   avg = (x + y)/2
   return avg

Every time someone wants to use the function with a negative argument, an error will be raised. Later, we are asked to develop a new function to compute the geometric average of two numbers, and we need them both to be positive. Our function will look like this:

import math

def geom_average(x, y):
   if x<0 or y<0:
      raise Exception("Both x and y have to be positive")

   avg = math.sqrt(x*y)
   return avg

There is a general rule of thumb that says that code shouldn't be copied more than twice. If you are going to copy-paste code for the third time there is probably a better way of doing it. In the examples above, you can see that the verification of the input is exactly the same in both functions. If we were to write a third function, we would meet the three-copies rule. It would be useful to have an external way of checking that both inputs are positive, and this is exactly what decorators are meant to do.

Functions as arguments and as outputs of other functions

Before we can go into the details of how to use decorators in Python, it is important to show how functions work with inputs and outputs that are other functions. For example, we could define a function that transforms the output of the averages defined above into integers. It is a very simplistic example but already shows the pattern that you can follow to achieve more complex behaviors.

def integer_output(func, x, y):
   res = func(x, y)
   return int(res)

In the code above, you can see that integer_output takes three arguments, a function func and two numbers, x and y . We use the function, regardless of what it is, with arguments x and y. It then returns the result of func converted to an integer value. integer_output can be used like this:

rounded = integer_output(average, 1, 2)
geom_rounded = integer_output(geom_average, 4, 5)

It is important to note that the first argument is a function and it doesn't matter which one. You could use average or geom_average. The next two arguments are going to be passed directly to func . This is already quite powerful and most likely you can think a lot of ways in which you can use it, but Python allows you to do even more interesting things.

Functions can also be defined within functions and you can use them based on your input arguments. For example, let's assume you want to use average only if the sum of x and y is even and the geom_average if the sum is odd:

def even_odd_average(x, y):
   def average(a, b):
      return (a+b)/2
   def geom_average(a, b):
      return math.sqrt(a*b)

   if (x+y) % 2 == 0:
      return average(x, y)
      return geom_average(x, y)

The function even_odd_average takes only two arguments on which it is going to perform the average. Inside we define two functions, exactly as we did earlier, average and geom_average, but this time they are available only within the even_odd_average function. Based on the input from the user, we either calculate the average or the geometric average as requested earlier and we return the value. We can use this function as:

print(even_odd_average(4, 6))
print(even_odd_average(4, 9))

So far, we have seen how to use functions as arguments in other functions and how to define functions within functions. The only missing part is to be able to return a function instead of a value. Let's assume you want to print the time it takes to calculate the average between two numbers, but you don't want to re-write your original function. We have to write a function wrapper.

import time

def timing_average(func):
   def wrapper(x, y):
      t0 = time.time()
      res = func(x, y)
      t1 = time.time()
      print("It took {} seconds to calculate the average".format(t1-t0))
      return res

   return wrapper

We start by defining a function that takes as an argument another function. We also define a new function called wrapper as we explained earlier. So far, both steps were done in the previous examples, but now we are going to use func within the wrapper. We start by storing the current time at the variable t0. We execute the function func with the arguments x and y and store the new time at t1. We print the total time it took to run the function and return the output of func. The important part here is the very last line. As you can see, we are not returning the value that func returns, but we are actually returning the wrapper, which is in itself a function. To see this in action, we can do the following:

new = timing_average(average)
new(2, 4)

What you see in the above code is that we create a function called new by using timing_average with only one argument, the function average. New will take the same inputs that the wrapper function takes. If we use new as a function, with arguments 2 and 4 , you will see that it prints to screen the total time it took to calculate the average. new is nothing more than the function wrapper, defined using average. We could do the same using geom_average:

new_geom = timing_average(geom_average)

The syntax above can be hard to understand and forces you to define new functions to add timing capabilities. When you see that you are assigning the output of timing_average to a variable called new you don't expect it to actually be a function. If you already have working code, you need to do a lot of refactoring in order to define and use the new functions.

Fortunately, Python offers a very clear and simple way of achieving the same functionality, without the downsides just said. If you managed to follow the above examples, you are ready to improve the way the code looks like by using Python syntactic sugar.

Syntactic Sugar for Decorators

You already know almost everything there is to know regarding how to use decorators, you are just missing the syntactic sugar of Python. With what you have already done, you can improve the style of your code quite easily. Assuming you want to add timing capabilities to your average or geometrical average function, you can simply do:

def average(x, y):
   return (x+y)/2

By simply adding @timing_average before your function, you are now able to use average as always, but printing the time it takes to calculate it. The obvious advantage of this syntax is that it allows you to add an interesting new functionality without altering your downstream code. You don't need to define a new function, you only need to add one line of code before the definition of your average. It runs as always:

avg = average(4, 6)
print('The average between 4 and 6 is {}'.format(avg))

Coming back to the examples of the averages that take only positive arguments, and building on the example of timing_average, we can develop a wrapper function that would check whether the input of our function is positive or not.

def check_positive(func):
   def func_wrapper(x, y):
      if x<0 or y<0:
         raise Exception("Both x and y have to be positive for function {} to work".format(func.__name__))
      res = func(x,y)
      return res
   return func_wrapper

The structure of check_positive is very similar to what we have done for the timing. The only difference is that we check the input arguments and we raise an Exception if they are not both positive. Since we are raising an exception for an unknown function, it becomes handy to display which function actually gave the error. We achieve that by using func.__name__, which will tell us the name of the function. The rest is exactly the same as with the timing example. We can write our average functions as follows:

def average(x, y):
   return (x + y)/2

def geom_average(x, y):
   return math.sqrt(x*y)

Both functions, average and geom_average don't change their names, therefore you can use them as always, but they will check for positive input before computing the average:

average(2, 4)
average(-2, 4)
geom_average(4, 9)
geom_average(-4, 10)

Decorators can also be combined, you can time a function AND request the inputs to be positive:

def average(x, y):
   return (x + y)/2

You can play around and see what happens if you change the order of the decorators. Importantly, if you use func.__name__ to print the name of the function that raised the Exception within a decorator, you can see that the name can change and become the name of the wrapper. In most cases this is not a desired situation because you won't be able to debug what is the real function giving troubles, you will just get the name of the decorator. However, this is a more subtle topic that will be covered in the future.

Decorators are very powerful and can help you develop very clean and useful code. The obvious application of decorators is to validate the input provided by the user. Decorators are also very useful when you are writing a library that other developers are going to use. Or when you want to alter the behavior of a method or function in a systematic way. For example, you could use some cache in order to avoid running the same function with the same arguments over and over again.

One of the advantages of decorators is that even if a developer doesn't fully understand what it is happening under the hood, it will for sure understand how to use them and what to expect. If you provide good examples in your code it will become apparent where and when to include specific decorators. Now that you have a basic understanding of what the @ means in Python you can start thinking about many more interesting applications.

In this article, we have shown a couple of very basic examples that can be greatly improved. If you have ever encountered decorators and didn't understand how to use them, or you are looking for more specific information, leave your message in the comment section below, and we will use your feedback to write a follow-up article specifically designed to answer your questions.

Article written by Aquiles Carattino
Join our newsletter!
If you liked the content, sign up to never miss an update.

Share your thoughts with us!

Support Us

If you like the content of this website, consider buying a copy of the book Python For The Lab