Closure — Python
I know which side of class I was sitting when closure topic came in Programming Languages class in Junior year of college. Having come across so many details of programming languages from currying to machine instructions, I was not that happy when closure came. I was fine with function and method.
Well, closure is more interesting, cool and also useful than what I might have initially sighed about.
In this article, we’ll go over variable scopes — global scope, local scope and nonlocal scope, and how closure works with variables of different scopes to make it interesting.
Global Scope
It is a module scope. It spans a single file only. There is no concept of truly global, available in all modules, scope. The only exception to this are built-in globally available objects, such as True
, False
, None
, print
, dict
, help
etc.
Global scopes are nested inside a built-in scope.
A namespace is a structure that contains unique name for every objects in Python. The objects could be a variable or a method.
You can use globals()
built-in function in builtin
module to see objects in global space.
The globals()
function returns a dictionary containing a current scope’s global variables. We also looked up __builtins__
key from the dictionary to see what all objects it contains.
Local Scope:
When we create functions, we can create variable names inside those functions using assignments. eg count = 0
. These variables are not created until the function is called and are scoped to that each individual function that contains them.
An actual object a variable references can be different for each function call which is also the case in recursion.
Let’s see an example:
On running the file, we get:
local()= {'a': 3, 'b': 'd', 'c': 'ddd'}local()= {'a': 200, 'b': 100, 'c': 20000}
We used locals()
built-in function to see local variables inside the multiply
function. We called it twice and each time the function’s local is different.
Let’s combine local and global scope in another example:
On running the file, we get:
function count #1's local()= {'a': 3, 'b': 'd', 'c': 'ddd'}function count #2's local()= {'a': 200, 'b': 100, 'c': 20000}
Now let’s visualize the code with scope:
We have variables in all built-in
, global
, and local
scopes. We had to use global
keyword to indicate that variable count
is of global scope, therefore preventing UnboundLocalError
exception.
Nonlocal scope
It is a scope of enclosing function and not declared in a function where it’s being used.
Let’s see an example:
On running the function, we get:
[BEFORE] outer_func's x's value: Harry, id: 140234708990448
inner_func's x's value: Potter, id: 140234709483120
[AFTER] outer_func's x's value: Harry, id: 140234708990448
The value of x
in inner_func
function is Potter
and its id
is different than that of outer_func
both before and after the function call. When the inner_func
is compiled, Python sees an assignment to x
. Hence, it determines that x
is a local variable to inner_func
.
Let’s checkout another example:
On running the file, we get:
[BEFORE] outer_func's x's value: Harry, id: 140307857914352
inner_func's x's value: Potter, id: 140307858406960
[AFTER] outer_func's x's value: Potter, id: 140307858406960
Here we see that outer_func
‘s x
‘s value after inner_func
is called is changed to Potter
. Inside the inner_func
function, we used nonlocal
keyword to refer to a variable enclosing it, and modified its value. Hence, it is reflected both in inner_func
and after the inner_func
function call in the outer_func
.
Notice that variable x
‘s id value is same in inner_func
and after inner_func
function call in outer_func
function scope. This is because string is immutable and if we want to update its reference a new object is created and there’s an update in the reference.
This is what’s happening pictographically.
Just for completion, you wouldn’t need to use nonlocal
keyword if you want to refer to enclosed scope’s variable that is mutable because reference is still the same.
Here’s an example:
On running the file, we get:
[BEFORE]: dict_keys(['english', 'mandarin', 'french', 'italian', 'swahili', 'german']), id: 140654542806592
[INNER]: dict_keys(['english', 'mandarin', 'french', 'italian', 'swahili', 'german', 'motswana']), id: 140654542806592
[AFTER]: dict_keys(['english', 'mandarin', 'french', 'italian', 'swahili', 'german', 'motswana']), id: 140654542806592
Look, the id
‘s value is same in all before, inner, and after greet
inner function call.
Those are the background knowledge required to understand Closure. Let’s dive in!
Closure:
Closure is a function with an extended scope that contains free variables. A teaser, we already have come across closure. The examples in nonlocal scope is a closure itself.
Let’s see an example with an illustration:
On running the file, we get:
19*2=38
19*3=57
Here the variable num
is shared between 2 scopes — multiplication_table
and multiply
functions. The num
is a free variable for function multiply.
However, num
references the same value. Python does this my creating a cell as an intermediate object. When requesting value of num
, Python first looks at the intermediate cell and then go to the object the cell points to get value, “double-hop”.
This diagram illustrates the process:
This is how it works in Python console. We can look at closure’s information by using __closure__
attribute on a function that is enclosed. There is also __code__.co_free_vars
attribute to find free variables in a closure.
Hope this gives you enough understanding to like back closure and face them friendly. 😌
Want a small challenge? Write a fibonacci series using closure. Good luck!
This is my solution.
That’s all for this topic.
Thank you for reading and congratulations on the completion! ☀️
My next article most probably will be on decorators. See you then!
Mmm… I was sitting in one of that single row of sittings that faced perpendicular to white board. 😁
Inspiration:
You can support me on Patreon! Thank you.