In [1]:
from IPython.display import Image

def decorators(f):
    return f

In [2]:
@decorators
def holidays():
    print("PyATL Decmember 2014")

In [3]:
Image("resources/present.jpg")


Out[3]:

In [4]:
Image("resources/wrapped_presents_2.jpg")


Out[4]:

What we'll cover

  • Foundations
  • Decorators
  • Patterns
  • Problems

First Class & Higher Order Functions


In [5]:
my_func = dir
funcs = [enumerate, list, int]
map(float, [1, 2, 3])

def returns_help():
    return help

Closures


In [6]:
def outer(x=4):
    def inner(y=0):
        return x+y
    return inner

o = outer()
print(o())
print(o.__closure__) #func_closure in Py2


4
(<cell at 0x7f117832b198: int object at 0x9f8860>,)

What are decorators?

  • Defined in PEP 318 (Python 2.4)
  • Callables that:
    • Take a callable
    • Do something
    • Return a callable

Why?

  • Add or remove logic from functions
  • Registration
  • Pre- or Post-processing
  • Callbacks
  • Many Others!

Syntax


In [7]:
def awesome(f):
    return f

@awesome
def my_func():
    pass

my_func = awesome(my_func)

A Basic Decorator


In [8]:
from time import time
from functools import wraps

def timeit(f):
    @wraps(f)
    def timer(*args, **kwargs):
        start = time()
        res = f(*args, **kwargs)
        print(time() - start)
        return res
    return timer

@timeit
def hello():
    print("Hello!")
    
hello()


Hello!
2.0503997802734375e-05

Useful Patterns

These things will make your life easier

Pass through decorators


In [9]:
def make_awesome(f):
    f.awesome = True
    return f

@make_awesome
def thing():
    return "Thing()"

assert hasattr(thing, "awesome")

Flexible Closures


In [10]:
def brittle(f):
    def wrapper(arg1, arg2, kwarg1=None, kwarg2=None):
        return f(arg1, arg2, kwarg1, kwarg2)
    return wrapper

In [11]:
def flexible(f):
    def wrapper(*args, **kwargs):
        return f(*args, **kwargs)
    return wrapper

Important because...


In [12]:
@flexible
def dinner(food, beer): # actually becomes wrapper
    print("Eating", food, "and drinking", beer)

dinner("Bratzel", "Azazel")
print(dinner.__name__)


Eating Bratzel and drinking Azazel
wrapper

Preserving Metadata

Move it over manually


In [13]:
def loggit(f):
    def logger(*args, **kw):
        print("Calling {}.".format(f.__name__))
        return f(*args, **kw)
    logger.__name__ = f.__name__
    logger.__doc__ = f.__doc__
    logger.__dict__.update(f.__dict__)
    return logger

Python can do this for us


In [14]:
from functools import wraps

def loggit(f):
    @wraps(f)
    def logger(*args, **kwargs):
        print("Calling {}.".format(f.__name__))
        return f(*args, **kwargs)
    return logger

@loggit
def random():
    """Chosen by fair dice roll."""
    return 4

print(random.__name__)
print(random.__doc__)


random
Chosen by fair dice roll.

What is @wraps?

  • wraps uses update_wrapper
  • update_wrapper moves metadata from one object to another
    • __name__, __doc__, __module__, __dict__
    • __qualname__ and __annotations__ in Py3
    • __wrapped__ in >= 3.2

Optional kwargs

Probably the most common pattern


