From 6a23db4844bd40f49d23edfd5150c75fc4f73e5d Mon Sep 17 00:00:00 2001
From: Paul McCarthy <pauldmccarthy@gmail.com>
Date: Wed, 14 Feb 2018 10:52:57 +0000
Subject: [PATCH] Finished main part of decorator practical - some appendices
 to go

---
 advanced_topics/decorators.ipynb | 400 +++++++++++++++++++++++++------
 advanced_topics/decorators.md    | 337 ++++++++++++++++++++------
 2 files changed, 600 insertions(+), 137 deletions(-)

diff --git a/advanced_topics/decorators.ipynb b/advanced_topics/decorators.ipynb
index eb09e68..e4eed7c 100644
--- a/advanced_topics/decorators.ipynb
+++ b/advanced_topics/decorators.ipynb
@@ -52,7 +52,9 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "Ok, this looks like it might do the trick:"
+    "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:"
    ]
   },
   {
@@ -108,16 +110,11 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "The `timeFunc` function is given a function as its sole argument. It then\n",
-    "creates and returns a new function, `wrapperFunc`. This `wrapperFunc` function\n",
-    "calls and times the function that was passed to `timeFunc`<sup>1</sup>.  But\n",
-    "note that when `timeFunc` is called, `wrapperFunc` is _not_ called - it is\n",
-    "only created and returned.\n",
-    "\n",
-    "\n",
-    "> <sup>1</sup> If you are wondering how the `wrapperFunc` is able to access\n",
-    "> the `func` argument, refer to the [appendix on\n",
-    "> closures](#appendix-closures).\n",
+    "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:"
@@ -207,6 +204,86 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
+    "## 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, read the next section on\n",
+    "[memoization](#example-memoization), and then take a look at the appendix on\n",
+    "[per-instance decorators](#appendix-per-instance-decorators).\n",
+    "\n",
+    "\n",
     "## Example - memoization\n",
     "\n",
     "\n",
@@ -270,8 +347,8 @@
     "@memoize\n",
     "def fib(n):\n",
     "\n",
-    "    if n in (1, 2):\n",
-    "        print('fib({}) = 1'.format(n))\n",
+    "    if n in (0, 1):\n",
+    "        print('fib({}) = {}'.format(n, n))\n",
     "        return 1\n",
     "\n",
     "    twoback = 1\n",
@@ -329,6 +406,10 @@
    "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",
     "## Decorators with arguments\n",
     "\n",
     "\n",
@@ -529,13 +610,136 @@
    "source": [
     "## Decorator classes\n",
     "\n",
-    "> not to be confused with _class decorators_ - see the\n",
-    "> [appendix](#appendix-class-decorators)\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",
     "\n",
     "\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",
     "\n",
     "\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 a test registry, and\n",
+    "# alias it to \"unitTest\" so\n",
+    "# that we can register tests\n",
+    "# with a \"@unitTest\" decorator.\n",
+    "registry = TestRegistry()\n",
+    "unitTest = registry"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "So we've defined a class, `TestRegistry`, which will manage all of our unit\n",
+    "tests. Now, in order to \"mark\" any function as being a unit test, we just need\n",
+    "to use the `unitTest` decorator:"
+   ]
+  },
+  {
+   "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",
     "\n",
     "## Appendix: Functions are not special\n",
     "\n",
@@ -613,8 +817,8 @@
     "\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 undefined type, which is\n",
-    "pointing to some item in memory (which does have a specific type). Deleting\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",
@@ -702,25 +906,21 @@
     "\n",
     "\n",
     "> If it bothers you that `print(inv2)` resulted in\n",
-    "> `<function inverse at ...>`, and not `<function inv2 at ...>`, then keep\n",
-    "> going straight through to the\n",
-    "> [next appendix](#appendix-preserving-function-metadata).\n",
-    "\n",
-    "\n",
-    "## Appendix: Preserving function metadata\n",
-    "\n",
-    "\n",
-    "TODO `functools.wraps`\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",
     "\n",
     "\n",
     "## Appendix: Closures\n",
     "\n",
     "\n",
-    "\n",
     "Whenever we define or use a decorator, we are taking advantage of a concept\n",
-    "called a [_closure_](https://www.geeksforgeeks.org/python-closures/). Take a\n",
-    "second to re-familiarise yourself with our `timeFunc` decorator function from\n",
-    "earlier - when `timeFunc` is called, it creates and returns `wrapperFunc`:"
+    "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)"
    ]
   },
   {
@@ -729,29 +929,36 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "import time\n",
-    "def timeFunc(func):\n",
+    "def memoize(func):\n",
     "\n",
-    "    def wrapperFunc(*args, **kwargs):\n",
+    "    cache = {}\n",
     "\n",
-    "        start  = time.time()\n",
-    "        retval = func(*args, **kwargs)\n",
-    "        end    = time.time()\n",
+    "    def wrapper(*args):\n",
     "\n",
-    "        print('Ran {} in {:0.2f} seconds'.format(func.__name__, end - start))\n",
+    "        # is there a value in the cache\n",
+    "        # for this set of inputs?\n",
+    "        cached = cache.get(args, None)\n",
     "\n",
-    "        return retval\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 wrapperFunc"
+    "        return cached\n",
+    "\n",
+    "    return wrapper"
    ]
   },
   {
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "Then `wrapperFunc` is executed at some arbitrary point in the future. But how\n",
-    "does it have access to `func`, defined within the scope of the `timeFunc`\n",
-    "function, after the execution of `timeFunc` has ended?"
+    "Then `wrapper` is executed at some arbitrary point in the future. But how does\n",
+    "it have access to `cache`, defined within the scope of the `memoize` function,\n",
+    "after the execution of `memoize` has ended?"
    ]
   },
   {
@@ -760,16 +967,16 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "def pause(secs):\n",
-    "    print('Pausing for {} seconds ...'.format(secs))\n",
-    "    time.sleep(secs)\n",
-    "    print('Finished pausing')\n",
+    "def nby2(n):\n",
+    "    return n * 2\n",
     "\n",
-    "# wrapperFunc is created here\n",
-    "pause = timeFunc(pause)\n",
+    "# wrapper function is created here (and\n",
+    "# assigned back to the nby2 reference)\n",
+    "nby2 = memoize(nby2)\n",
     "\n",
-    "# wrapperFunc is executed here\n",
-    "pause(2)"
+    "# wrapper function is executed here\n",
+    "print('nby2(2): ', nby2(2))\n",
+    "print('nby2(2): ', nby2(2))"
    ]
   },
   {
@@ -777,18 +984,73 @@
    "metadata": {},
    "source": [
     "The trick is that whenever a nested function is defined in Python, the scope\n",
-    "in which it is defined is preserved for that function's lifetime. So\n",
-    "`wrapperFunc` has access to all of the variables within the `timeFunc`\n",
-    "function's scope, that were defined at the time that `wrapperFunc` was created\n",
-    "(which was when we called `timeFunc`).  This is why `wrapperFunc` is able to\n",
-    "access `func`, even though at the time that `wrapperFunc` is called, the\n",
-    "execution of `timeFunc` has long since finished.\n",
+    "in which it is defined is preserved for that function's lifetime. So `wrapper`\n",
+    "has access to all of the variables within the `memoize` function's scope, that\n",
+    "were defined at the time that `wrapper` was created (which was when we called\n",
+    "`memoize`).  This is why `wrapper` is able to access `cache`, even though at\n",
+    "the time that `wrapper` is called, the execution of `memoize` has long since\n",
+    "finished.\n",
+    "\n",
+    "\n",
+    "This is what is known as a\n",
+    "[_closure_](https://www.geeksforgeeks.org/python-closures/). Closures are a\n",
+    "fundamental, and extremely powerful, aspect of Python and other high level\n",
+    "languages. So there's your answer,\n",
+    "[fishbulb](https://www.youtube.com/watch?v=CiAaEPcnlOg).\n",
     "\n",
     "\n",
-    "This is what is known as a _closure_. Closures are a fundamental, and\n",
-    "extremely powerful, aspect of Python and other high level languages such as\n",
-    "Javascript. So there's your answer,\n",
-    "[fishbulb](https://www.youtube.com/watch?v=CiAaEPcnlOg).\n",
+    "## Appendix: Preserving function metadata\n",
+    "\n",
+    "\n",
+    "TODO `functools.wraps`\n",
+    "\n",
+    "\n",
+    "## Appendix: per-instance decorators\n",
+    "\n",
+    "\n",
+    "Below, we have defined a class called `Multiplier`, which has a method\n",
+    "`multiply` that multiplies some number by a fixed (but changeable) constant,\n",
+    "and is memoized via the `@memoize` decorator we [defined\n",
+    "earlier](#example-memoization).\n",
+    "\n",
+    "\n",
+    "Can you spot the problem with this implementation?"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class Multiplier(object):\n",
+    "    def __init__(self):\n",
+    "        self.__multiplier = 1\n",
+    "\n",
+    "    @property\n",
+    "    def multiplier(self):\n",
+    "        return self.multiplier\n",
+    "\n",
+    "    @multiplier.setter\n",
+    "    def multiplier(self, m):\n",
+    "        self.multiplier = m\n",
+    "\n",
+    "    @memoize\n",
+    "    def multiply(self, x):\n",
+    "        return x * self.multiplier"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "TODO Instanecify\n",
+    "\n",
+    "\n",
+    "## Appendix: class decorators\n",
+    "\n",
+    "\n",
+    "TODO\n",
     "\n",
     "\n",
     "## Appendix: Decorators without arguments versus decorators with arguments\n",
@@ -797,9 +1059,9 @@
     "There are three ways to invoke a decorator with the `@` notation:\n",
     "\n",
     "\n",
-    "1. Naming it (e.g. `@mydecorator`)\n",
-    "2. Calling it (e.g. `@mydecorator()`)\n",
-    "3. Calling it, and passing it arguments (e.g. `@mydecorator(1, 2, 3)`)\n",
+    "1. Naming it, e.g. `@mydecorator`\n",
+    "2. Calling it, e.g. `@mydecorator()`\n",
+    "3. Calling it, and passing it arguments, e.g. `@mydecorator(1, 2, 3)`\n",
     "\n",
     "\n",
     "Python expects a decorator function to behave differently in the second and\n",
@@ -890,19 +1152,17 @@
     "> ```\n",
     "\n",
     "\n",
-    "## Appendix: class decorators\n",
-    "\n",
-    "\n",
-    "TODO\n",
-    "\n",
-    "\n",
     "## Useful references\n",
     "\n",
     "\n",
     "* [Decorator tutorial](http://blog.thedigitalcatonline.com/blog/2015/04/23/python-decorators-metaprogramming-with-style/)\n",
     "* [Another decorator tutorial](https://realpython.com/blog/python/primer-on-python-decorators/)\n",
+    "* [The decorators they won't tell you about](https://github.com/hchasestevens/hchasestevens.github.io/blob/master/notebooks/the-decorators-they-wont-tell-you-about.ipynb)\n",
+    "* [Closures - Wikipedia][wiki-closure]\n",
     "* [Closures in Python](https://www.geeksforgeeks.org/python-closures/)\n",
-    "* [Garbage collection in Python](https://www.quora.com/How-does-garbage-collection-in-Python-work-What-are-the-pros-and-cons)"
+    "* [Garbage collection in Python](https://www.quora.com/How-does-garbage-collection-in-Python-work-What-are-the-pros-and-cons)\n",
+    "\n",
+    "[wiki-closure]: https://en.wikipedia.org/wiki/Closure_(computer_programming)"
    ]
   }
  ],
