Python: Functions
Theory: More about decorators
Let us do a little more fantasizing. Imagine we want to be able to validate arguments of functions — check whether their values correspond to rules. And we want to do it using decorators that can be applied again. We will implement a couple of these decorators by the end of the lesson.
Using decorators with parameters
But first, we need to digress a little. What happens if the arguments of the function do not pass our verification? We need to show the error. But how do we do it? We will tell you more about working with errors in the subsequent, but for now, we will show you how to provoke an error:
It is the error we will show if the argument value does not pass validation. Now we can start creating decorators. Let us say a function has a numeric argument greater than zero and is not equal to invalid values.
Of course, we could make this sort of special decorators:
But that is not enough for all instances of such highly specialized decorators. We separate the wrapping of the function and the checks so that the usual predicates can act as the checks. But how does the decorator know about the predicate if it always accepts the wrapped function as a single parameter?
We can use closure. We need a function that will take a predicate function as an argument and return a wrapper function, and then it will also take a function as an argument and return the same function. Now Let us get down to writing this function layer cake:
The function checking_that_arg_is takes a predicate and returns wrapper. Here, wrapper is now our decorator with inner inside. The inner checks the argument using a predicate. If we meet the condition, we call it function.
Over time you will be able to read and write this sort of code quickly and easily because decorators, including those with parameters, are often seen in Python code. Using a decorator with parameters looks like this:
At last, we have something to wrap. Now we will write some closures that will act as checks:
The functions not_ and in_ have the symbol _ at the end of their name. We recommend calling variables whose names coincide with keywords or names of built-in functions.
These higher-order functions take parameters and return predicates that are convenient to use with the decorator described above. Remember that we need to check that the function argument has a value greater than zero and not equal to any bad ones. Here is how these conditions will look in the code:
The conditions look almost like simple spoken phrases. A predicate factory is a higher-order function that returns a predicate. It is abstract enough to be applicable for validating different values. And our predicates are composable, convenient for creating combinations of existing functions without writing new ones, like not_(in_(...)).
Wrapping functions
When we declare a function, it gets a name. Also, it can have a docstring — a documentation string.
We can show this documentation using IDEs or the help() function in Python REPL:
But what happens if we wrap a function with a decorator? We will see:
The function has also lost its name and documentation, so it became wrapped.<locals>.inner. But how do you keep both? You can do this manually by copying the attributes __name__ and __doc__.
But there is a better way. Let us rewrite our decorator using the wraps decorator from the functools module:
We wrapped the foo() function, but the wrapper kept the documentation and the name. By the way, have you noticed that wraps is also a decorator with a parameter? You can probably even imagine how to implement it. Wrappers created using wraps have another property.
You can always reach out to the wrapped function afterward because we store a reference to the original one in the wrapper attribute __wrapped__:
The wraps decorator will make your decorators exemplary, so we recommend you use it.