Closure

What is ‘Closure’?

In Wikipedia, Closure is described as follows.

In programming languages, a closure, also lexical closure or function closure, is a technique for implementing lexically scoped name binding in a language with first-class functions. Operationally, a closure is a record storing a function[a] together with an environment.[1] The environment is a mapping associating each free variable of the function (variables that are used locally, but defined in an enclosing scope) with the value or reference to which the name was bound when the closure was created.[b] Unlike a plain function, a closure allows the function to access those captured variables through the closure's copies of their values or references, even when the function is invoked outside their scope.

Before we deal with Closer, we need to know three concepts.

  • Nested function

  • First-class objects

  • Non-local

In Python, these three concepts are the basic elements that make up the closure.

Especially, if you are not familiar with non-local, I recommend you to study first about the concept of non-local.


Nested Function

First, let’s see example about nested function below.

def outer_func():  # 1
    message = 'Hi'  # 3
    def inner_func():  # 4
        print(message)  # 6
    return inner_func()  # 5

outer_func()  # 2

If we run this program, we can see that the string, “Hi” is printed.

What we need to note is that we can declare a function inside the function, and we can access the message, which is the variable of outer_func, from inner_func.

The process until the program print the string “Hi” is as follows.

  1. Calls the function outer_func defined in #1 from #2. Of course, outer_func doesn’t take any arguments.

  2. After outer_func is executed, The first thing to do is assign the string “Hi” to the variable ‘message’. (#3)

  3. In #4, define inner_func. In #5, Call the function inner_func and return the result of the function at the same time.

  4. In #6, refer to the message variable and print it. The variable message doesn’t defined inside of inner_func, it used inside of inner_func, so it called ‘free variable’.


First Class Object

In programming language, First Class Object means entity that supports operations that are generally applicable to all other objects within the language.

This operation usually …

  • Passed as a function’s parameter

  • Being return value of function

  • Presupposes things that are modified and allocated.

Let’s see simple example below.

def double(*args):
    return [i * 2 for i in args]


def execute(func, *args):
    return func(*args)

execute(double, [1, 2, 3, 4])

Function execute takes two parameter. First one is the function that apply to args, another is arguments that function will be applied.

In Python, function is also first class object like list, str, int.


Non-local

This concept is important. Let's start with a simple example.

z = 3

def outer(x):
    y = 10
    def inner():
        x = 1000
        return x

    return inner()

print(outer(10))

We already know that nested function is possible in Python.

In the above example, the code for the value of x is given twice.

The first is an arbitrary x received at the execution of the function, and the next is a variable x initializing to 1000 within the inner function.

What value should be returned when the parameter is given as 10 when calling a function?

The above issue is the problem about scope after all.

The area within the inner function is called the local scope.

All objects in the local area are under the control of the inner.

The area inside the outer but outside the inner is called a non-local scope.

The variable y of outer is a non-local scope variable from the inner point of view.

The area outside the outer function is the global scope.

The z variable is declared in global and will be referenced not only in the outer function but also in other codes or functions.

Each scopes has maximum interest in its own area and limited control over variables or objects in other areas.

Inner function may refer the variable y in non-local area.

It can be understood that ‘reading’ is possible for variables in the outer scope.

However, ‘writing’ is limited for areas other than one’s own.

def count(x):
    def increment():
        x += 1
        print(x)
    increment()

>>> count(5)

UnboundLocalError: local variable 'x' referenced before assignment

UnboundLocalError literally means that the local variable x was referenced before assignment.

There is always no problem with referring to or reading values for areas outside the local area, but modifying or assigning new values should not be written.

Python unconditionally assumes that x is a local variable that can be controlled by itself, unless otherwise stated in the case of a write code that modifies the value.

This is to prevent difficult bugs that touch external variables recklessly.

It is always good to clearly share the responsibilities and authority of the code area.

The function count in the example has one internal function, but in some cases it may have dozens of internal functions.

At this time, touching and modifying the state value of count for each internal function can lead to unexpected results.

If you want to update the value of nonlocal scope, you can use the nonlocal statement.

def count(x):
    def increment():
        nonlocal x #confirm that x is nonlocal variable, not local.
        x += 1
        print(x)

    increment()

count(5)

>>> 6

Now we can easily expect return value of the previous example.

z = 3

def outer(x):
    y = 10
    def inner():
        x = 1000
        return x

    return inner()

print(outer(10))

The function looks for the scope closest to it, which has the most control.

outer(1)
outer(10)
outer(100)

>>> 1000
>>> 1000
>>> 1000

Definition of Closure

In Python, closure means that the function that remembers the status value of the scope surrounding itself(namespace).

In order for a function to be a closure, the following three condition must be satisfied.

  1. The function must be a nested function within a function.

  2. The function must refer to the state value within the enclosure function.

  3. A function surrounding that function must return this function.

def in_cache(func):
    cache = {}
    def wrapper(n):
        if n in cache:
            return cache[n]
        else:
            cache[n] = func(n)
            return cache[n]
    return wrapper

Above is the example of the isolated module code to check for cache hits.

Before we actually apply this function, let's take a look at the wrapper function.

We have discussed above the features for being a closure of any function, and the wrapper function in the example satisfies all the conditions for being a closure.

  • Nested function within the in_cache function

  • It refers to the status value of cache for the in_cache scope that you are enclosing

  • The function surrounding itself is returning a wrapper!

First of all, wrapper is by definition a closure, so let's use this function properly.

Let's take a factorial function as an example and apply in_cache.

def in_cache(func):
    cache = {}
    def wrapper(n):
        if n in cache:
            return cache[n]
        else:
            cache[n] = func(n)
            return cache[n]
    return wrapper


def factorial(n):
    ret = 1
    for i in range(1, n+1):
        ret *= i
    return ret

factorial = in_cache(factorial)

The global scope contains numerous variables. Among them, there is also a factorial function.

This function is not a factorial function originally defined, but a function returned as a result of execution of in_cache(factorial), and this is the closure. (Condition No. 3 of the closure)

This closure has its own scope, so the cache state is not initialized every time it runs, but maintains a value. It's like a declaration in the entire space.

Curiously, in the original function definition, cache was declared outside the wrapper function scope.

Nevertheless, the wrapper (in this case, factorial) is accessed to cache, and the state can be stored and controlled within its scope.

The definition of Closer, "Closer is a function that remembers the state value of the scope (namespace) surrounding itself," means this.

In this example, the factorial scope may remember and control the state value of the enclosing scope.

Through this, the desired purpose can be achieved with a cache that is continuously managed and maintained, rather than being initialized every time a function is executed within its own scope.

Even if we delete the function in_cache from the memory, the factorial(wrapper) is callable without a problem.

Last updated