[Python] How Closures and Decorators in Python Work Together to Simplify Code
![[Python] How Closures and Decorators in Python Work Together to Simplify Code](https://cdn.hashnode.com/res/hashnode/image/upload/v1734617163779/1c4e85ae-37c8-42fe-9022-46dc16987a2b.png)
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).
ais declared in the outer function’s scopefunc2is declaredfunc1returnsfunc2. Now func1 is closed.Since
func1returnsfunc2,f = func1is 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 becausefunc1uses 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_clockdecorator.performance_clockreceivestime_funcas an argument. Therefore,performance_clock(time_func).performance_clockreturnsperformance_clocked.performance_clockedreceives 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.
![[CS fundamentals] Greedy - Dijkstra, Kruskal and Prim's Algorithm](https://cdn.hashnode.com/uploads/covers/644c253f17f6efe1e02ade41/f62b4d41-5a31-4cd3-9d44-a7004f4624b9.jpg)
![[CS fundamentals] Linked List Creation & Insertion explained in C.](https://cdn.hashnode.com/uploads/covers/644c253f17f6efe1e02ade41/6a74e5c0-44ca-4110-9ee7-e8e67767a9dc.png)
![[CS Fundamentals] Pointers in C](https://cdn.hashnode.com/uploads/covers/644c253f17f6efe1e02ade41/6f81f5a6-d87c-4382-883f-fa4d0a8b5968.png)
![[SQL] Writing Efficient SQL Queries](https://cdn.hashnode.com/res/hashnode/image/upload/v1751292603168/b0b43c7d-8e08-47b2-a64c-b600e3511831.jpeg)
![[CS Fundamentals] Binary Search](https://cdn.hashnode.com/res/hashnode/image/upload/v1747041878054/c8d8adb9-097d-484b-9562-f43e4bce9a46.png)