Home Python Python Inner Functions: Know their merits

Python Inner Functions: Know their merits

Python inner functions have a wide range of applications. They can be used to offer encapsulation and hide your functions from external access and write useful inner functions, closures, and decorators. In the following sections, you’ll learn about the first two use cases of inner functions in this section and how to create closure factory functions and decorators.

Inner Functions in Python

Inner functions, also known as nested functions, are functions that are defined within another function. This type of function in Python has direct access to the enclosing function’s variables and names. Inner functions can be used in a variety of ways, such as closure factories and decorator functions.

Functions are treated as first-class objects in Python. In a language, first-class objects are treated consistently throughout. They can be utilized in control structures, supplied as arguments, and saved in data structures. If a programming language handles functions as first-class objects, it is said to support first-class functions. Python supports the concept of First Class functions.

Making Inner Functions in Python

An inner function, also known as a nested function, is a function that is defined inside another function. This type of function can access names in the surrounding function in Python. An example of how to write an inner function in Python is as follows:

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


outer_func()

On print the Hello, Codeunderscored! Message to the screen, you declare inner_func() inside outer_func() in this code. On the last line of outer_func(), you call inner_func() to do this. In Python, this is the shortest way to write an inner function. On the other hand, inner functions offer a plethora of intriguing options beyond what is demonstrated in this example.

Functions of the first class have the following properties:

  • An instance of the Object type is a function.
  • The function can be saved in a variable.
  • The function can be sent as a parameter to another function.
  • From a function, you can return the function.
  • They can be stored in data structures such as hash tables, lists, and so on.

Inner functions

Inner function or nested function refers to a function that is defined inside another function. Nested functions can access the variables of the enclosing scope. Inner functions are utilized to shield them from anything that happens outside of the function. Encapsulation is another name for this procedure.

# code to illustrate nested functions

def outerFunction(enter_text):
	text = enter_text
	
	def innerFunction():
		print(text)
	
	innerFunction()
	
if __name__ == '__main__':
	outerFunction('Codeunderscored !')

InnerFunction() has been defined inside outerFunction() in the preceding example, making it an inner function. We must first call outerFunction() before calling innerFunction() . After that, outerFunction() will call innerFunction(), which has been declared inside it.

The outer function must be called for the inside function to run. Consider the following example to see what we mean:

# Python program to illustrate
# nested functions
def outerFunction(text):
	text = text
	
	def innerFunction():
		print(text)
	
	innerFunction()

Variable scope in nested functions

The scope of a variable refers to the location where we can find a variable and access it if necessary.
It’s common knowledge to access a global variable within a function, but what about an outer function’s variable? Consider the following scenario:

# Python program to
# demonstrate accessing of
# variables of nested functions

def main_function():
	new_string = 'Codeunderscored website'
	
	def sub_function():
		print(new_string)
		
	sub_function()

# call main_function
main_function()

It can be observed in the preceding example that it is comparable to accessing a global variable from a function. Let’s pretend you wish to update the outer function’s variable.

# program to demonstrate accessing of variables of nested functions

def main_function():
	new_string = 'Codeunderscored website'
	
	def sub_function():
		new_string = 'Python Project'
		print(new_string)
		
	sub_function()
	print(new_string)

# run main_function
main_function()

As can be observed, the value of the outer function’s variable has not changed. The value of the outer function’s variable, on the other hand, can be altered. The value of the outer function’s variable can be changed in a variety of ways.

Using an iterable

# program to demonstrate accessing of variables of nested functions

def main_function():
	new_string = ['Codeunderscored website']
	
	def sub_function():
		new_string[0] = 'Python Project'
		print(new_string)
		
	sub_function()
	print(new_string)

# run main_function
main_function()

Using nonlocal keyword

# program to demonstrate accessing of variables of nested functions

def main_function():
	new_string = 'Codeunderscored website'
	
	def sub_function():
		nonlocal s
		new_string = 'Python Project'
		print(s)
		
	sub_function()
	print(s)

# run main_function
main_function()

As seen in the sample below, the value can also be altered.

# program to demonstrate accessing of variables of nested functions

def main_function():
	main_function.s = 'Codeunderscored website'
	
	def sub_function():
		main_function.s = 'Python Project'
		print(main_function.s)
		
	sub_function()
	print(main_function.s)

# run main_function
main_function()

Closures in Python

Closures are function objects that remember values in enclosing scopes even if they aren’t in memory.

It’s a record that records a function and its environment: a mapping that links each function’s free variables (variables that are used locally but defined in an enclosing scope) to the value or reference to which the name was bound when the closure was formed.

Unlike a regular function, a closure allows the function to access the captured variables via the closure’s copies of their values or references, even if the function is called outside of their scope.

# program to illustrate closures
def outerFunction(text):
	text = text
	
	def innerFunction():
		print(text)
	
	return innerFunction # Note we are returning function WITHOUT parenthesis
	
if __name__ == '__main__':
	myFunction = outerFunction('Codeunderscored!')
	myFunction()

As seen in the code above, Closures allow you to call functions that aren’t in your scope.

The innerFunction function’s scope is limited to the outerFunction. However, we can increase its scope by using closures to call a function that isn’t in its scope.

# program to illustrate closures
import logging
logging.basicConfig(filename ='codeunderscored_log.log', level = logging.INFO)
	
	
def logger(func):
	def log_func(*args):
		logging.info(
			'Running "{}" with arguments {}'.format(func.__name__, args))
		print(func(*args))
	# Necessary for closure to work (returning WITHOUT parenthesis)
	return log_func			
	
def add_function(x, y):
	return x + y
	
def sub_function(x, y):
	return x-y
	
add_logger = logger(add_function)
sub_logger = logger(sub_function)
	
add_logger(5, 5)
add_logger(6, 7)
	
sub_logger(12, 7)
sub_logger(22, 12)

Inner Functions for Adding Behavior: Decorators

Python decorators, particularly for closures, are another widespread and practical use case for inner functions. Decorators are higher-order functions that accept as an argument a callable (function, method, or class) and return another callable.

Decorator functions can be used to dynamically add responsibilities to an existing callable and extend its behavior without altering or modifying the original callable.

To make a decorator, declare a callable (a function, method, or class) that takes a function object as an input process and returns a new function object with different behavior.

You can use your decorator function on any callable once you’ve created it. To do so, add the decorator name on its line directly before the decorated callable and use the at symbol (@) in front of it:

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

If you use this syntax, the decorator will take decorated_func() as a parameter and process it in its body. This operation is a shortened version of the following task:

decorated_func = decorator(decorated_func)

Here’s an example of how to add new functionality to an existing function using a decorator function:

 def display_messages(func):
     def _add_messages():
         print("Codeunderscored first decorator")
         func()
         print("Bye!")
     return _add_messages


 @display_messages
 def salute():
     print("Hello, Codeunderscored!")


 salute()

Instead of only printing Hello, Codeunderscored!, your salute() function prints two new messages. @display_messages are used to decorate salute() in this situation. The decorated function gains new capabilities as a result of this.

Python decorators have a wide range of applications. Here are a few examples:

  • Debugging
  • Caching
  • Logging
  • Timing

Inserting calls to print() to check the contents of variables, ensure that a code block is performed, and so on is a popular approach for debugging Python programs. Adding and removing print() calls can be tedious, and you risk forgetting some of them. To avoid this, create a decorator that looks like this:

def debug(func):
	def _debug(*args, **kwargs):
		final_result = func(*args, **kwargs)
		print(f"{func.__name__}(args: {args}, kwargs: {kwargs}) -> {final_result}")
		return final_result
	return _debug

@debug
def add_function(x, y):
	return x + y


add_function(8, 9)

Debug() is a decorator that takes a function as a parameter and prints its signature, including the current value of each argument and its corresponding return value. This decorator can be used to debug your functions. Once you’ve achieved the desired outcome, remove the @debug decorator, and your function will be ready for the next stage.

Finally, here’s an example of how to make a decorator. You’ll reimplement generate_power() as a decorator function this time:

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

raise_two(5)


@generate_power(3)
def raise_three(n):
	return n

raise_three(3)

This version of generate_power() gives you the same results as the previous version. In this case, you use a closure to remember the exponent and a decorator to return a modified version of the input function, func().

You’ll need two nested tiers of inner functions because the decorator has to take an argument (exponent). The decorated function is passed as an input to power(), which represents the first level. Inner_power(), which packs the argument exponent in args, does the final power calculation, delivers the result, and represents the second level.

Closures: Retaining State With Inner Functions

Functions are treated as first-class citizens in Python. It means they can be compared to other objects like numbers, strings, lists, tuples, modules, etc. You can construct and destroy them dynamically, store them in data structures, send them as parameters to other functions, return them, and so on.

Python also allows you to design higher-order functions. Higher-order functions interact with other functions by taking them as arguments, returning them, or doing both.

So far, all of the instances of inner functions you’ve seen have been ordinary functions that are nested inside other functions. There’s no reason for your functions to be nested unless you need to hide them from the outside world. Those functions could be defined as private top-level functions, and you’d be ready to go.

Closures are functions that are produced dynamically and returned by other functions. Even when the enclosing function has returned and finished executing, they have full access to the variables and names defined in the local namespace where the closure was formed. Closure factory functions are covered in this section.

When you return an inner function object in Python, the interpreter bundles the function with its surrounding environment or closure. All variables and names defined in the contained scope are saved in the function object. There are three steps to defining a closure:

  • Make an inside function
  • Variables from the enclosing function are used as references
  • The inner function is returned

With this foundation, you can begin generating closures right away and take advantage of their most important feature: state preservation between function calls.

In a Closure, Retaining State

When invoked, a closure causes the inner function to keep the state of its environment. The inner function, not the inner function alone, but the inner function and its encompassing environment, is the closure. The closure catches and preserves the local variables and names in the containing function.

Consider the following illustration:

# powers.py

def generate_power(exponent):
    def power(base):
        return base ** exponent
    return power

what’s going on in this function:

generate power(), a closure factory function, is created on line 3. Each time it is called, it produces a new closure and then returns it to the caller.

power() is an inner function defined on line 4 that takes a single argument, base, and returns the result of the expression base ** exponent.

Without calling power, line 6 returns it as a function object.

Where does the value of exponent come from in power()? It is where the conclusion is made. In this case, the value of the exponent is obtained from the outer function to generat_power(). When you use generate_power() in Python, it accomplishes the following:

  • Create a new instance of power() with a single base argument.
  • Would you please take a picture of the power() environment, which includes the exponent and its current value?
  • Return power() together with its entire environment.
  • In this manner, you can see that the method remembers the value of exponent when you call the instance of power() returned by generate_power():
 from powers import generate_power

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

 raise_two(6)

 raise_two(7)


 raise_three(6)

 raise_three(8)

raise_two() remembers that exponent=2 in these cases, and raise_three() remembers that exponent=3 in these examples.

Between calls, both closures remember their respective exponents.

Consider the following scenario:

def has_permission(page):
     def permission(username):
         if username.lower() == "superuser":
             return f"'{username}' has access to {page}."
         else:
             return f"'{username}' doesn't have access to {page}."
     return permission


 check_admin_page_permision = has_permission("Administrative Page")

 check_admin_page_permision("superuser")

 check_admin_page_permision("ken")

The inner function determines whether a given user has the necessary rights to view a specific page. You could immediately change this to verify if the user is in session and has the proper credentials to visit a specific route.

Instead of checking if the user has the permission “superuser,” you might query a SQL database to check the permission and then return the appropriate view based on the credentials.

Closures that don’t change their enclosing state or closures with a static enclosing state, as seen in the examples above, are prevalent. You may also use mutable objects like dictionaries, sets, and lists to create closures that change their enclosing state.

Assume you need to find the mean of a set of data. It would help if you had your function to remember the previous measurements between calls since the data comes in a stream of successive measurements of the parameter under investigation. You can write a closure factory function like this in this case:

def mean():
  sample = []
  def inner_mean(number):
    sample.append(number)
    return sum(sample) / len(sample)
  return inner_mean


 test_mean = mean()
  
 test_mean(300)

 test_mean(205)

 test_mean(401)

 test_mean(140)





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
     
    # Attach 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(3, 4)
point.get_x()

point.get_y()

point()

Point(3, 4)

point.set_x(52)
point.set_y(17)
point()
Point(52, 17)

make_point() returns a closure representing a point object in this case. This object is equipped with getter and setter functions. Those functions can be used to acquire read and write access to the variables x and y, specified in the enclosing scope and sent with the closure.

Even though this function generates potentially faster closures than equivalent classes, you should be mindful that it lacks essential features such as inheritance, properties, descriptors, and class and static methods. Check out Simple Tool for Simulating Classes Using Closures and Nested Scopes if you want to learn more about this concept (Python Recipe).

Building Helper Inner Functions

You can have a function that executes the same block of code multiple times throughout its body. Assume you wish to develop a function to process a CSV file containing information about New York City’s Wi-Fi hotspots. Create the following script to get the total number of hotspots in New York as well as the firm that offers the majority of them:

# hotspots.py

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"There are {len(hotspots)} Wi-Fi hotspots in NYC.\n"
            f"{counter.most_common(1)[0][0]} has the most with "
            f"{counter.most_common(1)[0][1]}."
        )

    if isinstance(file, str):
        # Got a string-based filepath
        file_obj = open(file, "r")
        most_common_provider(file_obj)
    else:
        # Got a file object
        most_common_provider(file)

process_hotspots() takes a file as an input in this case. The function determines whether the file is a file object or a string-based path to a physical file. Then it runs the most_common_provider() helper inner method, which takes a file object and performs the following operations:

Read the contents of the file into a csv-based dictionary generator.

DictReader

  • Make a list of Wi-Fi service providers
  • Using collections, count the number of Wi-Fi hotspots per provider
  • The object acts as a counterbalance
  • Print a message with the information that was retrieved

When you run the function, you’ll see the following results:

from hotspots import process_hotspots
file_obj = open("./NYC_Wi-Fi_Hotspot_Locations.csv", "r")
process_hotspots(file_obj)
process_hotspots("./NYC_Wi-Fi_Hotspot_Locations.csv")

Whether you call process_hotspots() with a string-based file path or a file object, the result is the same.

Inner Helper Functions vs. Private Helper Functions

You usually write auxiliary inner functions like the most common provider(). You can also create inner functions if you believe you won’t use them elsewhere other than the contained function.

Although creating your helper functions as inner functions achieve the desired purpose, extracting them as top-level functions is preferable. In this situation, you may specify that the function is private to the current module or class by including a leading underscore (_) in its name. It allows you to access and reuse your helper functions from wherever else in the current module or class.

It is possible to make your code clearer and more understandable by extracting core functions into top-level private functions. As a result of this technique, functions that follow the single-responsibility concept can be created.

Encapsulation of Information

When you need to shield, or conceal a particular function from everything that happens outside of it, so that it is completely hidden from the global scope, you can utilize inner functions. Encapsulation is the term for this type of action.

Here’s an example that exemplifies the point:

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

 increment(10)

You can’t use inner_increment() directly in this case. If you attempt to do so, you will receive a NameError. Because increment() completely hides inner_increment() from the global scope, prohibiting you from using it.

Conclusion

You create an inner function called a nested function when you declare a function inside another function. Inner functions have immediate access to variables and names defined in the surrounding function in Python. You can build helper functions, closures, and decorators using this approach.

You learned how to do the following in this tutorial:

  • By nesting functions inside other functions, you can provide encapsulation
  • Create helper functions to repurpose code
  • Implement factory closure functions that keep state between calls
  • Create decorator functions to add additional capabilities

You’re now ready to use inner functions in your code for a variety of other additional purposes.

You may also like

Leave a Comment

This website uses cookies to improve your experience. We'll assume you're ok with this, but you can opt-out if you wish. Accept Read More