Closures
Updating the value of an immutable variable
We need to use the nonlocal
keyword to update the value of an immutable variable (e.g. int
, float
, etc).
def make_counter():
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
>>> counter = make_counter()
>>> counter()
1
>>> counter()
2
>>> counter()
3
Retaining state between function calls
We are using now a mutable variable (a list). We don’t need to use any keyword in this case:
def cumulative_average():
data = []
def average(value):
data.append(value)
return sum(data) / len(data)
return average
>>> sa = cumulative_average()
>>> sa(12)
12.0
>>> sa(13)
12.5
>>> sa(11)
12.0
Writing decorators
A decorator is a callable that takes another function as an argument.
def decorator(function):
def closure():
print("Before calling the function")
function()
print("After calling the function")
return closure
@decorator
def greet():
print("Hello")
>>> greet()
Before calling the function
Hello
After calling the function
Stacking decorators
Stacking decorators is the same as nesting functions. The following pieces of code are equivalent:
@alpha
@beta
def f():
...
def f():
...
>>> f = alpha(beta(f))
Implementing memoization
def memoize(function):
cache = {}
def closure(number):
if number not in cache:
cache[number] = function(number)
return cache[number]
return closure
import time
import timeit
@memoize
def slow_op(number):
time.sleep(1)
>>> timeit.timeit("[slow_op(number) for number in [2,3,4,2,3,4]]", globals=globals(), number=1)
3.0022875580007167
>>> timeit.timeit("[slow_op(number) for number in [2,3,4,2,3,4]]", globals=globals(), number=1)
7.003999598964583e-06
Cache solutions in the standard library
Python >= 3.9: functools.cache
@functools.cache
def fibonacci(n):
...
Python 3.8: functools.lru_cache
@functools.lru_cache
def fibonacci(n):
...
Python >= 3.2: lru_cache
@lru_cache()
def fibonacci(n):
...
Using a class as an alternative to a closure
class Averager():
def __init__(self):
self.series = []
def __call__(self, value):
self.series.append(value)
return sum(self.series) / len(self.series)
>>> avg = Averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0