diff --git a/advanced_topics/decorators.md b/advanced_topics/decorators.md
index 9643787..34e1107 100644
--- a/advanced_topics/decorators.md
+++ b/advanced_topics/decorators.md
@@ -38,7 +38,9 @@ def timeFunc(func, *args, **kwargs):
 ```
 
 
-Ok, this looks like it might do the trick:
+The `timeFunc` function accepts another function, `func`, as its first
+argument. It calls `func`, passing it all of the other arguments, and then
+prints the time taken for `func` to complete:
 
 
 ```
@@ -78,16 +80,11 @@ def timeFunc(func):
 ```
 
 
-The `timeFunc` function is given a function as its sole argument. It then
-creates and returns a new function, `wrapperFunc`. This `wrapperFunc` function
-calls and times the function that was passed to `timeFunc`<sup>1</sup>.  But
-note that when `timeFunc` is called, `wrapperFunc` is _not_ called - it is
-only created and returned.
-
-
-> <sup>1</sup> If you are wondering how the `wrapperFunc` is able to access
-> the `func` argument, refer to the [appendix on
-> closures](#appendix-closures).
+This new `timeFunc` function is again passed a function `func`, but this time
+as its sole argument. It then creates and returns a new function,
+`wrapperFunc`. This `wrapperFunc` function calls and times the function that
+was passed to `timeFunc`.  But note that when `timeFunc` is called,
+`wrapperFunc` is _not_ called - it is only created and returned.
 
 
 Let's use our new `timeFunc` implementation:
@@ -153,6 +150,62 @@ invdata = inverse(data)
 ```
 
 
