Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Decorators

A decorator is a feature that transforms each function call into something else. What do you think about that example?

def fibnaive(n: int) -> int:
    if n <= 1:
        return 1
    else:
        return fibnaive(n-1) + fibnaive(n-2)
fibnaive(35)
14930352

First example: write the calls

Our goal is to write a line into the terminal at each call of the function.

Bad solution: Hack the function itself

def fibnaive(n: int) -> int:
    print("fibnaive(", n, ")")
    if n <= 1:
        return 1
    else:
        return fibnaive(n-1) + fibnaive(n-2)
fibnaive(5)
fibnaive( 5 )
fibnaive( 4 )
fibnaive( 3 )
fibnaive( 2 )
fibnaive( 1 )
fibnaive( 0 )
fibnaive( 1 )
fibnaive( 2 )
fibnaive( 1 )
fibnaive( 0 )
fibnaive( 3 )
fibnaive( 2 )
fibnaive( 1 )
fibnaive( 0 )
fibnaive( 1 )





8

It works but it is not reusable.

Write a higher-order function

from typing import Callable, Any

def printed(f: Callable[..., Any]) -> Callable[..., Any]:
    """
    Return a version of f that prints the calls

    Arg:
        f (Callable): a function.

    Returns:
        (Callable): the version of f that prints f(..) at each call
    """
    def printerVersion(*args: Any):
        print(f.__name__, "(", *args, ")")
        return f(*args)
    return printerVersion
def fibnaive(n: int) -> int:
    if n <= 1:
        return 1
    else:
        return fibnaive(n-1) + fibnaive(n-2)
    
fibnaive = printed(fibnaive)
fibnaive(3)
fibnaive ( 3 )
fibnaive ( 2 )
fibnaive ( 1 )
fibnaive ( 0 )
fibnaive ( 1 )





3

Writing the decorator

@printed
def fibnaive(n: int) -> int:
    if n <= 1:
        return 1
    else:
        return fibnaive(n-1) + fibnaive(n-2)
fibnaive(3)
fibnaive ( 3 )
fibnaive ( 2 )
fibnaive ( 1 )
fibnaive ( 0 )
fibnaive ( 1 )





3

Writing a class instead of a function

class Printed:
    def __init__(self, f: Callable[..., Any]):
        self.f = f

    def __call__(self, *args: Any):
        print(self.f.__name__, "(", *args, ")")
        return self.f(*args)
@Printed
def fibnaive(n: int) -> int:
    if n <= 1:
        return 1
    else:
        return fibnaive(n-1) + fibnaive(n-2)
fibnaive(4)
fibnaive ( 4 )
fibnaive ( 3 )
fibnaive ( 2 )
fibnaive ( 1 )
fibnaive ( 0 )
fibnaive ( 1 )
fibnaive ( 2 )
fibnaive ( 1 )
fibnaive ( 0 )





5

Second example: printing the running time

We want measure how fibnaiveis slow.

Bad solution

import time

def fibnaive(n: int) -> int:
    if n <= 1:
        return 1
    else:
        return fibnaive(n-1) + fibnaive(n-2)
    
begin = time.time()
fibnaive(30)
end = time.time()
print("Total time: ", end - begin)
Total time:  0.15117168426513672

The above code is ugly and not reusable.

Writing a function

The code below is nice because it can be reused for any function.

def timedfunction(f):
    def timedf(*args, **kwargs):
        begin = time.time()
        result = f(*args, **kwargs)
        end = time.time()
        print("Total time: ", end - begin)
        return result
    
    return timedf
fibnaive = timedfunction(fibnaive)
fibnaive(3)
Total time:  7.152557373046875e-07
Total time:  2.1457672119140625e-06
Total time:  0.0013761520385742188
Total time:  4.76837158203125e-07
Total time:  0.001458883285522461





3

Attempt to write the decorator with a function

Instead of writing timedfunction(fibnaive) which is combersome, we try to decorate the function before its definition.

@timedfunction
def fibnaive(n):
    if n <= 1:
        return 1
    else:
        return fibnaive(n-1) + fibnaive(n-2)
