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
This article was published as a part of the Data Science Blogathon
Let’s start with a code example containing a nested function:
def outer_func(): def inner_func(): print("Hello, World!") inner_func() outer_func()
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!")
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)
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.
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))
11
# Let's call the nested function inner_increment() >>> print(inner_increment())
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()
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()
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()
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.
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.
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:
Let’s look at some examples now.
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()
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()
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.
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.
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
A. A nested function is a function defined within another function. For example, in Python:
def outer_function(x):
def inner_function(y):
return x + y
return inner_function
Here, inner_function
is nested inside outer_function
and can access variables from its enclosing scope, like x
.
A. A nested function refers to a function that is defined within another function’s body. In programming languages that support this feature (like Python), the inner function can access variables and parameters from its enclosing (outer) function, even after the outer function has finished executing. This concept is also known as a “closure” and allows for more flexible and modular code design.
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.