What Is Closure?
A closure is created when a function (the inner function) is defined within another function (the outer function) and the inner function references variables from the outer function.
def func1():
a = []
def func2(v):
a.append(v)
return sum(a) / len(a)
return func2
f = func1
f(10)
It looks simple, but seems weired as well. You may wonder how func2
refers to a
even though func1
is completely finished after returning func2
? func2
should be excuted after func1
returns func2
, right?
Let’s see what happens when we call f(10).
a
is declared in the outer function’s scopefunc2
is declaredfunc1
returnsfunc2
. Now func1 is closed.Since
func1
returnsfunc2
,f = func1
is the same asf = func2()
f(10)
meansfunc2(10)
, but we are trying to refer toa
, which is the variable of outer function (and outer function is closed already). However, the program runs without any errors. It’s becausefunc1
uses a closure
Characteristics of a closure:
A function is a inner function of an otuer function.
The inner function refers to the variable in the outer function’s scope.
The outer function returns the inner function.
Free Variable
You must understand what is a free variable as it is related to decorators and closures. It is a variable that is used within a function but is not defined inside the function or passed as a parameter. Instead, it refers to a variable that exists in the surrounding scope, such as a global or nonlocal scope. Simply, it is the variable declared in the outer function’s scope.
Example:
def func1():
# Free variables
a = []
b = {}
def func2():
return
return
Deep Dive into Closure
def closure_ex():
# Free variable
series = []
def averager(v):
series.append(v)
print(f'inner >>> {series} / {len(series)}')
return sum(series) / len(series)
return averager
avg_closure = closure_ex()
avg_closure1(10)
avg_closure1(40)
print(avg_closure.__code__.co_freevars) # ('series',)
print(avg_closure.__closure__[0].cell_contents) # [10, 40]
We can check free variables with .__code__.co_freevars
We can also check the contents with .__closure__[0].cell_contents
Advantages of using closure:
- Encapsulation of State
- Closures allow you to encapsulate state within a function, so you can hide variables and data from the outside world. This makes it easier to manage and protect data while still providing controlled access through the closure.
- Data Hiding
- With closures, you can create private variables that are not directly accessible from outside the function. This mimics the concept of private variables in object-oriented programming.
- Improved Code Reusability
- Closures enable writing more reusable and modular code. You can define a function once and generate customized versions of it with different parameters, making the code more flexible and reducing redundancy.
- Memory Efficiency
- Since closures can capture the environment and maintain state without creating additional objects or storing state globally, they can be memory efficient in certain use cases.
Decorator
A decorator is a design pattern that allows you to modify or enhance the behavior of functions or methods without changing their actual code. This allows us not to waste time and memory. Simply, decoratr “decorates” our functions, adding extra things.
Example (Timing function execution):
def performance_clock(func):
def performance_clocked(*args):
start_time = time.perf_counter()
result = func(*args)
end_time = time.perf_counter() - start_time
name = func.__name__
arg_str = ', '.join(repr(arg) for arg in args)
print('[%0.5fs] %s(%s) -> %r' % (end_time, name, arg_str, result))
return result
return performance_clocked
@performance_clock
def time_func(seconds):
time.sleep(seconds)
@performance_clock
def sum_func(*numbers):
return sum(numbers)
time_func(1.5) # [1.50155s] time_func(1.5) -> None
sum_func(1, 2, 3, 4, 5) # [0.00000s] sum_func(1, 2, 3, 4, 5) -> 15
@performance_clock
← this part is a decorator. We can apply a decorator syntax with using @
symbol.
How it works:
time_func(1.5)
uses aperformance_clock
decorator.performance_clock
receivestime_func
as an argument. Therefore,performance_clock(time_func)
.performance_clock
returnsperformance_clocked
.performance_clocked
receives 1.5 as an argument.Then, things inside print will be printed.
Then, it wil return the the
result
.
Without the decorator, if you call time_func(1.5)
, nothing really happens. Just function will be finished after a few seconds that is assigned to the argument.