Skip to content

Instantly share code, notes, and snippets.

@chalmerj
Created June 25, 2013 19:26
Show Gist options
  • Save chalmerj/5861545 to your computer and use it in GitHub Desktop.
Save chalmerj/5861545 to your computer and use it in GitHub Desktop.
IPython Notbook about Python Decorators!
Display the source blob
Display the rendered blob
Raw
{
"metadata": {
"name": "FT-Python Topics - @Decorators - Public"
},
"nbformat": 3,
"nbformat_minor": 0,
"worksheets": [
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## FT-Python Topics: @Decorators.\n",
"### Press shift-enter to 'run' a cell. \n",
"<br />\n",
"With deep thanks and appreciation to: \n",
"\n",
"* [http://simeonfranklin.com/blog/2012/jul/1/python-decorators-in-12-steps/]()\n",
"* [http://stackoverflow.com/questions/739654/understanding-python-decorators]()\n",
"* [http://wiki.python.org/moin/PythonDecoratorLibrary]()\n",
"* [http://blogs.oucs.ox.ac.uk/inapickle/2012/01/05/python-decorators-with-optional-arguments/]()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"To understand `@decorators`, we can break down the concept into easy to understand chunks."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Variables, Scope and Lifetime\n",
"\n",
"Variables store data. \n",
"__Scope__ is used to describe from *where* in your program that data is accessible. \n",
"\n",
"The *scope* (also called 'namespace') can either be __local__ or __global__.\n",
"\n",
"* __Global:__ Global scope can be accessed from anywhere in the current program.\n",
"* __Local:__ Local scope means the variable (object, really) can be accessed *only* from within the current context, be that class, object or function.\n",
"\n",
"The __lifetime__ of a variable is dependant on its scope: the scope is actually created / destroyed each time a function is called.\n",
"\n",
"Python will **resolve** a variable based on scope: it will look for a variable in the local scope first, then the global scope."
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"# Globally scoped Variable: \n",
"global_string = \"This is a global string\"\n",
"\n",
"def global_printFoo(): # The globally scoped variable 'my_string' is\n",
" print global_string # accessible from inside this function.\n",
"\n",
"global_printFoo()\n"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"This is a global string\n"
]
}
],
"prompt_number": 79
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"# Locally scoped Variable\n",
"\n",
"def local_printFoo():\n",
" local_string = \"This is a local string\"\n",
" print local_string\n",
"\n",
"local_printFoo()\n",
"print local_string # This will cause an error!"
],
"language": "python",
"metadata": {},
"outputs": [
{
"ename": "NameError",
"evalue": "name 'local_string' is not defined",
"output_type": "pyerr",
"traceback": [
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m\n\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)",
"\u001b[0;32m<ipython-input-80-7ad57ba91456>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m()\u001b[0m\n\u001b[1;32m 6\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 7\u001b[0m \u001b[0mlocal_printFoo\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 8\u001b[0;31m \u001b[0;32mprint\u001b[0m \u001b[0mlocal_string\u001b[0m \u001b[0;31m# This will cause an error!\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
"\u001b[0;31mNameError\u001b[0m: name 'local_string' is not defined"
]
},
{
"output_type": "stream",
"stream": "stdout",
"text": [
"This is a local string\n"
]
}
],
"prompt_number": 80
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Arguments and Parameters\n",
"\n",
"Parameters that are passed as arguments to functions become part of the **local** scope (namespace)."
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"# Parameters are part of the local scope for that function\n",
"# The `locals()` built-in returns the current local scope as a dictionary.\n",
"def local_parameters(number):\n",
" print locals() \n",
"\n",
"local_parameters(10)"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"{'number': 10}\n"
]
}
],
"prompt_number": 87
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Nesting Functions\n",
"Functions can be nested. Just like variables, nested funtions have a scope and lifetime.\n",
"\n",
"Nested functions will use the scope resolution rules to look for variables."
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"# Declaring a nested function\n",
"def outer_function():\n",
" x = 1 # Define a local variable\n",
" \n",
" def inner_function(): # Define a nested function\n",
" print x # Call a variable from the local parent scope\n",
" \n",
" inner_function() # Actually call the inner function.\n",
"\n",
"outer_function() # Calling the outer function will result in the inner_function being called."
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"1\n"
]
}
],
"prompt_number": 88
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Functions are Objects too!\n",
"\n",
"Functions, (just like everything else) are also objects. That means functions can have attributes associated with them, and functions can be used as arguments for other functions!\n",
"\n",
"In the example below, we have two functions: `space` and `underscore`. They both return *data*. \n",
"\n",
"The third function (`apply_combine`) returns a *function*. Passing that to the `print` command returns the result of *running* that function."
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"# Functions can be passed as arguments to other functions\n",
"def space(string1, string2):\n",
" ''' Combines two strings into one string with a space between them'''\n",
" return string1+\" \"+string2\n",
"\n",
"def underscore(string1, string2):\n",
" ''' Combines two strings into one string with an underscore between them'''\n",
" return string1+\"_\"+string2\n",
"\n",
"def apply_combine(func, string1, string2):\n",
" return func(string1, string2)\n",
"\n",
"print apply_combine(space, \"Hello\", \"World!\")\n",
"\n",
"print apply_combine(underscore, \"What's\", \"up!\")"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"Hello World!\n",
"What's_up!\n"
]
}
],
"prompt_number": 89
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Closures\n",
"\n",
"So we've seen how scoping works, and we know we can nest functions and pass as arguments them like any other kind of data. \n",
"\n",
"This means we can create a _function_ that takes a _function_ as input and returns a _function_ as output! (Whew.)\n",
"\n",
"However: a local scope only exists *when a function is called*. This might seem like a problem - what if your inner function needs data from the parent function's scope?\n",
"\n",
"Observe the example below:"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"def outer_function():\n",
" x = 1\n",
" \n",
" def inner_function():\n",
" print x\n",
" \n",
" return inner_function"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 92
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We have an **`outer_function`** that contains a single variable (`x`), and defines a nested **`inner_function`**.\n",
"\n",
"When run, **`outer_function`** returns **`inner_function`**. \n",
"\n",
"This means if the result **`outer_function`** is assigned to a variable, it will be a pointer to **`inner_function`**!\n"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"foo = outer_function() # Assign the result of `outer_function` to variable `foo`.\n",
"\n",
"print foo.__name__"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"inner_function\n"
]
}
],
"prompt_number": 93
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"So what happens if we run **`foo()`**? \n",
"\n",
"Only **`inner_function`** is being run, and yet it depends on variable **`x`** from it's parent scope!"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"foo()"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"1\n"
]
}
],
"prompt_number": 94
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"How does this work!? The answer: **Closures**.\n",
"\n",
"In short: A **closure** is a way to bind data to a function without passing it as a parameter. Or to put it another way, it's a way to access data from an environment (*scope*) that's no longer active. [More on Closures][1]\n",
"\n",
"In the case of Python, it's a way to access data that was present in the scope *when the function was defined*. In this case, even though we're only refering to **`inner_function`**, we can still access the value of **`x`**.\n",
"\n",
"[1]:http://stackoverflow.com/questions/13857/can-you-explain-closures-as-they-relate-to-python"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## And now: Decorators!\n",
"\n",
"\n",
"Simply defined: A **Decorator** is a callable object (like a function) that takes a function as an input and returns a replacement function as a result.\n",
"\n",
"It allows us to replace a callable object with another callable that has different behavior."
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"# An example Decorator object\n",
"def myDecorator(input_function): # Takes a function as an argument\n",
" def wrapper(): # Defines an inner 'wrapper' function with specific behavior.\n",
" print \"Decorator called!\"\n",
" input_function() # Calls the input_function.\n",
" return wrapper # Returns the 'wrapper' as the replacement function.\n",
"\n",
"\n",
"# We 'apply' a decorator to a function definition with the '@' symbol\n",
"@myDecorator\n",
"def printHello():\n",
" print \"Hello!\"\n",
" \n",
"# What happens when we run our decorated function?\n",
"printHello()"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"Decorator called!\n",
"Hello!\n"
]
}
],
"prompt_number": 95
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"So our very simple decorator behaves as a 'Wrapper' function. This means we can use it to do fun things, like logging, debugging and profiling! [See here][examples] for some excellent examples!\n",
"\n",
"----\n",
"\n",
"## Exercise 1\n",
"\n",
"Using a sandwich function:\n",
"\n",
"[examples]:http://stackoverflow.com/a/1594484"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"def sandwich():\n",
"\tprint \"--ham--\""
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 41
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Create a `bread` decorator that prints bread around the sandwich when run:"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"@bread\n",
"@fixins\n",
"def sandwich():\n",
"\tprint \"--ham--\""
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 44
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"sandwich()"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"</''''''''\\>\n",
"##tomato##\n",
"--ham--\n",
"~~lettuce~~\n",
"<\\________/>\n"
]
}
],
"prompt_number": 96
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"-----\n",
"\n",
"## Context Loss\n",
"\n",
"We know that a decorator 'wraps' a function by replacing it. What happens to the original function's information (e.g. name, module, docstring)? It gets replaced!"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"# A function's context specific information\n",
"def myFunction():\n",
" '''My Function's Docstring'''\n",
" return 0\n",
"\n",
"print myFunction.__name__\n",
"print myFunction.__doc__"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"myFunction\n",
"My Function's Docstring\n"
]
}
],
"prompt_number": 97
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"# A decorated function's context gets replaced!\n",
"\n",
"@myDecorator\n",
"def myNewFunction():\n",
" '''New Function Docstring'''\n",
" return 0\n",
"\n",
"print myNewFunction.__name__\n",
"print myNewFunction.__doc__"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"wrapper\n",
"None\n"
]
}
],
"prompt_number": 98
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can avoid this unfortunate loss of context using a bit of Python Magic: **Functools**.\n",
"\n",
"Using the `functools.wraps` decorator (yo, dawg) we can preserve the context of our decorated function."
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"# An example Decorator object using functools.wraps\n",
"import functools\n",
"\n",
"def mySafeDecorator(input_function): # Takes a function as an argument\n",
" @functools.wraps(input_function) # We use a 'wraps()' decorator to indicate the function we're wrapping.\n",
" def wrapper(): # Defines an inner 'wrapper' function with specific behavior.\n",
" print \"Decorator called!\"\n",
" input_function() # Calls the input_function.\n",
" return wrapper # Returns the 'wrapper' as the replacement function.\n",
"\n",
"\n",
"\n",
"@mySafeDecorator\n",
"def mySafeFunction():\n",
" '''Safe Docstring'''\n",
" return 0\n",
" \n",
"# What happens when we run our safely decorated function?\n",
"print mySafeFunction.__name__\n",
"print mySafeFunction.__doc__"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"mySafeFunction\n",
"Safe Docstring\n"
]
}
],
"prompt_number": 99
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"----\n",
"## Arguments!\n",
"\n",
"What happens when our decorated function takes arguments? \n",
"\n",
"The replacement function must **take the same arguments**!\n",
"\n",
"How is this accomplished? Argument Passing!\n",
"\n",
"As a fantastic side effect, we can *use* those passed arguments **in our replacement function**."
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"# An example decorator function that passes any and all arguments to the wrapper\n",
"def myArgumentDecorator(func):\n",
" @functools.wraps(func)\n",
" def wrapper(*args, **kwargs):\n",
" print \"Decorated Function!\"\n",
" print \"Passed Arguments: \",args, kwargs\n",
" func(*args, **kwargs)\n",
" return wrapper\n",
"\n",
"@myArgumentDecorator\n",
"def myArgumentFunction(number):\n",
" print number\n",
" \n",
"myArgumentFunction(number=10)"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"Decorated Function!\n",
"Passed Arguments: () {'number': 10}\n",
"10\n"
]
}
],
"prompt_number": 100
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"----\n",
"## Exercise 2\n",
"\n",
"Using the following username function:"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"authorized_users = ['alice', 'bob', 'charles']\n",
"\n",
"def getUsername(username):\n",
"\tprint username"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 77
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Create a decorator function that only prints the username if it matches the username from a list of authorized users."
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"@authRequired\n",
"def getUsername(username):\n",
"\tprint username\n",
" \n",
"getUsername(\"jeff\")\n",
"print \"---\"\n",
"getUsername(\"bob\")"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"Not Authorized!\n",
"---\n",
"Authorized!\n",
"bob\n"
]
}
],
"prompt_number": 76
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"----\n",
"## Answer to Exercise 1"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"def bread(func):\n",
"\tdef wrapper():\n",
"\t\tprint \"</''''''''\\>\"\n",
"\t\tfunc()\n",
"\t\tprint \"<\\________/>\"\n",
"\treturn wrapper\n",
"\n",
"def fixins(func):\n",
"\tdef wrapper():\n",
"\t\tprint \"##tomato##\"\n",
"\t\tfunc()\n",
"\t\tprint \"~~lettuce~~\"\n",
"\treturn wrapper\n",
"\n",
"@bread\n",
"@fixins\n",
"def sandwich():\n",
" print \"--ham--\"\n",
"\n",
"sandwich()"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"</''''''''\\>\n",
"##tomato##\n",
"--ham--\n",
"~~lettuce~~\n",
"<\\________/>\n"
]
}
],
"prompt_number": 1
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"-----\n",
"## Answer to Exercise 2"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"from functools import wraps\n",
"\n",
"authorized_users = ['alice', 'bob', 'charles']\n",
"\n",
"def authRequired(func):\n",
" @wraps(func)\n",
" def authWrapper(username):\n",
" if username in authorized_users:\n",
" print \"Authorized!\"\n",
" func(username)\n",
" else:\n",
" print \"Not Authorized!\"\n",
" return authWrapper\n",
"\n",
"@authRequired\n",
"def getUsername(username):\n",
" print username\n",
"\n",
"getUsername(\"jeff\")\n",
"# >>> Not Authorized!\n",
"\n",
"getUsername(\"bob\")\n",
"# >>> Authorized!\n",
"# >>> bob"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"Not Authorized!\n",
"Authorized!\n",
"bob\n"
]
}
],
"prompt_number": 2
}
],
"metadata": {}
}
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment