An Introduction To Closures and Decorators in Python

Today’s tutorial will introduce two slightly more advanced concepts of closures and decorators in Python. We’ll explain what they are, how each is defined, and where and how they will help in your coding.

Nested Functions

I’m sure you are all familiar with functions, and some of you may have used, or heard of, nested functions. A nested function is where one function is ‘nested’ or enclosed inside another.

A closure is a type of nested function, but all nested functions are not closures. Confused? So was I. Let’s start with nested functions, then move on to what makes a closure different and why and when you’d use one.

The following code creates a nested function. The first, or enclosing function, applies a markup to a wholesale price to return a retail cost.

The inner, or enclosed function, applies a markup to the same wholesale price to return a reduced sale price.

Finally, we call the nested function we’ve just defined, sale_price(), before exiting the code.

def markup(wholesale_price):

    retail_price = (wholesale_price * 1.76)
    print("Retail price: ${}".format(round(retail_price, 2)))

    def sale_price():

        sale = (wholesale_price * 1.56)
        print("Sale price: ${}, save {}%!.".format(round(sale, 2), round(((retail_price - sale)/retail_price)*100), 2))

    sale_price()


markup(1.20)

# Result

# Retail price: $2.38
# Sale price: $2.11, save 11%!.

Having defined the nested function, we then call it and provide the wholesale price of $1.20. The return is as you would expect, with the retail price printed first, followed by the sale price from the nested function.

There are two important points to note from this code.

  1. The enclosed function may access, on a read-only basis, non-local variables contained within the enclosing function. In our case, the nested function sale_price() could utilise the variable wholesale_price contained within the enclosing function markup().
  2. Once the function executes and carries out the required task, Python forgets the variables involved.

To prove point two, let’s call a print on the retail_price variable once the function has finished execution.

def markup(wholesale_price):

    retail_price = (wholesale_price * 1.76)
    print("Retail price: ${}".format(round(retail_price, 2)))

    def sale_price():

        sale = (wholesale_price * 1.56)
        print("Sale price: ${}, save {}%!.".format(round(sale, 2), \
              round(((retail_price - sale)/retail_price)*100), 2))

    sale_price()


markup(1.20)

print(retail_price)

# Result

# NameError: name 'retail_price' is not defined
# Retail price: $2.11
# Sale price: $1.87, save 11%!.

As expected, we receive a NameError because Python has forgotten the variables used within the functions once executed.

Closures

So what makes a closure different to a nested function? A closure is defined when a function accesses a variable from an enclosing scope after that enclosing function has completed execution.

How is that possible? It simply requires us to bind the enclosing function and its arguments to a name. We may then call that name at any stage to retrieve the variable. Even if we delete the function after binding it, the variables will still be accessible. Here’s the code.

def markup(wholesale_price):

    retail_price = (wholesale_price * 1.76)
    print("Retail price: ${}".format(round(retail_price, 2)))

    def sale_price():

        sale = (wholesale_price * 1.56)
        print("Sale price: ${}, save {}%!.".format(round(sale, 2), round(((retail_price - sale)/retail_price)*100), 2))

    return sale_price

label = markup(1.35)

label()

del markup

print('\n', 'Markup has been deleted', '\n')

label()

# Result
'''
Retail price: $2.38
Sale price: $2.11, save 11%!.

Markup has been deleted 

Sale price: $2.11, save 11%!.
'''

In this code, we changed a couple of things. Instead of calling sales_price as we did in the first two blocks of code, which activated the print() command, we returned it. That return statement returns an explicit value allowing use in other expression.

We then bound that returned value to a name 'label', using the label = markup(1.35) line of code. When we execute that line of code, it prints the retail price as shown in the first line of the result and then passes the sale price to the name 'label'.

If we then call label(), it will print the sale price string as shown in the second line of the result.

To prove that we have created a closure rather than simply a nested function, we deleted the enclosing function called markup() and printed a string to mark the deletion. Finally, we called label() again and despite the function no longer being in existence, the variables were all accessible and returned as before.

Why Use Closures?

There are four reasons to use closures, and all have to do with being efficient and elegant with your coding.

(1) To prevent the unnecessary use of classes:

If you define a class that only uses one method other than __init__, it is more elegant to use a closure.

(2) To implement a form of data hiding:

When using a nested function, the only way to access the enclosed function is by calling the outer function.

(3) Avoid using the global scope:

Suppose you have a variable that only one function will utilise; rather than define a global variable, use a closure, define the variable in the outer function and utilise it in the enclosed function.

(4) To access a function environment after execution:

The variables from the function will remain accessible for use later in your programme.

What Is A Decorator?

Put simply, a decorator takes a function, adds some further functionality to it, then returns a result. In effect, it allows you to take existing code and reuse it with an enlarged or greater functionality. It does not change the original function.


Here’s a basic function that takes a variable, called buy_price, and multiplies it by a markup before printing and returning the selling price.

buy_price = .89