fibnaive(3)
Total time:  4.76837158203125e-07
Total time:  4.76837158203125e-07
Total time:  0.00020813941955566406
Total time:  4.76837158203125e-07
Total time:  0.0002760887145996094





3
def timedfunction(f):
        
    def timedf(*args, **kwargs):
    
        begin = time.time()
        result = f(*args, **kwargs)
        end = time.time()
        print("Total time: ", end - begin)
        return result
    
    return timedf
@timedfunction
def fibnaive(n):
    if n <= 1:
        return 1
    else:
        return fibnaive(n-1) + fibnaive(n-2)
fibnaive(3)
Total time:  7.152557373046875e-07
Total time:  7.152557373046875e-07
Total time:  0.00014638900756835938
Total time:  4.76837158203125e-07
Total time:  0.00020551681518554688





3

Write the decorator with a class

class Timed:
    def __init__(self, f):
        self.f = f
        self.__name__ = f.__name__
        self.inside = False

    def __call__(self, *args):
        if not self.inside:
            self.inside = True
            begin = time.time()
            result = self.f(*args)
            end = time.time()
            print("Total time: ", end - begin)
            self.inside = False
            return result
        else:
            return self.f(*args)
@Timed
def fibnaive(n):
    if n <= 1:
        return 1
    else:
        return fibnaive(n-1) + fibnaive(n-2)
fibnaive(32)
Total time:  1.010364294052124





3524578

Memoization

The code for computing Fibonnaci numbers with a recursive function is so beautiful…

fibnaive(12)
Total time:  0.0001595020294189453





233

… but is so slow.

Direct code for memoized Fibonacci

It consists in storing the values already computed in a hashmap.

def fibmemo(n):
    valuesAlreadyComputed = {}

    def fibrec(n):
        if n in valuesAlreadyComputed:
            return valuesAlreadyComputed[n]
        if n <= 1:
            return 1
        else:
            valuesAlreadyComputed[n] = fibrec(n-1) + fibrec(n-2)
            return valuesAlreadyComputed[n]
        
    return fibrec(n)
fibmemo(35)
14930352

The previous code is ugly. And it handles two responsabilities: to compute the Fibonacci number, and to perform memoïsation. It should be seperated…

First attempt to write a decorator with a higher-order function

def memo(f):
    valuesAlreadyComputed = {}
    def fmemo(*args):
        if args not in valuesAlreadyComputed:
            valuesAlreadyComputed[args] = f(*args)
        return valuesAlreadyComputed[args]
       
    return fmemo
def fibnaive(n):
    if n <= 1:
        return 1
    else:
        return fibnaive(n-1) + fibnaive(n-2)

fibnaive = memo(fibnaive)
fibnaive(120)
8670007398507948658051921
@memo
def fibnaive(n):
    if n <= 1:
        return 1
    else:
        return fibnaive(n-1) + fibnaive(n-2)
fibnaive(120)
8670007398507948658051921
class Memoize:
    def __init__(self, f: Callable[..., Any]):
        self.f = f
        self.__name__ = f.__name__
        self.valuesAlreadyComputed: dict[Any, Any] = {}
        
    def __call__(self, *args: Any):
        if args not in self.valuesAlreadyComputed:
            self.valuesAlreadyComputed[args] = self.f(*args)
        return self.valuesAlreadyComputed[args]
@Memoize
def fib(n: int) -> int:
    if n <= 1:
        return 1
    else:
        return fib(n-1) + fib(n-2)
fib(120)
8670007398507948658051921

Combining decorators

@Printed
@Timed
@Memoize
def fib(n:int) -> int:
    if n <= 1:
        return 1
    else:
        return fib(n-1) + fib(n-2)
fib(5)
fib ( 5 )
Total time:  5.0067901611328125e-06





8

Exercices

  • Apply Dynamic Programming to solve Knapsack problem. Then apply memoization.
  • Write factorial in imperative form, recursive form and recursive with tail recursion and compare the running time.