Decorators (Livehacking Screenplay)

Closures Recap

def decorator(param):
    def wrapper():
        print('wrapper called, param =', param)
        return param
    return wrapper

eins = decorator(1)
zwei = decorator(2)
blah = decorator('blah')

print('eins', eins())
print('zwei', zwei())
print('blah', blah())
$ python code/10-closures-recap.py
wrapper called, param = 1
eins 1
wrapper called, param = 2
zwei 2
wrapper called, param = blah
blah blah
  • ignore names decorator, wrapper

    • think outer and inner, or foo and bar

  • decorator semantics will come next

Simple Decorator: Function Without Args

def decorator(func):
    def wrapper():
        print('wrapper called, func =', func.__name__)
        return func()
    return wrapper

def f1():
    print('f1 called')
    return 1

def f2():
    print('f2 called')
    return 2

f1 = decorator(f1)
f2 = decorator(f2)

print('f1 returned', f1())
print('f2 returned', f2())
$ python code/20-decorator-no-args.py
wrapper called, func = f1
f1 called
f1 returned 1
wrapper called, func = f2
f2 called
f2 returned 2
  • Modify decorator to take a function as parameter

  • print return values, and wrapping information

Decorators are Syntactic Sugar

def decorator(func):
    def wrapper():
        print('wrapper called, func =', func.__name__)
        return func()
    return wrapper

@decorator
def f1():
    print('f1 called')
    return 1

@decorator
def f2():
    print('f2 called')
    return 2

print('f1 returned', f1())
print('f2 returned', f2())
$ python code/30-decorator-syntactic-sugar.py
wrapper called, func = f1
f1 called
f1 returned 1
wrapper called, func = f2
f2 called
f2 returned 2
  • Replace explicit wrapper call to decorator with “@” directly before function definitions

  • Simply call functions (they are automatically wrapped)

  • ⟶ exactly the same

  • That’s basically it!

*args, **kwargs: A Debug-Decorator

def debug(func):
    def wrapper():
        print('debug: func =', func.__name__)
        return func()
    return wrapper

@debug
def add(l, r):
    return l+r

@debug
def sub(l, r):
    return l-r

print('add(1,2) returned', add(1,2))
print('sub(1,2) returned', sub(1,2))
$ python code/40-debug-starargs-wrong.py
Traceback (most recent call last):
  File "code/40-debug-starargs-wrong.py", line 15, in <module>
    print('add(1,2) returned', add(1,2))
TypeError: wrapper() takes 0 positional arguments but 2 were given
  • rename decorator to debug ⟶ no change, basically

  • but: give f1 and f2 parameters l and r, and rename them to add and sub

  • call program unmodified ⟶ missing parameters

def debug(func):
    def wrapper(*args, **kwargs):
        print('debug: func =', func.__name__, args, kwargs)
        return func(*args, **kwargs)
    return wrapper

@debug
def add(l, r):
    return l+r

@debug
def sub(l, r):
    return l-r

print('add(1,2) returned', add(1,2))
print('sub(1,2) returned', sub(1,2))
$ python code/45-debug-starargs-right.py
debug: func = add (1, 2) {}
add(1,2) returned 3
debug: func = sub (1, 2) {}
sub(1,2) returned -1
  • Fix it: give wrapper 2 args

  • think exactly: wrapper was given 2 arguments (we fix this)

  • why?

  • ⟶ it is the function that is actually called (returned by the decorator)

Sideway: functools.wraps

def debug(func):
    def wrapper(*args, **kwargs):
        print('debug: func =', func.__name__, args, kwargs)
        return func(*args, **kwargs)
    return wrapper

@debug
def add(l, r):
    return l+r

@debug
def sub(l, r):
    return l-r

print('add name:', add.__name__)
print('sub name:', sub.__name__)
$ python code/50-wrapper-name-ugly.py
add name: wrapper
sub name: wrapper
  • instead of printing the return value of the wrapped functions, print the names of the wrappers

  • ⟶ both called “wrapper”

import functools

def debug(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print('debug: func =', func.__name__, args, kwargs)
        return func(*args, **kwargs)
    return wrapper

@debug
def add(l, r):
    return l+r

@debug
def sub(l, r):
    return l-r

print('add name:', add.__name__)
print('sub name:', sub.__name__)
$ python code/55-wrapper-name-pretty.py
add name: add
sub name: sub
  • better ⟶ copies metadata over

Class Decorator: debug() with prefix

import functools

def debug(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print('debug: func =', func.__name__, args, kwargs)
        return func(*args, **kwargs)
    return wrapper

@debug
def add(l, r):
    return l+r

@debug
def sub(l, r):
    return l-r

print('add name:', add.__name__)
print('sub name:', sub.__name__)
$ python code/60-class-decorator.py
wtf: func = add, (1, 2), {}
add(1,2) =  3
gosh: func = sub, (1, 2), {}
sub(1,2) =  -1
  • start with decorator usage: @debug('wtf')

  • see how this can be done … think … discuss

  • ⟶ decorator must keep state

  • ⟶ an object of a class

  • hack it!. Explain errors as they are encountered, and fix.