Skip to content
Snippets Groups Projects
06_decorators.ipynb 44.7 KiB
Newer Older
{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Decorators\n",
    "\n",
    "\n",
    "Remember that in Python, everything is an object, including functions. This\n",
    "means that we can do things like:\n",
    "\n",
    "\n",
    "- Pass a function as an argument to another function.\n",
    "- Create/define a function inside another function.\n",
    "- Write a function which returns another function.\n",
    "\n",
    "\n",
    "These abilities mean that we can do some neat things with functions in Python.\n",
    "\n",
    "\n",
    "* [Overview](#overview)\n",
    "* [Decorators on methods](#decorators-on-methods)\n",
    "* [Example - memoization](#example-memoization)\n",
    "* [Decorators with arguments](#decorators-with-arguments)\n",
    "* [Chaining decorators](#chaining-decorators)\n",
    "* [Decorator classes](#decorator-classes)\n",
    "* [Appendix: Functions are not special](#appendix-functions-are-not-special)\n",
    "* [Appendix: Closures](#appendix-closures)\n",
    "* [Appendix: Decorators without arguments versus decorators with arguments](#appendix-decorators-without-arguments-versus-decorators-with-arguments)\n",
    "* [Appendix: Per-instance decorators](#appendix-per-instance-decorators)\n",
    "* [Appendix: Preserving function metadata](#appendix-preserving-function-metadata)\n",
    "* [Appendix: Class decorators](#appendix-class-decorators)\n",
    "* [Useful references](#useful-references)\n",
    "\n",
    "\n",
    "<a class=\"anchor\" id=\"overview\"></a>\n",
    "## Overview\n",
    "\n",
    "\n",
    "Let's say that we want a way to calculate the execution time of any function\n",
    "(this example might feel familiar to you if you have gone through the\n",
    "practical on operator overloading).\n",
    "\n",
    "\n",
    "Our first attempt at writing such a function might look like this:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import time\n",
    "def timeFunc(func, *args, **kwargs):\n",
    "\n",
    "    start  = time.time()\n",
    "    retval = func(*args, **kwargs)\n",
    "    end    = time.time()\n",
    "\n",
    "    print('Ran {} in {:0.2f} seconds'.format(func.__name__, end - start))\n",
    "\n",
    "    return retval"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The `timeFunc` function accepts another function, `func`, as its first\n",
    "argument. It calls `func`, passing it all of the other arguments, and then\n",
    "prints the time taken for `func` to complete:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy        as np\n",
    "import numpy.linalg as npla\n",
    "\n",
    "def inverse(a):\n",
    "    return npla.inv(a)\n",
    "\n",
    "data    = np.random.random((2000, 2000))\n",
    "invdata = timeFunc(inverse, data)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "But this means that whenever we want to time something, we have to call the\n",
    "`timeFunc` function directly. Let's take advantage of the fact that we can\n",
    "define a function inside another funciton. Look at the next block of code\n",
    "carefully, and make sure you understand what our new `timeFunc` implementation\n",
    "is doing."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import time\n",
    "def timeFunc(func):\n",
    "\n",
    "    def wrapperFunc(*args, **kwargs):\n",
    "\n",
    "        start  = time.time()\n",
    "        retval = func(*args, **kwargs)\n",
    "        end    = time.time()\n",
    "\n",
    "        print('Ran {} in {:0.2f} seconds'.format(func.__name__, end - start))\n",
    "\n",
    "        return retval\n",
    "\n",
    "    return wrapperFunc"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This new `timeFunc` function is again passed a function `func`, but this time\n",
    "as its sole argument. It then creates and returns a new function,\n",
    "`wrapperFunc`. This `wrapperFunc` function calls and times the function that\n",
    "was passed to `timeFunc`.  But note that when `timeFunc` is called,\n",
    "`wrapperFunc` is _not_ called - it is only created and returned.\n",
    "\n",
    "\n",
    "Let's use our new `timeFunc` implementation:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy        as np\n",
    "import numpy.linalg as npla\n",
    "\n",
    "def inverse(a):\n",
    "    return npla.inv(a)\n",
    "\n",
    "data    = np.random.random((2000, 2000))\n",
    "inverse = timeFunc(inverse)\n",
    "invdata = inverse(data)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Here, we did the following:\n",
    "\n",
    "\n",
    "1. We defined a function called `inverse`:\n",
    "\n",
    "  > ```\n",
    "  > def inverse(a):\n",
    "  >     return npla.inv(a)\n",
    "  > ```\n",
    "\n",
    "2. We passed the `inverse` function to the `timeFunc` function, and\n",
    "   re-assigned the return value of `timeFunc` back to `inverse`:\n",
    "\n",
    "  > ```\n",
    "  > inverse = timeFunc(inverse)\n",
    "  > ```\n",
    "\n",
    "3. We called the new `inverse` function:\n",
    "\n",
    "  > ```\n",
    "  > invdata = inverse(data)\n",
    "  > ```\n",
    "\n",
    "\n",
    "So now the `inverse` variable refers to an instantiation of `wrapperFunc`,\n",
    "which holds a reference to the original definition of `inverse`.\n",
    "\n",
    "\n",
    "> If this is not clear, take a break now and read through the appendix on how\n",
    "> [functions are not special](#appendix-functions-are-not-special).\n",
    "\n",
    "\n",
    "Guess what? We have just created a __decorator__. A decorator is simply a\n",
    "function which accepts a function as its input, and returns another function\n",
    "as its output. In the example above, we have _decorated_ the `inverse`\n",
    "function with the `timeFunc` decorator.\n",
    "\n",
    "\n",
    "Python provides an alternative syntax for decorating one function with\n",
    "another, using the `@` character. The approach that we used to decorate\n",
    "`inverse` above:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def inverse(a):\n",
    "    return npla.inv(a)\n",
    "\n",
    "inverse = timeFunc(inverse)\n",
    "invdata = inverse(data)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "is semantically equivalent to this:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "@timeFunc\n",
    "def inverse(a):\n",
    "    return npla.inv(a)\n",
    "\n",
    "invdata = inverse(data)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<a class=\"anchor\" id=\"decorators-on-methods\"></a>\n",
    "## Decorators on methods\n",
    "\n",
    "\n",
    "Applying a decorator to the methods of a class works in the same way:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy.linalg as npla\n",
    "\n",
    "class MiscMaths(object):\n",
    "\n",
    "    @timeFunc\n",
    "    def inverse(self, a):\n",
    "        return npla.inv(a)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now, the `inverse` method of all `MiscMaths` instances will be timed:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "mm1 = MiscMaths()\n",
    "mm2 = MiscMaths()\n",
    "\n",
    "i1 = mm1.inverse(np.random.random((1000, 1000)))\n",
    "i2 = mm2.inverse(np.random.random((1500, 1500)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Note that only one `timeFunc` decorator was created here - the `timeFunc`\n",
    "function was only called once - when the `MiscMaths` class was defined.  This\n",
    "might be clearer if we re-write the above code in the following (equivalent)\n",
    "manner:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class MiscMaths(object):\n",
    "    def inverse(self, a):\n",
    "        return npla.inv(a)\n",
    "\n",
    "MiscMaths.inverse = timeFunc(MiscMaths.inverse)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So only one `wrapperFunc` function exists, and this function is _shared_ by\n",
    "all instances of the `MiscMaths` class - (such as the `mm1` and `mm2`\n",
    "instances in the example above). In many cases this is not a problem, but\n",
    "there can be situations where you need each instance of your class to have its\n",
    "own unique decorator.\n",
    "\n",
    "\n",
    "> If you are interested in solutions to this problem, take a look at the\n",
    "> appendix on [per-instance decorators](#appendix-per-instance-decorators).\n",
    "<a class=\"anchor\" id=\"example-memoization\"></a>\n",
    "## Example - memoization\n",
    "\n",
    "\n",
    "Let's move onto another example.\n",
    "[Meowmoization](https://en.wikipedia.org/wiki/Memoization) is a common\n",
    "performance optimisation technique used in cats. I mean software. Essentially,\n",
    "memoization refers to the process of maintaining a cache for a function which\n",
    "performs some expensive calculation. When the function is executed with a set\n",
    "of inputs, the calculation is performed, and then a copy of the inputs and the\n",
    "result are cached. If the function is called again with the same inputs, the\n",
    "cached result can be returned.\n",
    "\n",
    "\n",
    "This is a perfect problem to tackle with decorators:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def memoize(func):\n",
    "\n",
    "    cache = {}\n",
    "\n",
    "    def wrapper(*args):\n",
    "\n",
    "        # is there a value in the cache\n",
    "        # for this set of inputs?\n",
    "        cached = cache.get(args, None)\n",
    "\n",
    "        # If not, call the function,\n",
    "        # and cache the result.\n",
    "        if cached is None:\n",
    "            cached      = func(*args)\n",
    "            cache[args] = cached\n",
    "        else:\n",
    "            print('Cached {}({}): {}'.format(func.__name__, args, cached))\n",
    "\n",
    "        return cached\n",
    "\n",
    "    return wrapper"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can now use our `memoize` decorator to add a memoization cache to any\n",
    "function.  Let's memoize a function which generates the $n^{th}$ number in the\n",
    "[Fibonacci series](https://en.wikipedia.org/wiki/Fibonacci_number):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "@memoize\n",
    "def fib(n):\n",
    "\n",
    "    if n in (0, 1):\n",
    "        print('fib({}) = {}'.format(n, n))\n",
    "\n",
    "    twoback = 1\n",
    "    oneback = 1\n",
    "\n",
    "    for _ in range(2, n):\n",
    "\n",
    "        val     = oneback + twoback\n",
    "        twoback = oneback\n",
    "        oneback = val\n",
    "\n",
    "    print('fib({}) = {}'.format(n, val))\n",
    "\n",
    "    return val"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "For a given input, when `fib` is called the first time, it will calculate the\n",
    "$n^{th}$ Fibonacci number:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for i in range(10):\n",
    "    fib(i)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "However, on repeated calls with the same input, the calculation is skipped,\n",
    "and instead the result is retrieved from the memoization cache:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for i in range(10):\n",
    "    fib(i)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> If you are wondering how the `wrapper` function is able to access the\n",
    "> `cache` variable, refer to the [appendix on closures](#appendix-closures).\n",
    "\n",
    "\n",
    "<a class=\"anchor\" id=\"decorators-with-arguments\"></a>\n",
    "## Decorators with arguments\n",
    "\n",
    "\n",
    "Continuing with our memoization example, let's say that we want to place a\n",
    "limit on the maximum size that our cache can grow to. For example, the output\n",
    "of our function might have large memory requirements, so we can only afford to\n",
    "store a handful of pre-calculated results. It would be nice to be able to\n",
    "specify the maximum cache size when we define our function to be memoized,\n",
    "like so:\n",
    "\n",
    "\n",
    "> ```\n",
    "> # cache at most 10 results\n",
    "> @limitedMemoize(10):\n",
    "> def fib(n):\n",
    ">     ...\n",
    "> ```\n",
    "\n",
    "\n",
    "In order to support this, our `memoize` decorator function needs to be\n",
    "modified - it is currently written to accept a function as its sole argument,\n",
    "but we need it to accept a cache size limit."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from collections import OrderedDict\n",
    "\n",
    "def limitedMemoize(maxSize):\n",
    "\n",
    "    cache = OrderedDict()\n",
    "\n",
    "    def decorator(func):\n",
    "        def wrapper(*args):\n",
    "\n",
    "            # is there a value in the cache\n",
    "            # for this set of inputs?\n",
    "            cached = cache.get(args, None)\n",
    "\n",
    "            # If not, call the function,\n",
    "            # and cache the result.\n",
    "            if cached is None:\n",
    "\n",
    "                cached = func(*args)\n",
    "\n",
    "                # If the cache has grown too big,\n",
    "                # remove the oldest item. In practice\n",
    "                # it would make more sense to remove\n",
    "                # the item with the oldest access\n",
Paul McCarthy's avatar
Paul McCarthy committed
    "                # time (or remove the least recently\n",
    "                # used item, as the built-in\n",
    "                # @functools.lru_cache does), but this\n",
    "                # is good enough for now!\n",
    "                if len(cache) >= maxSize:\n",
    "                    cache.popitem(last=False)\n",
    "\n",
    "                cache[args] = cached\n",
    "            else:\n",
    "                print('Cached {}({}): {}'.format(func.__name__, args, cached))\n",
    "\n",
    "            return cached\n",
    "        return wrapper\n",
    "    return decorator"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> We used the handy\n",
    "> [`collections.OrderedDict`](https://docs.python.org/3.5/library/collections.html#collections.OrderedDict)\n",
    "> class here which preserves the insertion order of key-value pairs.\n",
    "\n",
    "\n",
    "This is starting to look a little complicated - we now have _three_ layers of\n",
    "functions. This is necessary when you wish to write a decorator which accepts\n",
    "arguments (refer to the\n",
    "[appendix](#appendix-decorators-without-arguments-versus-decorators-with-arguments)\n",
    "for more details).\n",
    "\n",
    "\n",
    "But this `limitedMemoize` decorator is used in essentially the same way as our\n",
    "earlier `memoize` decorator:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "@limitedMemoize(5)\n",
    "def fib(n):\n",
    "\n",
    "    if n in (0, 1):\n",
    "        print('fib({}) = 1'.format(n))\n",
    "        return n\n",
    "\n",
    "    twoback = 1\n",
    "    oneback = 1\n",
    "    val     = 1\n",
    "\n",
    "    for _ in range(2, n):\n",
    "\n",
    "        val     = oneback + twoback\n",
    "        twoback = oneback\n",
    "        oneback = val\n",
    "\n",
    "    print('fib({}) = {}'.format(n, val))\n",
    "\n",
    "    return val"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Except that now, the `fib` function will only cache up to 5 values."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "fib(10)\n",
    "fib(11)\n",
    "fib(12)\n",
    "fib(13)\n",
    "fib(14)\n",
    "print('The result for 10 should come from the cache')\n",
    "fib(10)\n",
    "fib(15)\n",
    "print('The result for 10 should no longer be cached')\n",
    "fib(10)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<a class=\"anchor\" id=\"chaining-decorators\"></a>\n",
    "## Chaining decorators\n",
    "\n",
    "\n",
    "Decorators can easily be chained, or nested:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import time\n",
    "\n",
    "@timeFunc\n",
    "@memoize\n",
    "def expensiveFunc(n):\n",
    "    time.sleep(n)\n",
    "    return n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> Remember that this is semantically equivalent to the following:\n",
    ">\n",
    "> ```\n",
    "> def expensiveFunc(n):\n",
    ">     time.sleep(n)\n",
    ">     return n\n",
    ">\n",
    "> expensiveFunc = timeFunc(memoize(expensiveFunc))\n",
    "> ```\n",
    "\n",
    "\n",
    "Now we can see the effect of our memoization layer on performance:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "expensiveFunc(0.5)\n",
    "expensiveFunc(1)\n",
    "expensiveFunc(1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> Note that in Python 3.2 and newer you can use the\n",
    "> [`functools.lru_cache`](https://docs.python.org/3/library/functools.html#functools.lru_cache)\n",
    "> to memoize your functions.\n",
    "\n",
    "\n",
    "<a class=\"anchor\" id=\"decorator-classes\"></a>\n",
    "## Decorator classes\n",
    "\n",
    "\n",
    "By now, you will have gained the impression that a decorator is a function\n",
    "which _decorates_ another function. But if you went through the practical on\n",
    "operator overloading, you might remember the special `__call__` method, that\n",
    "allows an object to be called as if it were a function.\n",
    "This feature allows us to write our decorators as classes, instead of\n",
    "functions. This can be handy if you are writing a decorator that has\n",
    "complicated behaviour, and/or needs to maintain some sort of state which\n",
    "cannot be easily or elegantly written using nested functions.\n",
    "As an example, let's say we are writing a framework for unit testing. We want\n",
    "to be able to \"mark\" our test functions like so, so they can be easily\n",
    "identified and executed:\n",
    "\n",
    "\n",
    "> ```\n",
    "> @unitTest\n",
    "> def testblerk():\n",
    ">     \"\"\"tests the blerk algorithm.\"\"\"\n",
    ">     ...\n",
    "> ```\n",
    "\n",
    "\n",
    "With a decorator like this, we wouldn't need to worry about where our tests\n",
    "are located - they will all be detected because we have marked them as test\n",
    "functions. What does this `unitTest` decorator look like?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class TestRegistry(object):\n",
    "\n",
    "    def __init__(self):\n",
    "        self.testFuncs = []\n",
    "\n",
    "    def __call__(self, func):\n",
    "        self.testFuncs.append(func)\n",
    "\n",
    "    def listTests(self):\n",
    "        print('All registered tests:')\n",
    "        for test in self.testFuncs:\n",
    "            print(' ', test.__name__)\n",
    "\n",
    "    def runTests(self):\n",
    "        for test in self.testFuncs:\n",
    "            print('Running test {:10s} ... '.format(test.__name__), end='')\n",
    "            try:\n",
    "                test()\n",
    "                print('passed!')\n",
    "            except Exception as e:\n",
    "                print('failed!')\n",
    "\n",
    "# Create our test registry\n",
    "\n",
    "# Alias our registry to \"unitTest\"\n",
    "# so that we can register tests\n",
    "# with a \"@unitTest\" decorator.\n",
    "unitTest = registry"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So we've defined a class, `TestRegistry`, and created an instance of it,\n",
    "`registry`, which will manage all of our unit tests. Now, in order to \"mark\"\n",
    "any function as being a unit test, we just need to use the `unitTest`\n",
    "decorator (which is simply a reference to our `TestRegistry` instance):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "@unitTest\n",
    "def testFoo():\n",
    "    assert 'a' in 'bcde'\n",
    "\n",
    "@unitTest\n",
    "def testBar():\n",
    "    assert 1 > 0\n",
    "\n",
    "@unitTest\n",
    "def testBlerk():\n",
    "    assert 9 % 2 == 0"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now that these functions have been registered with our `TestRegistry`\n",
    "instance, we can run them all:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "registry.listTests()\n",
    "registry.runTests()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> Unit testing is something which you must do! This is __especially__\n",
    "> important in an interpreted language such as Python, where there is no\n",
    "> compiler to catch all of your mistakes.\n",
    ">\n",
    "> Python has a built-in\n",
    "> [`unittest`](https://docs.python.org/3.5/library/unittest.html) module,\n",
    "> however the third-party [`pytest`](https://docs.pytest.org/en/latest/) and\n",
    "> [`nose`](http://nose2.readthedocs.io/en/latest/) are popular.  It is also\n",
    "> wise to combine your unit tests with\n",
    "> [`coverage`](https://coverage.readthedocs.io/en/coverage-4.5.1/), which\n",
    "> tells you how much of your code was executed, or _covered_ when your\n",
    "> tests were run.\n",
    "\n",
    "<a class=\"anchor\" id=\"appendix-functions-are-not-special\"></a>\n",
    "## Appendix: Functions are not special\n",
    "\n",
    "\n",
    "When we write a statement like this:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "a = [1, 2, 3]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "the variable `a` is a reference to a `list`. We can create a new reference to\n",
    "the same list, and delete `a`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "b = a\n",
    "del a"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Deleting `a` doesn't affect the list at all - the list still exists, and is\n",
    "now referred to by a variable called `b`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('b: ', b)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`a` has, however, been deleted:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('a: ', a)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The variables `a` and `b` are just references to a list that is sitting in\n",
    "memory somewhere - renaming or removing a reference does not have any effect\n",
    "upon the list<sup>2</sup>.\n",
    "\n",
    "\n",
    "If you are familiar with C or C++, you can think of a variable in Python as\n",
    "like a `void *` pointer - it is just a pointer of an unspecified type, which\n",
    "is pointing to some item in memory (which does have a specific type). Deleting\n",
    "the pointer does not have any effect upon the item to which it was pointing.\n",
    "\n",
    "\n",
    "> <sup>2</sup> Until no more references to the list exist, at which point it\n",
    "> will be\n",
    "> [garbage-collected](https://www.quora.com/How-does-garbage-collection-in-Python-work-What-are-the-pros-and-cons).\n",
    "\n",
    "\n",
    "Now, functions in Python work in _exactly_ the same way as variables.  When we\n",
    "define a function like this:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def inverse(a):\n",
    "    return npla.inv(a)\n",
    "\n",
    "print(inverse)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "there is nothing special about the name `inverse` - `inverse` is just a\n",
    "reference to a function that resides somewhere in memory. We can create a new\n",
    "reference to this function:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "inv2 = inverse"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And delete the old reference:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "del inverse"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "But the function still exists, and is still callable, via our second\n",
    "reference:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(inv2)\n",
    "data    = np.random.random((10, 10))\n",
    "invdata = inv2(data)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So there is nothing special about functions in Python - they are just items\n",
    "that reside somewhere in memory, and to which we can create as many references\n",
    "as we like.\n",
    "\n",
    "\n",
    "> If it bothers you that `print(inv2)` resulted in\n",
    "> `<function inverse at ...>`, and not `<function inv2 at ...>`, then refer to\n",
    "> the appendix on\n",
    "> [preserving function metdata](#appendix-preserving-function-metadata).\n",
    "<a class=\"anchor\" id=\"appendix-closures\"></a>\n",
    "## Appendix: Closures\n",
    "\n",
    "\n",
    "Whenever we define or use a decorator, we are taking advantage of a concept\n",
    "called a [_closure_][wiki-closure]. Take a second to re-familiarise yourself\n",
    "with our `memoize` decorator function from earlier - when `memoize` is called,\n",
    "it creates and returns a function called `wrapper`:\n",
    "\n",
    "\n",
    "[wiki-closure]: https://en.wikipedia.org/wiki/Closure_(computer_programming)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "        # is there a value in the cache\n",
    "        # for this set of inputs?\n",
    "        cached = cache.get(args, None)\n",
    "        # If not, call the function,\n",
    "        # and cache the result.\n",
    "        if cached is None:\n",
    "            cached      = func(*args)\n",
    "            cache[args] = cached\n",
    "        else:\n",
    "            print('Cached {}({}): {}'.format(func.__name__, args, cached))\n",
    "        return cached\n",
    "\n",
    "    return wrapper"