CRÜCIAL PYTHON Week 7: Decorators

In [1]:
from IPython.core.display import Image
Image(url='', width=600)

This session of crucial python borrows heavily from which covers decorators from the ground up.

Background: Functions as first-class objects

In Python, functions are objects that you can pass around and manipulate just as you would a normal variable.

In [2]:
def say_hey():
    """ A function which always returns the string 'hey' """
    return 'hey'
def print_function_output(function):
    """ A function which calls an input function and prints its output """
    print function()

In [3]:
# Functions can also be returned just like normal variables 
def i_return_a_function():
    """ A function which builds a new function and returns it """
    def i_get_returned():
        """ This is a nested function, which gets built during i_return_a_function """
        print "I was built in i_return_a_function"
    return i_get_returned

result = i_return_a_function()
print result
<function i_get_returned at 0x10461cc08>
I was built in i_return_a_function

Background: Function closures

When you define a function within another function, it will remember what the local namespace looked like at definition time.

In [4]:
def make_printer(print_me):
    """ Construct a function which prints whatever was input to make_printer """
    def printer():
        """ Print the value of print_me passed to make_printer when printer was created """
        print print_me
    return printer

hey_printer = make_printer('hey')
you_printer = make_printer('you')

Aside: functools.partial is a more useful example. From the documentation: "The partial() is used for partial function application which “freezes” some portion of a function’s arguments and/or keywords resulting in a new object with a simplified signature."

In [5]:
print int.__doc__
int(x=0) -> int or long
int(x, base=10) -> int or long

Convert a number or string to an integer, or return 0 if no arguments
are given.  If x is floating point, the conversion truncates towards zero.
If x is outside the integer range, the function returns a long instead.

If x is not a number or if base is given, then x must be a string or
Unicode object representing an integer literal in the given base.  The
literal can be preceded by '+' or '-' and be surrounded by whitespace.
The base defaults to 10.  Valid bases are 0 and 2-36.  Base 0 means to
interpret the base from the string as an integer literal.
>>> int('0b100', base=0)
In [6]:
import functools
basetwo = functools.partial(int, base=2)


Decorators are simply functions which take a function as input and return a function.

In [7]:
def double_function(function):
    """ Return a version of function which doubles its output """
    def doubler():
        return function()*2
    return doubler

def return_10():
    return 10
return_double_10 = double_function(return_10)
print return_10()
print return_double_10()

The syntax function = decorator(function) ends up popping up a lot when you start using decorators. So, as a convenience, Python includes the @ syntax. Placing @decorator above a function definition replaces the function at definition time with its decorated version. You might remember seeing this syntax last week; it's used by flask extensively.

In [8]:
def return_20():
    return 10
print return_20()

Practical Example

Say we have a bunch of functions, all of which take at least one input: x, which should be a float which is greater than 0. We can use a decorator to check for appropriate values of x and apply it to each function

In [9]:
import warnings
def validate_x(x_function):
    """ Validates the input values of a function. """
    # The *args and **kwargs variables are any function arguments beyond the first
    def x_function_validated(x, *args, **kwargs):
        # Check that x is a float, and try casting it
        if not type(x) is float:
            warnings.warn('x is not a float')
                x = float(x)
                raise TypeError('Could not cast x to a float')
        # Confirm that x is greater than 0
        if not x > 0:
            raise TypeError('x should be greater than 0')
        return x_function(x, *args, **kwargs)
    return x_function_validated

def root(x, n):
    """ Compute the n'th root of x """
    return x**(1/float(n))

def invert_multiply_add(x, multiply=1., add=0.):
    """ Computes multiply/x + add """
    return multiply/x + add
In [10]:
print root(16, 4)
-c:8: UserWarning: x is not a float
In [11]:
print invert_multiply_add(0, 1)
TypeError                                 Traceback (most recent call last)
<ipython-input-11-563bacac26d9> in <module>()
----> 1 print invert_multiply_add(0, 1)

<ipython-input-9-e090e6c8875d> in x_function_validated(x, *args, **kwargs)
     13         # Confirm that x is greater than 0
     14         if not x > 0:
---> 15             raise TypeError('x should be greater than 0')
     16         return x_function(x, *args, **kwargs)
     17     return x_function_validated

TypeError: x should be greater than 0

More substantial examples are available here: