Codementor Events

A Dive Into Python Closures and Decorators - Part 1

Published Jul 03, 2017Last updated Jul 11, 2024
A Dive Into Python Closures and Decorators - Part 1

We will be looking at local functions, the concepts of closure and what decorators are, sounds like a lot? Then let's dive into it.

Local functions

Probably most functions you are familiar with are either defined in module/global scope or within classes i.e methods. However Python allows us to define our functions in a local scope i.e within a function.
Knowing this, local functions are functions defined within another function. We say these functions are defined to the scope of a specific function e.g

def my_function():
  def my_local_function():
    print('I am a local function, local to my_function')
  print('I am not a local function')

The function my_local_function is a local function as it is only available inside my_function and can only be used within the function.
Let's look at another example:

def remove_first_item(my_array):
  def get_first_item(s):
    return s[0]
  my_array.remove(get_first_item(my_array))
  return my_array
    
print(remove_first_item(['1','2','3'])))	# ['2','3']
print(get_first_item([1,2,3,4]))
# NameError: name 'get_first_item' is not defined

From the above, we see that calling get_first_item throws an error. This is because it can only be accessed within the remove_first_item function, that makes it a local function.

Local functions can also be returned from functions. Returning a local function is similar to returning in any other object. Let's take a look:

def enclosing_func():
  def local_func():
    print ('I am a local function')
  return local_func()
 
 print(enclosing_func())	# I am a local function'

Local functions are subject to the same scoping rules as other functions, this brings to the LEGB rule for name look up in python - checking starts with the local scope, then the enclosing, then global and finally the built-in scope
LEGB - Local, Eclosing, Global, Built-in

x = 'global'
def outer_func():
  y = 'enclose'
  def inner_func():
    z = 'local'
    print(x, y, z)
  inner_func()
  
 print(outer_func())	# global enclose local

Local functions can be used when there is need for specialized functions. They also help with code organization and readability.

Closure

The local functions we have looked at so far have no definite way of interacting with the enclosing scope, that is about to change. Local functions can make use of variables in their enclosing scope, the LEGB rule makes this possible.

def outer_func():
  x = 5
  def inner_func(y = 3):
    return (x + y)
  return inner_func
a = outer_func()
 
print(a())	# 8

From the above, we see that the inner_func function makes reference to the outer_func fucntion for the value of x. The local function is able to reference the outer scope through closures. Closures maintain references to objects from the earlier scope.

Closure is commonly used in what is referred to as Function Factory - these are functions that return other functions. The returned functions are specialized. The Function Factory takes in argument(s), creates local function that creates its own argument(s) and also uses the argument(s) passed to the function factory. This is possible with closures

def multiply_by(num):
  def multiply_by_num(k):
    return num * k
  return multiply_by_num
  
five = multiply_by(5)
print(five(2))	# 10
print(five(4))	# 20
 
decimal = multiply_by(10)
print(decimal(20))	# 200
print(decimal(3))	# 30

Here we see that the local function multiply_by_num takes an argument k and returns the argument multiplied by the argument of it's enclosing function. Also, multply_by fuction takes an argument num and returns the function that multiplies num by its argument.

LEGB rule does not apply when new name bindings happen

text = "global text"
def outer_func():
  text = "enclosing text"
  def inner_func():
    text = "inner text"
    print('inner_func:', text)	# inner_func: global text
  print('outer_func:', text)	# outer_func: enclosing text
  inner_func()
  print('outer_func:', text)	# outer_func: enclosing text
 
print('global:', text)	# global: global text
outer_func()
print('global:', text)	# global: global text

In the inner_func function, we have created a new name binding by re-assiging the variable text in the function scope. Calling the inner_func does not affect the variable text in the outer_func, likewise calling outer_func does not affect the global variable text.

global

global is a python keyword that introduces names from global namespace into the local namespace. From the previous code, we can make the inner_func to modify the variable text rather than create a new one.

text = "global text"
def outer_func():
  text = "enclosing text"
  def inner_func():
  	global text	# binds the global text to the local text
  	text = "inner text"
  	print('inner_func:', text)	# inner_func: inner text
  print('outer_func:', text)	# outer_func: enclosing text
  inner_func()
  print('outer_func:', text)	# outer_func: enclosing text
 
print('global:', text)	# global: global text
outer_func()
print('global:', text)	# global: inner text

Since the global keyword has binded the global text variable to the local text variabel, calling the outer_func function makes changes to the global text, hence, reassigning the variable text in the global scope.

nonlocal

The nonlocal keyword allows us to introduce names from enclosing namespace into the local namespace. Still looking at the previous code:

text = "global text"
def outer_func():
  text = "enclosing text"
  def inner_func():
  	nonlocal text	# binds the local text to the enclosing text
  	text = "inner text"
  	print('inner_func:', text)	# inner_func: inner text
  print('outer_func:', text)	# outer_func: enclosing text
  inner_func()
  print('outer_func:', text)	# outer_func: inner text
 
print('global:', text)	# global: global text
outer_func()
print('global:', text)	# global: global text

Here, the enclosing text variable changes when the inner_func was called.

Decorators

Since we have understanding of local functions and closure, we can then look at function decorators.
Decorators are used to enhance existing functions without changing their definition. A decorator is itself a callable and takes in another callable to return another callable. Simply put - A decorator is a function that takes in another function and returns another function, although it is a bit more than just that.

# Syntax for decorator
 
@my_decorator
def my_function():
  pass

The result of calling my_function is passed into the my_decorator

def capitalize(func):
  def uppercase():
    result = func()
    return result.upper()
  return uppercase
 
@capitalize
def say_hello():
  return "hello"
  
print(say_hello())	# 'HELLO'

The result of calling say_hello is passed into the capitalize decorator. The decorator modifies the say_hello function by changing its result to uppercase. We see that capitalize decorator takes in a callable(say_hello) as an argument and returns another callable(uppercase). This is just a basic example on decorator

In the next post, we will continue with understanding how decorators work in python. In preparation for that, let's take a brief look at *args and **kwargs

*args and **kwargs

*args allows you to use any number of arguments in a function. You use it when you're not sure of how many arguments might be passed into a function

def add_all_arguments(*args):
  result = 0
  for i in args:
    result += i
  return result
    
print(add_all_arguments(1,5,7,9,10))	# 32
print(add_all_arguments(1,9))	# 10
print(add_all_arguments(1,2,3,4,5,6,7,8,9,10))	# 55
print(add_all_arguments(1))	# 1
print(add_all_arguments())	# 0

Like *args, **kwargs can take many arguments you would like to supply to it. However, **kwargs differs from *args in that you will need to assign keywords

def print_arguments(**kwargs):
  print(kwargs)
    
print(print_arguments(name = 'Moyosore'))	# {'name': 'moyosore'}
print(print_arguments(name = 'Moyosore'), country = 'Nigeria')
print(print_arguments())	# {}
# {'name': 'moyosore', 'country': 'Nigeria'}
 
def print_argument_values(**kwargs):
    for key, value in kwargs.items():
        print('{0}: {1}'.format(key, value))
 
print_argument_values(name="Moyosore", country="Nigeria")
# name: Moyosore
# country: Nigeria

Args and kwargs can be used together in a function, with args always coming before kwargs. If there are any other required arguments, they come before args and kwargs

def add_and_mul(*args, **kwargs):
  pass
 
def add_and_mul(my_arg, *args, **kwargs):
  pass
 
def add_and_mul(my_arg, my_arg_1, *args, **kwargs):
  pass

You can do more reading on args and kwargs for better understanding. This is just an example for you to have basic idea on args and kwargs.

Next post will be dealing with decorators in details!

Discover and read more posts from Moyosore Sosan
get started
post comments2Replies
Simon Kelly
7 years ago

Just a quick correction. Your example for nonlocal has 2 errors in it. It should read:

text = "global text"
def outer_func():
  text = "enclosing text"
  def inner_func():
    nonlocal text   # binds the local text to the enclosing text
    text = "inner text"
    print('inner_func:', text)  # inner_func: inner text
  print('outer_func:', text)    # outer_func: enclosing text
  inner_func()
  print('outer_func:', text)    # outer_func: inner text
 
print('global:', text)  # global: global text
outer_func()
print('global:', text)  # global: global text
Moyosore Sosan
7 years ago

Thanks, that was an oversigth on my end. Corrected