In [15]:
def before(phrase="Setting up"):
    def actual_deco(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            print(phrase)
            return f(*args, **kwargs)
        return wrapper
    return actual_deco

In [16]:
@before(phrase="much setup!")
def does_nothing():
    print("I did nothing.")
    
@before() # parens must be present
def add_two(x):
    return x + 2

Why not a partial?


In [17]:
from functools import partial

def before(f=None, phrase="Setting up"):
    if f is None:
        return partial(before, phrase=phrase)
    
    @wraps(f)
    def wrapper(*args, **kwargs):
        print(phrase)
        return f(*args, **kwargs)
    
    return wrapper

In [18]:
@before(phrase="much setup!")
def does_nothing():
    print("I did nothing.")
    
@before # parens not required
def add_two(x):
    return x + 2

Decorators all the way Down


In [19]:
def optional_kwargs(deco):
    @wraps(deco)
    def wrapper(f=None, **kwargs):
        if f is None:
            return partial(wrapper, **kwargs)
        return deco(f, **kwargs)
    return wrapper

In [20]:
@optional_kwargs
def before(f, phrase="Setting up"):
    @wraps(f)
    def wrapper(*args, **kwargs):
        print(phrase)
        return f(*args, **kwargs)    
    return wrapper

@before
def add_two(x):
    return x + 2

add_two(4)


Setting up
Out[20]:
6

Decorating Classes

  • Introduced in Python 2.6
  • PEP 3129

In [21]:
from functools import total_ordering

@total_ordering
class Frob:
    
    def __init__(self, bar):
        self.bar = bar
        self.baz = bar * 2
    
    def __eq__(self, other):
        (self.bar + self.baz) == (other.bar + other.baz)
    
    def __gt__(self, other):
        (self.bar + self.baz) > (other.bar + other.baz)
        
Frob(4) < Frob(2)


Out[21]:
True

Using Objects To Decorate

  • One instance per decoration
  • One instance for all decoration

In [22]:
from functools import update_wrapper

class wrapperobj:
    def __init__(self, f):
        update_wrapper(self, f)
        self.f = f
    
    def __call__(self, *args, **kwargs):
        return self.f(*args, **kwargs)

@wrapperobj
def random():
    return 4
print(random())


4

In [23]:
class singlewrapper:
    
    def __call__(self, f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            return f(*args, **kwargs)
        return wrapper

wrapper = singlewrapper()

@wrapper
def random():
    return 4
print(random())


4

Decorating Methods

  • Easy with functional closures
  • Harder with objects

In [24]:
class Foo:
    @loggit
    def frob(self, n=2):
        print(" ".join(["Frob"] * n))

Foo().frob(n=2)


Calling frob.
Frob Frob

In [25]:
class Foo:
    @wrapperobj
    def frob(self, n=1):
        print(" ".join(["Frob"] * n))

Foo().frob


Out[25]:
<__main__.wrapperobj at 0x7f1178357828>

Non-Data Descriptors In One Slide

  • Instances that live on classes
  • __get__ serves as the closure
  • Partially apply the method
  • Python handles the rest

In [26]:
class methodwrapper:
    def __init__(self, method):
        self.method = method
    
    def __get__(self, instance, cls):
        if instance is None:
            return self.method
        return partial(self.method, instance)

In [27]:
class Foo:
    @methodwrapper
    def frob(self, n=1):
        print(" ".join(["Frob"] * n))

Foo().frob(n=2)


Frob Frob

Stacking decorators


In [28]:
Image("resources/Star-Wars-Nesting-Dolls-02.jpg")


Out[28]:

In [29]:
@wrapperobj
@before(phrase="This was chosen by a fair dice roll.")
@make_awesome
def random(*args, **kwargs):
    return 4

print(hasattr(random, 'awesome'))
print(random.__name__)


True
random

An aside

  • Bottom up evaluation
  • Use wraps or update_wrapper
  • Descriptors (@property) last

Problems

These things will make your life harder

Testing

  • :(
  • Original object hidden
  • Testing the whole mechanism

Introspection

  • Wrong argument signature
  • Wrong source code
  • type and isinstance return the wrong results w/ objects

In [30]:
import inspect

print(inspect.getargspec(dinner))
print(inspect.getsource(dinner))


ArgSpec(args=[], varargs='args', keywords='kwargs', defaults=None)
    def wrapper(*args, **kwargs):
        return f(*args, **kwargs)

Blowing Stuff Up

  • Inheritance
  • inspect doesn't take too kindly to objects (Mostly Py2)

Fixing

  • __wrapped__ or inspect.unwrap
  • Objects, Descriptors and Proxies

In [31]:
print(random.__wrapped__.__name__)
print(inspect.getsource(inspect.unwrap(random)))


random
@wrapperobj
@before(phrase="This was chosen by a fair dice roll.")
@make_awesome
def random(*args, **kwargs):
    return 4


In [32]:
print(inspect.getsource(Foo.frob))


    @methodwrapper
    def frob(self, n=1):
        print(" ".join(["Frob"] * n))

Just Because You Can Doesn't Mean You Should


In [33]:
Image("resources/ian-malcolm.jpg")


Out[33]:

In [34]:
from itertools import repeat

@repeat
def frob():
    return 4

print(next(frob))


<function frob at 0x7f11780d89d8>

The Truth

  • Don't be scared!
  • They're easy and useful
  • Use a library if needed
  • pip install --user wrapt decorator