def markup():
    retail_price = (buy_price * 1.76)
    print('Normal retail price: $', round(retail_price, 2))
    return retail_price

markup()

# Result

# Normal retail price: $ 1.57

What if we want to be able to offer a special or sales price? We could define another function and pass the first function to the second.  In effect, using the original markup() function but modifying the output for a limited time sale. Here’s the code for that.

buy_price = .89

def sale(func):
    def calc():
        print('Special pricing this week only: $', round(func() * 0.8, 2), 'Save 20%!')
    return calc()

def markup():
    retail_price = (buy_price * 1.76)
    print('Normal retail price: $', round(retail_price, 2))
    return retail_price

sale(markup)

# Result

Normal retail price: $ 1.57
Special pricing this week only: $ 1.25 Save 20%!

In the preceding code, we generated a second function which took the first function as an argument then printed and returned a discount on the original marked up price from the first function. We passed the markup function to the sale function using the sale(markup) command, and we returned the normal retail price, and a sales price.


This second function, called sale(), is a decorator. It takes the original code and modifies it for a one time special without rewriting the original code.


Yet, it’s still a little cumbersome in its current form. There is a shorthand version of applying the decorator, using the @ symbol and the decorators name. With this we simply call the original function to have the sale automatically activated. Here it is.

buy_price = .89

def sale(func):
    def calc():
        print('Special pricing this week only: $', round(func() * 0.8, 2), 'Save 20%!')
    return calc()

@sale
def markup():
    retail_price = (buy_price * 1.76)
    print('Normal retail price: $', round(retail_price, 2))
    return retail_price

markup

# Result
'''
Normal retail price: $ 1.57
Special pricing this week only: $ 1.25 Save 20%!
'''

Using Multiple Arguments

The previous code used a simple function that didn’t accept any parameters. Let’s quickly look at an example where we pass a product name and a buy price to the original function. That will return a retail price. We then apply a decorator that takes the parameters from the original function to apply a special sale price.

Note that the nested function calc() parameters are the same as the parameters of the function it decorates. If you intend to have a decorator with a broader application than our example, you can use the *args and **kwargs placeholders to account for the passing of unknown parameters.

def sale(func):
    def calc(name, buy_price):
        print('Cans of {} on sale. Normal price ${}, now ${}!'.format(name, round(buy_price * 1.76, 2), round(buy_price * 1.4, 2)))
        print('Save 20%')
    return calc

@sale
def markup(name, buy_price):
    retail_price = (buy_price * 1.76)
    print('{} at a retail price of ${}'.format(name, round(retail_price, 2)))
    return retail_price

markup('Tomato Soup', 1.2)

# Result
'''
Cans of Tomato Soup on sale. Normal price $2.11, now $1.68!
Save 20%
'''

Chaining Decorators

We are not limited in the number of decorators we can apply to a function. You may place as many as you wish above the function you wish to enhance. This is called β€˜chaining’.

buy_price = .89

def sale(func):
    def calc():
        print('Special pricing this week only: $', round(func() * 0.8, 2), 'Save 20%!')
    return calc()

def no_freeze(func):
    def label():
        print('This product was thawed from frozen. Do not refreeze. Must be used within 7 days of opening')
    return label()

@no_freeze
@sale
def markup():
    retail_price = (buy_price * 1.76)
    print('Normal retail price: $', round(retail_price, 2))
    return retail_price

markup

# Result
'''
Normal retail price: $ 1.57
Special pricing this week only: $ 1.25 Save 20%!
This product was thawed from frozen. Do not refreeze. Must be used within 7 days of opening
'''

In the preceding code, we created a second function called no_freeze() that we applied to markup in addition to sale(), and we see the result in the third line returned. However, one item to note is that the order of chaining does matter and if you do not see the returns you expect, consider the order in which the decorators are chained.

Summary

In this article, we looked at two advanced concepts in Python, called closures and decorators.

A closure is a form of nested function that allows a function to access a variable from an enclosing scope after that enclosing function has completed execution. We achieve that by binding the enclosing function and its arguments to a name.

Closures provide four benefits to assist you in being more efficient and elegant with your coding.

  1. They prevent the unnecessary use of classes if you define a class that only uses one method.
  2. They implement a form of data hiding because when using a nested function, the only way to access the enclosed function is by calling the outer function.
  3. They avoid the use of global scope when you have a variable that only one function will utilise. You define the variable in the outer function and utilise it in the enclosed function.
  4. They allow variables from the function to remain accessible for use later in your programme, even after function execution.

We then discussed decorators, which are functions allowing you to take an existing function and modify or ‘decorate’ its output. We learned that rather than needing to call the decorator and pass to it the function we wish modified, we could use a shorthand notation. That notation uses the @ symbol and the name of the decorating function. This notation is then placed immediately in front of the function to be modified. Calling the original function will cause the decorator to be applied.

That’s all for this article. I trust it’s been helpful, and thank you for reading.