Register to get access to free programming courses with interactive exercises

More about decorators Python: Functions

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.

About Hexlet learning process

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.

Get access
130
courses
1000
exercises
2000+
hours of theory
3200
tests

Sign up

Programming courses for beginners and experienced developers. Start training for free

  • 130 courses, 2000+ hours of theory
  • 1000 practical tasks in a browser
  • 360 000 students
By sending this form, you agree to our Personal Policy and Service Conditions

Our graduates work in companies:

<span class="translation_missing" title="translation missing: en.web.courses.lessons.registration.bookmate">Bookmate</span>
<span class="translation_missing" title="translation missing: en.web.courses.lessons.registration.healthsamurai">Healthsamurai</span>
<span class="translation_missing" title="translation missing: en.web.courses.lessons.registration.dualboot">Dualboot</span>
<span class="translation_missing" title="translation missing: en.web.courses.lessons.registration.abbyy">Abbyy</span>
Suggested learning programs
profession
new
Developing web applications with Django
10 months
from scratch
under development
Start at any time

Use Hexlet to the fullest extent!

  • Ask questions about the lesson
  • Test your knowledge in quizzes
  • Practice in your browser
  • Track your progress

Sign up or sign in

By sending this form, you agree to our Personal Policy and Service Conditions
Toto Image

Ask questions if you want to discuss a theory or an exercise. Hexlet Support Team and experienced community members can help find answers and solve a problem.