Working on your own

Day 29: Exercise Solutions

Python Guru with a screen instead of a face, typing on a computer keyboard with a dark purple background to match the day 29 image.

Here's our solution for the day 29 exercise in the 30 Days of Python series. Make sure you try the exercise yourself before checking out the solution!

1) Make a decorator which calls a given function twice. You can assume the functions don't return anything important, but they may take arguments.

You can call your decorator whatever you like, but I'm going to go for double here.

def double():
    pass

There are few things we know we need to do right off the bat. We need our decorator to accept a function as an argument, and we need to return a reference to a function we define in the decorator's function body.

It's also a good idea to use wraps to preserve the original function name and documentation.

from functools import wraps

def double(func):
    @wraps(func)
    def inner():
        pass

    return inner

Because our functions may accept arguments, we should define our inner function to take any number of positional and keyword arguments using *args, **kwargs**. We should then pass on those arguments when calling the functions in inner.

from functools import wraps

def double(func):
    @wraps(func)
    def inner(*args, **kwargs):
        func(*args, **kwargs)
        func(*args, **kwargs)

    return inner

2) Imagine you have a list called books, which several functions in your application interact with. Write a decorator which causes your functions to run only if books is not empty.

Let's start by setting up our usual decorator boilerplate. I'm also going to create a list called books.

from functools import wraps

books = []

def requires_content(func):
    @wraps(func)
    def inner(*args, **kwargs):
        pass

    return inner

The actual logic here is fairly simple. We just need to check the truth value of books inside inner, and only call the function if the condition evaluates to True.

from functools import wraps

books = []

def requires_content(func):
    @wraps(func)
    def inner(*args, **kwargs):
        if books:
            func(*args, **kwargs)

    return inner

We should probably also return the return value of func, just in case the function returns something.

from functools import wraps

books = []

def requires_content(func):
    @wraps(func)
    def inner(*args, **kwargs):
        if books:
            return func(*args, **kwargs)

    return inner

3) Write a decorator called printer which causes any decorated function to print their return values. If the return value of a given function is None, printer should do nothing.

This exercise is fairly similar to the previous one: we just need to have a condition based on the return value of the function, rather than some external variable.

Let's start with our boilerplate again.

from functools import wraps

def printer(func):
    @wraps(func)
    def inner(*args, **kwargs):
        pass

    return inner

The first step here is going to be actually calling the function inside inner.

from functools import wraps

def printer(func):
    @wraps(func)
    def inner(*args, **kwargs):
        return_value = func(*args, **kwargs)

    return inner

Now we need to check the value we stored in return_value.

It's really important that we don't just check the truth value of return_value here, because there are plenty of other falsy values that might be returned by the function. For example, 0, False, or [].

from functools import wraps

def printer(func):
    @wraps(func)
    def inner(*args, **kwargs):
        return_value = func(*args, **kwargs)

        if return_value is not None:
            print(return_value)

    return inner

Note that the idiom when checking for None is to use is and is not, not == and !=. This works because all instances of None are actually the same value in memory.