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