Shikha Gupta — August 24, 2021
Beginner Programming Python
This article was published as a part of the Data Science Blogathon

Introduction

Let’s look at examples of the benefits of nested Python functions and how to use them to encapsulate code, closures, and decorators.

Nested (or inner, nested) functions are functions that we define inside other functions to directly access the variables and names defined in the enclosing function. Nested functions have many uses, primarily for creating closures and decorators.

In this guide, we will

  • learn how to provide encapsulation and hide functions from external access

  • write helper functions to make code easier to reuse

  • implement closures to persist state between calls

  • let’s create decorators to add behaviour to existing functions

Creating Nested Functions in Python

Let’s start with a code example containing a nested function:

def outer_func():
    def inner_func():
        print("Hello, World!")
    inner_func()
outer_func()

OUTPUT

Hello, World!

In this code, we define internally to display a string . To do this, we call on the last line . inner_func()  outer_func() Hello, World! inner_func() outer_func()

The major use of internal functions is their easiness to access variables and objects from an subscribed (“external”) function. The enclosing function provides a namespace available to the nested function:

def outer_func(who):
    def inner_func():
        print(f"Hello, {who}")
    inner_func()
outer_func("World!")

OUTPUT

Hello, World!

Now we can pass a string as an argument to the function, and will refer to this argument through the name . This name is defined in the local scope. The names we defined in the local scope of the outer function are defined as. They are non-local from a point of view. external_func() inner_func() who outer_func() nonlocal inner_func()

Another example of a more complex nested function:

def factorial(number):
    if not isinstance(number, int):
        raise TypeError("The number must be whole.")
    if number < 0:
        raise ValueError("The number must be non-negative.")
    #Factorial calculation
    def inner_factorial(number):
        if number <= 1:
            return 1
        return number * inner_factorial(number - 1)
    return inner_factorial(number)
 factorial(4)

OUTPUT

24

In the function, we first validate the input to make sure the user is providing a non-negative integer. We then define a recursive named inner function that computes the factorial. In the last step, the corresponding calculation is called and performed. factorial() inner_factorial() inner_factorial()

The main advantage of using such a pattern is that by doing all the argument checks in the outer function, we can safely skip the error checking in the inner function and focus on the current computation.

The basics of using nested functions in Python

Encapsulation

A common use case for internal functions is when you need to protect or hide a function from everything that happens outside of it, that is, completely hide the function from the global scope. This behaviour is commonly referred to as encapsulation.

Let’s start with an illustrative example:

def increment(number):
    def inner_increment():
        return number + 1
    return inner_increment()
 print(increment(10))

OUTPUT

11

# Let's call the nested function inner_increment()
>>> print(inner_increment())

OUTPUT

Name Error: name ‘inner_increment’ is not defined

In this example, we don’t have direct access to. By trying to access the nested function, we get. The function hides the function completely, preventing access from the global scope. inner_increment() Name Error increment() inner_increment()

Creating internal helper functions

Sometimes we need a function that executes the same piece of code in several places in its body. Let’s take an example to create a function to access and use a CSV file containing information about Wireless hotspots. To find out the total number of access points, as well as information about the company that provides them, we created the following script:

import CSV
from collections import Counter
def process_hotspots(file):
    def most_common_provider(file_obj):
        hotspots = []
        with file_obj as csv_file:
            content = csv.DictReader(csv_file)
            for row in content:
                hotspots.append(row["Provider"])
        counter = Counter(hotspots)
        print(
             f "{counter.most_common (1) [0] [1]} provides"
             f "{counter.most_common (1) [0] [0]}."
        )
    if isinstance(file, str):
# Get the path to the file
                file_obj = open(file, "r")
        most_common_provider(file_obj)
    else:
        # We take the file object
        most_common_provider(file)

This takes an argument and checks if the file is a string path to a physical file or a file object. The function then calls a helper inner function that takes a file object and performs the following operations: process_hotspots() file most_common_provider()

  1. Reads the contents of a file into a generator that creates dictionaries using. CSV.DictReader

  2. Lists Wi-Fi providers.

  3. Counts the number of Wi-Fi hotspots for each provider using a facility. collections.Counter

  4. Prints a message with the information received.

By running the function, we get the following output:

>>> from hotspots import process_hotspots
>>> file_obj = open ("./ NY_Wi-Fi_Hotspot_Locations.csv", "r")
>>> process_hotspots (file_obj)
There are 3,319 Wi-Fi points in New York.
1,868 of these are provided by LinkNY - City bridge.
>>> process_hotspots ("./ NY_Wi-Fi_Hotspot_Locations.csv")
There are 3,319 Wi-Fi points in New York.
1,868 of these are provided by LinkNY - City bridge.

Regardless of whether we call with a string file path or a file object, you will get the same result. process_hotspots()

Using internal and private helper functions

We usually create helper internal functions, such as when we want to provide encapsulation or when we are not going to call them anywhere other than the enclosing function. most_common_provider()

Although writing internal helper functions gives the desired result, it is usually best to expose them as top-level functions. In this case, you can use the underscore prefix in the function name to indicate that it is private to the current module or class. To make the code cleaner and readable we use extraction of internal functions into top-level private functions. This practice is consistent with the principle of single responsibility.

Saving State with Nested Functions: Closures in Python

Python functions in their rights are equal to any other objects, such as numbers, strings, lists, tuples, modules, etc. That is, they can be dynamically created or destroyed, stored in data structures, passed as arguments to other functions, used as returned values.

What is Closure? How to Implement it in Python? | by Aykhan Nazimzada | nested functions in python

Source

If we do not need to hide the internal functions from the outside world, then there is no particular reason for nesting.

In this section, we’ll talk about another kind of nested function – closures. These are dynamically created functions returned by other functions. To access variables and names defined in the local namespace closures have full rights, no matter whether the enclosing function has finished executing or not.

There are three steps to defining a closure:

  1. Create a nested function.

  2. Refer to variables from the enclosing function.

  3. Return a nested function.

Let’s look at some examples now.

Saving state in a closure

So, the closure forces the nested function, when called, to save the state of its environment. That is, the closure is not only the internal function itself but also the environment.

Consider the following example:

powers.py
def generate_power(exponent):
    def power(base):
        return base ** exponent
    return power

Here we define a function that is a factory for creating closures. That is, this function creates and returns a new closure function each time it is called. The next line defines a function that is an internal function and takes a single argument and returns the result of the expression. The last line returns as a function object without calling it. generate_power() power() base base ** exponent power

Where does the exponent value come from? This is where snapping comes into play. This example gets the exponent value from an external function. This is what Python does when we call : power() exponent power() generate_power() generate_power()

  1. Defines a new instance that takes an argument. power() base

  2. Takes a “snapshot” of the environment. It includes the current value. power() exponent

  3. Returns along with the state. power()

Thus, when we call the instance returned by the function, we can see that the function remembers the value of the degree: power() generate_power() exponent

>>> raise_two = generate_power(2)
>>> raise_three = generate_power(3)
>>> raise_two(4)

OUTPUT

16

>>> raise_two(5)

OUTPUT

25

>>> raise_three(4)

OUTPUT

64

Note that both closures remember the corresponding exponent between calls. In these examples, remembers what , and remembers what . raise_two() exponent = 2 rise_three() exponent = 3

Let’s consider another example:

def has_permission(page):
    def permission(username):
        if username.lower() == "admin":
          return f "'{username}' has right to open {page}."
         else:
             return f "'{username}' does not have right to open {page}."
    return permission
check_admin_page_permision = has_permission("Admin Page")
>>> print (check_admin_page_permision ("admin"))

OUTPUT

‘admin’ has access to the Admin Page.

>>> print (check_admin_page_permision ("john"))

OUTPUT

‘john’ does not have access to the Admin Page.

The nested function checks if the given user has the required access rights to the page. Instead of checking if the user is equal, you can query the database . ‘admin’

Closures usually do not change the state they received at birth, as shown in the examples above. But you can also create dynamic closures using mutable objects such as dictionaries, sets, or lists.

Suppose you want to calculate the average for a dataset. The data comes in the form of a stream of successive measurements of the analyzed parameter, and it is necessary that the function retains the previous measurements between calls. In this case, the factory code for creating closures might look like this:

def mean():
    sample = []
    def inner_mean(number):
        sample.append(number)
        return sum(sample) / len(sample)
    return inner_mean
sample_mean = mean()
>>> sample_mean(100)
100.0
>>> sample_mean(105)
102.5
>>> sample_mean(101)
102.0
>>> sample_mean(98)
101.0

A closure assigned maintains the fetch state between calls. Although we define a list internally, it is also available in a closure. sample_mean sample mean()

Changing the state of a snapping

Closure variables are usually completely hidden from the outside world. However, we can define getter and setter functions for them:

def make_point(x, y):
    def point():
        print(f"Point({x}, {y})")
    def get_x():
        return x
    def get_y():
        return y
    def set_x(value):
        nonlocal x
        x = value
    def set_y(value):
        nonlocal y
        y = value
    # Adding getters and setters
    point.get_x = get_x
    point.set_x = set_x
    point.get_y = get_y
    point.set_y = set_y
    return point
point = make_point(1, 2)
>>> point.get_x()
1
>>> point.get_y()
2
>>> point()
Point(1, 2)
>>> point.set_x(42)
>>> point.set_y(7)
>>> point()
Point(42, 7)

Here returns a closure representing the object. Functions are attached to this object that we can use to gain access to read and write variables and. make_point() point x y

Such a factory may even be faster than an equivalent class, but the approach does not provide inheritance, descriptors, and other features of Python classes.

Changing Behavior with Nested Functions: Decorators

Python decorators are the next popular and easier use cases for internal functions, especially for closures. Decorators are higher-order functions that take a callable object (function, method, class) as an argument and return another callable object.

python decorators | nested functions in python

Source

Typically, decorators are used to dynamically add properties to an existing callee and transparently extend its behaviour without affecting or modifying the callee. A decorator function can be applied to any callable object. To do this, the symbol and the name of the decorator are placed in the line preceding it

@decorator
def decorated_func():
    # Function body...

pass

This syntax forces you to automatically accept it as an argument and process it in your body. This operation is an abbreviation for a statement like this: decorator() decorator_func()

decorated_func = decorator(decorated_func)

Here’s an example of how you can create a decorator function to change the behaviour of an existing function:

def add_messages(func):
    def _add_messages():
       print ("This is my first decorator.")
         func ()
         print ("Bye!")
     return _add_messages
@add_messages
def greet ():
     print ("Hello world!")
 greet()

OUTPUT

Hello World!

In this example, we use functions to decorate. As a result, the function gains new functionality. Now, when we call, instead of just typing, it prints out two additional messages. @add_messages greet() greet() Hello World!

The simplest practice for debugging Python code is to insert calls to check the values ​​of variables. However, by adding and removing calls, we risk forgetting about some of them. To prevent this situation, we can write the following decorator: print() print()

def debug(func):
    def _debug(*args, **kwargs):
        result = func(*args, **kwargs)
        print(
            f"{func.__name__}(args: {args}, kwargs: {kwargs}) -> {result}"
        )
        return result
    return _debug
@debug
def add(a, b):
    return a + b
>>> add(5, 6)
add(args: (5, 6), kwargs: {}) -> 11
11

In this example, the decorator function prints the name of the function to decorate, the current values ​​of each argument, and the return result. Such a decorator can be used for the basic debugging of functions. Once we get the desired result, it is enough to remove the call to the decorator, and the debugged function will work as usual. debug () @debug

Let’s give the last example and reimplement it as a decorator function: generate_power()

def generate_power(exponent):
    def power(func):
        def inner_power(*args):
            base = func(*args)
            return base ** exponent
        return inner_power
    return power
@generate_power(2)
def raise_two(n):
    return n
@generate_power(3)
def raise_three(n):
    return n
>>> raise_two(7)
49
>>> raise_three(5)
125

This version gives the same results as the original implementation. In this case, to store the exponent. generate_power() func() in a modified version we use both the closure as well as a decorator.

Here the decorator must take an argument (exponent), so we needed two levels of nesting. The first level is represented by a function that takes the function to decorate as an argument. The second level is represented by a function that packs the exponent into, performs the final calculation, and returns the result. power() inner_power() args

Conclusion

So, in Python, nested functions have direct access to the variables and names that you define in the enclosing function. It provides a mechanism for encapsulating functions, creating helper solutions, and implementing closures and decorators.

The media shown in this article are not owned by Analytics Vidhya and are used at the Author’s discretion.

About the Author

Our Top Authors

  • Analytics Vidhya
  • Guest Blog
  • Tavish Srivastava
  • Aishwarya Singh
  • Aniruddha Bhandari
  • Abhishek Sharma
  • Aarshay Jain

Download Analytics Vidhya App for the Latest blog/Article

Leave a Reply Your email address will not be published. Required fields are marked *