+## Decorators on methods
+
+
+Applying a decorator to the methods of a class works in the same way:
+
+
+```
+import numpy.linalg as npla
+
+class MiscMaths(object):
+
+    @timeFunc
+    def inverse(self, a):
+        return npla.inv(a)
+```
+
+
+Now, the `inverse` method of all `MiscMaths` instances will be timed:
+
+
+```
+mm1 = MiscMaths()
+mm2 = MiscMaths()
+
+i1 = mm1.inverse(np.random.random((1000, 1000)))
+i2 = mm2.inverse(np.random.random((1500, 1500)))
+```
+
+
+Note that only one `timeFunc` decorator was created here - the `timeFunc`
+function was only called once - when the `MiscMaths` class was defined.  This
+might be clearer if we re-write the above code in the following (equivalent)
+manner:
+
+
+```
+class MiscMaths(object):
+    def inverse(self, a):
+        return npla.inv(a)
+
+MiscMaths.inverse = timeFunc(MiscMaths.inverse)
+```
+
+
+So only one `wrapperFunc` function exists, and this function is _shared_ by
+all instances of the `MiscMaths` class - (such as the `mm1` and `mm2`
+instances in the example above). In many cases this is not a problem, but
+there can be situations where you need each instance of your class to have its
+own unique decorator.
+
+
+If you are interested in solutions to this problem, read the next section on
+[memoization](#example-memoization), and then take a look at the appendix on
+[per-instance decorators](#appendix-per-instance-decorators).
+
+
 ## Example - memoization
 
 
@@ -203,8 +256,8 @@ function.  Let's memoize a function which generates the $n^{th}$ number in the
 @memoize
 def fib(n):
 
-    if n in (1, 2):
-        print('fib({}) = 1'.format(n))
+    if n in (0, 1):
+        print('fib({}) = {}'.format(n, n))
         return 1
 
     twoback = 1
@@ -222,6 +275,7 @@ def fib(n):
     return val
 ```
 
+
 For a given input, when `fib` is called the first time, it will calculate the
 $n^{th}$ Fibonacci number:
 
@@ -242,6 +296,10 @@ for i in range(10):
 ```
 
 
+> If you are wondering how the `wrapper` function is able to access the
+> `cache` variable, refer to the [appendix on closures](#appendix-closures).
+
+
 ## Decorators with arguments
 
 
@@ -400,12 +458,109 @@ expensiveFunc(2)
 
 ## Decorator classes
 
-> not to be confused with _class decorators_ - see the
-> [appendix](#appendix-class-decorators)
 
+By now, you will have gained the impression that a decorator is a function
+which _decorates_ another function. But if you went through the practical on
+operator overloading, you might remember the special `__call__` method, that
+allows an object to be called as if it were a function.
+
+
+This feature allows us to write our decorators as classes, instead of
+functions. This can be handy if you are writing a decorator that has
+complicated behaviour, and/or needs to maintain some sort of state which
+cannot be easily or elegantly written using nested functions.
+
+
+As an example, let's say we are writing a framework for unit testing. We want
+to be able to "mark" our test functions like so, so they can be easily
+identified and executed:
+
+
+> ```
+> @unitTest
+> def testblerk():
+>     """tests the blerk algorithm."""
+>     ...
+> ```
+
+
+With a decorator like this, we wouldn't need to worry about where our tests
+are located - they will all be detected because we have marked them as test
+functions. What does this `unitTest` decorator look like?
+
+
+```
+class TestRegistry(object):
 
+    def __init__(self):
+        self.testFuncs = []
+
+    def __call__(self, func):
+        self.testFuncs.append(func)
+
+    def listTests(self):
+        print('All registered tests:')
+        for test in self.testFuncs:
+            print(' ', test.__name__)
+
+    def runTests(self):
+        for test in self.testFuncs:
+            print('Running test {:10s} ...'.format(test.__name__), end='')
+            try:
+                test()
+                print('passed!')
+            except Exception as e:
+                print('failed!')
+
+# Create a test registry, and
+# alias it to "unitTest" so
+# that we can register tests
+# with a "@unitTest" decorator.
+registry = TestRegistry()
+unitTest = registry
+```
+
+So we've defined a class, `TestRegistry`, which will manage all of our unit
+tests. Now, in order to "mark" any function as being a unit test, we just need
+to use the `unitTest` decorator:
+
+
+```
+@unitTest
+def testFoo():
+    assert 'a' in 'bcde'
+
+@unitTest
+def testBar():
+    assert 1 > 0
+
+@unitTest
+def testBlerk():
+    assert 9 % 2 == 0
+```
+
+
+Now that these functions have been registered with our `TestRegistry`
+instance, we can run them all:
+
+```
+registry.listTests()
+registry.runTests()
+```
 
 
+> Unit testing is something which you must do! This is __especially__
+> important in an interpreted language such as Python, where there is no
+> compiler to catch all of your mistakes.
+>
+> Python has a built-in
+> [`unittest`](https://docs.python.org/3.5/library/unittest.html) module,
+> however the third-party [`pytest`](https://docs.pytest.org/en/latest/) and
+> [`nose`](http://nose2.readthedocs.io/en/latest/) are popular.  It is also
+> wise to combine your unit tests with
+> [`coverage`](https://coverage.readthedocs.io/en/coverage-4.5.1/), which
+> tells you how much of your code was executed, or _covered_ when your
+> tests were run.
 
 
 ## Appendix: Functions are not special
@@ -451,8 +606,8 @@ upon the list<sup>2</sup>.
 
 
 If you are familiar with C or C++, you can think of a variable in Python as
-like a `void *` pointer - it is just a pointer of an undefined type, which is
-pointing to some item in memory (which does have a specific type). Deleting
+like a `void *` pointer - it is just a pointer of an unspecified type, which
+is pointing to some item in memory (which does have a specific type). Deleting
 the pointer does not have any effect upon the item to which it was pointing.
 
 
@@ -508,77 +663,127 @@ as we like.
 
 
 > If it bothers you that `print(inv2)` resulted in
-> `<function inverse at ...>`, and not `<function inv2 at ...>`, then keep
-> going straight through to the
-> [next appendix](#appendix-preserving-function-metadata).
+> `<function inverse at ...>`, and not `<function inv2 at ...>`, then refer to
+> the appendix on
+> [preserving function metdata](#appendix-preserving-function-metadata).
 
 
-## Appendix: Preserving function metadata
+## Appendix: Closures
 
 
-TODO `functools.wraps`
+Whenever we define or use a decorator, we are taking advantage of a concept
+called a [_closure_][wiki-closure]. Take a second to re-familiarise yourself
+with our `memoize` decorator function from earlier - when `memoize` is called,
+it creates and returns a function called `wrapper`:
 
 
-## Appendix: Closures
+[wiki-closure]: https://en.wikipedia.org/wiki/Closure_(computer_programming)
 
 
+```
+def memoize(func):
 
-Whenever we define or use a decorator, we are taking advantage of a concept
-called a [_closure_](https://www.geeksforgeeks.org/python-closures/). Take a
-second to re-familiarise yourself with our `timeFunc` decorator function from
-earlier - when `timeFunc` is called, it creates and returns `wrapperFunc`:
+    cache = {}
+
+    def wrapper(*args):
 
+        # is there a value in the cache
+        # for this set of inputs?
+        cached = cache.get(args, None)
 
+        # If not, call the function,
+        # and cache the result.
+        if cached is None:
+            cached      = func(*args)
+            cache[args] = cached
+        else:
+            print('Cached {}({}): {}'.format(func.__name__, args, cached))
+
+        return cached
+
+    return wrapper
 ```
-import time
-def timeFunc(func):
 
-    def wrapperFunc(*args, **kwargs):
 
-        start  = time.time()
-        retval = func(*args, **kwargs)
-        end    = time.time()
+Then `wrapper` is executed at some arbitrary point in the future. But how does
+it have access to `cache`, defined within the scope of the `memoize` function,
+after the execution of `memoize` has ended?
 
-        print('Ran {} in {:0.2f} seconds'.format(func.__name__, end - start))
 
-        return retval
+```
+def nby2(n):
+    return n * 2
 
-    return wrapperFunc
+# wrapper function is created here (and
+# assigned back to the nby2 reference)
+nby2 = memoize(nby2)
+
+# wrapper function is executed here
+print('nby2(2): ', nby2(2))
+print('nby2(2): ', nby2(2))
 ```
 
 
-Then `wrapperFunc` is executed at some arbitrary point in the future. But how
-does it have access to `func`, defined within the scope of the `timeFunc`
-function, after the execution of `timeFunc` has ended?
+The trick is that whenever a nested function is defined in Python, the scope
+in which it is defined is preserved for that function's lifetime. So `wrapper`
+has access to all of the variables within the `memoize` function's scope, that
+were defined at the time that `wrapper` was created (which was when we called
+`memoize`).  This is why `wrapper` is able to access `cache`, even though at
+the time that `wrapper` is called, the execution of `memoize` has long since
+finished.
+
+
+This is what is known as a
+[_closure_](https://www.geeksforgeeks.org/python-closures/). Closures are a
+fundamental, and extremely powerful, aspect of Python and other high level
+languages. So there's your answer,
+[fishbulb](https://www.youtube.com/watch?v=CiAaEPcnlOg).
+
+
+## Appendix: Preserving function metadata
+
+
+TODO `functools.wraps`
+
+
+## Appendix: per-instance decorators
+
+
+Below, we have defined a class called `Multiplier`, which has a method
+`multiply` that multiplies some number by a fixed (but changeable) constant,
+and is memoized via the `@memoize` decorator we [defined
+earlier](#example-memoization).
+
+
+Can you spot the problem with this implementation?
 
 
 ```
-def pause(secs):
-    print('Pausing for {} seconds ...'.format(secs))
-    time.sleep(secs)
-    print('Finished pausing')
+class Multiplier(object):
+    def __init__(self):
+        self.__multiplier = 1
 
-# wrapperFunc is created here
-pause = timeFunc(pause)
+    @property
+    def multiplier(self):
+        return self.multiplier
 
-# wrapperFunc is executed here
-pause(2)
+    @multiplier.setter
+    def multiplier(self, m):
+        self.multiplier = m
+
+    @memoize
+    def multiply(self, x):
+        return x * self.multiplier
 ```
 
 
-The trick is that whenever a nested function is defined in Python, the scope
-in which it is defined is preserved for that function's lifetime. So
-`wrapperFunc` has access to all of the variables within the `timeFunc`
-function's scope, that were defined at the time that `wrapperFunc` was created
-(which was when we called `timeFunc`).  This is why `wrapperFunc` is able to
-access `func`, even though at the time that `wrapperFunc` is called, the
-execution of `timeFunc` has long since finished.
+TODO Instanecify
 
 
-This is what is known as a _closure_. Closures are a fundamental, and
-extremely powerful, aspect of Python and other high level languages such as
-Javascript. So there's your answer,
-[fishbulb](https://www.youtube.com/watch?v=CiAaEPcnlOg).
+## Appendix: class decorators
+
+
+TODO
 
 
 ## Appendix: Decorators without arguments versus decorators with arguments
@@ -587,9 +792,9 @@ Javascript. So there's your answer,
 There are three ways to invoke a decorator with the `@` notation:
 
 
-1. Naming it (e.g. `@mydecorator`)
-2. Calling it (e.g. `@mydecorator()`)
-3. Calling it, and passing it arguments (e.g. `@mydecorator(1, 2, 3)`)
+1. Naming it, e.g. `@mydecorator`
+2. Calling it, e.g. `@mydecorator()`
+3. Calling it, and passing it arguments, e.g. `@mydecorator(1, 2, 3)`
 
 
 Python expects a decorator function to behave differently in the second and
@@ -663,16 +868,14 @@ def decorator(*args):
 > ```
 
 
-## Appendix: class decorators
-
-
-TODO
-
-
 ## Useful references
 
 
 * [Decorator tutorial](http://blog.thedigitalcatonline.com/blog/2015/04/23/python-decorators-metaprogramming-with-style/)
 * [Another decorator tutorial](https://realpython.com/blog/python/primer-on-python-decorators/)
+* [The decorators they won't tell you about](https://github.com/hchasestevens/hchasestevens.github.io/blob/master/notebooks/the-decorators-they-wont-tell-you-about.ipynb)
+* [Closures - Wikipedia][wiki-closure]
 * [Closures in Python](https://www.geeksforgeeks.org/python-closures/)
 * [Garbage collection in Python](https://www.quora.com/How-does-garbage-collection-in-Python-work-What-are-the-pros-and-cons)
+
+[wiki-closure]: https://en.wikipedia.org/wiki/Closure_(computer_programming)
-- 
GitLab