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:
raise ValueError('Value too low!')
# From the traceback, the most recent calls are the last:
# File "<stdin>", line 1, in <module>
# ValueError: Value is too low!
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:
@greater_than_zero
@not_bad
def function(arg):
# …
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:
def checking_that_arg_is(predicate, error_message):
def wrapper(function):
def inner(arg):
if not predicate(arg):
raise ValueError(error_message)
return function(arg)
return inner
return wrapper
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:
@checking_that_arg_is(condition, "Invalid value!")
def foo(arg):
# …
At last, we have something to wrap. Now we will write some closures that will act as checks:
def greater_than(value):
def predicate(arg):
return arg > value
return predicate
def in_(*values):
def predicate(arg):
return arg in values
return predicate
def not_(other_predicate):
def predicate(arg):
return not other_predicate(arg)
return predicate
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:
@checking_that_arg_is(greater_than(0), "Non-positive!")
@checking_that_arg_is(not_(in_(5, 15, 42)), "Bad value!")
def foo(arg):
return arg
foo(0)
# From the traceback, the most recent calls are the last:
# File "<stdin>", line 1, in <module>
# File "<stdin>", line 5, in inner
# ValueError: Non-positive!
foo(5)
# From the traceback, the most recent calls are the last:
# File "<stdin>", line 1, in <module>
# File "<stdin>", line 6, in inner
# File "<stdin>", line 5, in inner
# ValueError: Bad value!
foo(6)
# 6
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:
def add_one(arg):
"""
Add one to the argument
The argument should be a number
"""
return arg + 1
add_one
# <function add_one at 0x7f105936cd08>
# ^ And here is the name of the function object
help(add_one)
# …
# add_one(arg)
# Adding one to the argument
# The argument should be a number
# …
But what happens if we wrap a function with a decorator? We will see:
def wrapped(function):
def inner(arg):
return function(arg)
return inner
add_one = wrapped(add_one)
add_one
# <function wrapped.<locals>.inner at 0x7f1056f041e0>
help(add_one)
# …
# inner(arg)
# …
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:
from functools import wraps
def wrapped(function):
@wraps(function)
def inner(arg):
return function(arg)
return inner
def foo(_):
"""Bar"""
return 42
foo = wrapped(foo)
foo
# <function foo at 0x7f1057b15048>
help(foo)
# …
# foo()
# Bar
# …
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__
:
foo.__wrapped__
# <function foo at 0x7f1056f04158>
The wraps
decorator will make your decorators exemplary, so we recommend you use it.
Are there any more questions? Ask them in the Discussion section.
The Hexlet support team or other students will answer you.
For full access to the course you need a professional subscription.
A professional subscription will give you full access to all Hexlet courses, projects and lifetime access to the theory of lessons learned. You can cancel your subscription at any time.