Intro

I'm Wil Langford. I like math, Python, and games.

me@github

This talk is located in my DecoratorsTalk2015 repository at:

https://github.com/wil-langford/DecoratorsTalk2015 (or http://goo.gl/AAJ7U0 for short)

To prepare for the talk, please clone the repository and load the iPython notebook. Let me know if you need any help with this step.

Concept roll call

  • function
  • docstring
  • function object
  • wrapper
  • decorator
  • @property
  • (optional) protocol
  • (optional) descriptor

Before we go any further, we need to choose a super-secure password.


In [ ]:
global PASSWORD
PASSWORD = "Guild o' Code"

function


In [ ]:
def halver(num):
    """Returns half of the 'num' argument."""  # docstring
    return num / 2

function object


In [ ]:
print "halver's name:", halver.__name__
print "halver's docstring:", halver.__doc__

In [ ]:
halver?

In [ ]:
print halver(20)

In [ ]:
print halver(10)

Uh-oh...


In [ ]:
print halver(5)

In [ ]:
print [i/2 for i in range(10)]

In [ ]:
print [i/2.0 for i in range(10)]

In [ ]:
print [float(i)/2 for i in range(10)]

wrapper


In [ ]:
def float_wrapper(func):
    def wrapper(float_me):
        return func(float(float_me))
    return wrapper

In [ ]:
print float_wrapper(halver)(10)
print float_wrapper(halver)(5)

In [ ]:
halve = float_wrapper(halver)

In [ ]:
print halver(10)
print halver(5)

In [ ]:
def halver(num):
    """Returns half of the 'num' argument.
    This is a reimplementation of halver()."""
    return num / 2
halver = float_wrapper(halver)

In [ ]:
print halve(5)

decorator


In [ ]:
@float_wrapper
def halver2(num):
    """Returns half of the 'num' argument.
    This is a re-reimplementation of halver()."""
    return num / 2

In [ ]:
print halver2(5)

Dust off your hands and kick back. We're completely, totally...


In [ ]:
print "halver's name:", halver.__name__
print "halver's docstring:", halver.__doc__

print "halver2's name:", halver2.__name__
print "halver2's docstring:", halver2.__doc__

In [ ]:
halver?

... not done yet.


In [ ]:
import functools

def better_float_wrapper(func):
    @functools.wraps(func)       # this line is the only difference
    def wrapper(num):
        return func(float(num))
    return wrapper

In [ ]:
@better_float_wrapper
def halver3(num):
    """Returns half of the 'num' argument.
    This is a re-reimplementation of halve()."""
    return num / 2

In [ ]:
print halver3(5)
print "halver3's name:", halver3.__name__
print "halver3's docstring:", halver3.__doc__

In [ ]:
halver3?

Usage of @property


In [ ]:
class StrictAttributeHolder(object):
    def __init__(self):
        self._int_val = None
        
    @property
    def int_val(self):
        if self._int_val is not None:
            return self._int_val
        else:
            raise Exception("Can't read what isn't written!")
    
    @int_val.setter
    def int_val(self, value):
        if isinstance(value, int):
            self._int_val = value
        else:
            raise TypeError("Can't set int_val to a non-int value!")

In [ ]:
sah = StrictAttributeHolder()

In [ ]:
print sah.int_val

In [ ]:
sah.int_val = 5

In [ ]:
print sah.int_val

In [ ]:
sah.int_val = 5.0

In [ ]:
sah.int_val = [5]

Create your own!


In [ ]:
# Create a @timed_function decorator that computes and prints the execution time of
# any function that it wraps.  Use *args and **kwargs to capture all function
# arguments.

def timed_function(func):
    # Your implementation here
    pass

In [ ]:
# Create a @case_mod decorator that gives any function that it wraps an
# all-lowercase version of an input string and then returns an all-uppercase
# version of the wrapped function's output

def case_mod(func):
    # Your implementation here
    pass

In [ ]:
# Create a @secured_function decorator that looks for a global password before
# running the wrapped function and will raise an exception instead of running
# the wrapped function if the wrong password is provided. Use *args and **kwargs
# capture all function arguments.

def timed_function(func):
    global PASSWORD
    # Your implementation here
    pass

and then...

Avoid exceptions when running the three cells below. If you run into problems, try adjusting your decorators above.


In [ ]:
# Execute this cell without modifying it.
picky_eater_food = "You can now write your own decorators!".split(' ')

@secured_function
@timed_function
@case_mod
def picky_eater(food):
    if food.islower():
        time.sleep(0.1 * len(food))
        return food
    else:
        raise Exception("I don't wanna eat this!")

In [ ]:
# Change ONLY the assigned value of PASSWORD in this cell, then
# execute it.
global PASSWORD
PASSWORD = ''

In [ ]:
# Run this cell as-is without any exceptions cropping up and with
# an execution time printed out for each morsel in picky_eater_food.
for morsel in picky_eater_food:
    print picky_eater(morsel)

Optional stuff

Under the hood of @property

  • descriptor protocol

    • Extremely general purpose
    • Any one of the following:
      • __get__() method
      • __set__() method
      • __del__() method
  • object attribute access

  • property() returns a descriptor

Next steps

  • Using classes as decorators
    • Give this a try! It can be a little more straightforward.
  • Decorators with arguments
    • Example: Django's @require_http_methods(["GET", "POST"])