From 076a73f9e63f801f6e1c40132512aeadb2b0ad1f Mon Sep 17 00:00:00 2001
From: Paul McCarthy <pauldmccarthy@gmail.com>
Date: Thu, 15 Feb 2018 14:13:33 +0000
Subject: [PATCH] more updates to ctxman prac

---
 advanced_topics/05_context_managers.ipynb | 284 +++++++++++++++++++++-
 advanced_topics/05_context_managers.md    | 236 +++++++++++++++++-
 2 files changed, 512 insertions(+), 8 deletions(-)

diff --git a/advanced_topics/05_context_managers.ipynb b/advanced_topics/05_context_managers.ipynb
index b313229..7f2f3d3 100644
--- a/advanced_topics/05_context_managers.ipynb
+++ b/advanced_topics/05_context_managers.ipynb
@@ -382,8 +382,8 @@
     "In fact, there is another way to create context managers in Python. The\n",
     "built-in [`contextlib`\n",
     "module](https://docs.python.org/3.5/library/contextlib.html#contextlib.contextmanager)\n",
-    "has a decorator called `@contextmanager`, which allows us to turn __any__\n",
-    "function into a context manager.  The only requirement is that the function\n",
+    "has a decorator called `@contextmanager`, which allows us to turn __any\n",
+    "function__ into a context manager.  The only requirement is that the function\n",
     "must have a `yield` statement<sup>1</sup>. So we could rewrite our `TempDir`\n",
     "class from above as a function:"
    ]
@@ -447,8 +447,270 @@
     "> beyond the scope of this practical.\n",
     "\n",
     "\n",
+    "## Methods as context managers\n",
+    "\n",
+    "\n",
     "Since it is possible to write a function which is a context manager, it is of\n",
-    "course also possible to write a _method_ which is a context manager."
+    "course also possible to write a _method_ which is a context manager. Let's\n",
+    "play with another example. We have a `Notifier` class which can be used to\n",
+    "notify interested listeners when an event occurs. Listeners can be registered\n",
+    "for notification via the `register` method:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from collections import OrderedDict\n",
+    "\n",
+    "class Notifier(object):\n",
+    "    def __init__(self):\n",
+    "        super().__init__()\n",
+    "        self.listeners = OrderedDict()\n",
+    "\n",
+    "    def register(self, name, func):\n",
+    "        self.listeners[name] = func\n",
+    "\n",
+    "    def notify(self):\n",
+    "        for listener in self.listeners.values():\n",
+    "            listener()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Now, let's build a little plotting application. First of all, we have a `Line`\n",
+    "class, which represents a line plot. The `Line` class is a sub-class of\n",
+    "`Notifier`, so whenever its display properties (`colour`, `width`, or `name`)\n",
+    "change, it emits a notification, and whatever is drawing it can refresh the\n",
+    "display:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import numpy as np\n",
+    "\n",
+    "class Line(Notifier):\n",
+    "\n",
+    "    def __init__(self, data):\n",
+    "        super().__init__()\n",
+    "        self.__data   = data\n",
+    "        self.__colour = '#000000'\n",
+    "        self.__width  = 1\n",
+    "        self.__name   = 'line'\n",
+    "\n",
+    "    @property\n",
+    "    def xdata(self):\n",
+    "        return np.arange(len(self.__data))\n",
+    "\n",
+    "    @property\n",
+    "    def ydata(self):\n",
+    "        return np.copy(self.__data)\n",
+    "\n",
+    "    @property\n",
+    "    def colour(self):\n",
+    "        return self.__colour\n",
+    "\n",
+    "    @colour.setter\n",
+    "    def colour(self, newColour):\n",
+    "        self.__colour = newColour\n",
+    "        print('Line: colour changed: {}'.format(newColour))\n",
+    "        self.notify()\n",
+    "\n",
+    "    @property\n",
+    "    def width(self):\n",
+    "        return self.__width\n",
+    "\n",
+    "    @width.setter\n",
+    "    def width(self, newWidth):\n",
+    "        self.__width = newWidth\n",
+    "        print('Line: width changed: {}'.format(newWidth))\n",
+    "        self.notify()\n",
+    "\n",
+    "    @property\n",
+    "    def name(self):\n",
+    "        return self.__name\n",
+    "\n",
+    "    @name.setter\n",
+    "    def name(self, newName):\n",
+    "        self.__name = newName\n",
+    "        print('Line: name changed: {}'.format(newName))\n",
+    "        self.notify()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Now let's write a `Plotter` class, which can plot one or more `Line`\n",
+    "instances:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import matplotlib.pyplot as plt\n",
+    "\n",
+    "# this line is only necessary when\n",
+    "# working in jupyer notebook/ipython\n",
+    "%matplotlib\n",
+    "\n",
+    "class Plotter(object):\n",
+    "    def __init__(self, axis):\n",
+    "        self.__axis   = axis\n",
+    "        self.__lines  = []\n",
+    "\n",
+    "    def addData(self, data):\n",
+    "        line = Line(data)\n",
+    "        self.__lines.append(line)\n",
+    "        line.register('plot', self.lineChanged)\n",
+    "        self.draw()\n",
+    "        return line\n",
+    "\n",
+    "    def lineChanged(self):\n",
+    "        self.draw()\n",
+    "\n",
+    "    def draw(self):\n",
+    "        print('Plotter: redrawing plot')\n",
+    "\n",
+    "        ax = self.__axis\n",
+    "        ax.clear()\n",
+    "        for line in self.__lines:\n",
+    "            ax.plot(line.xdata,\n",
+    "                    line.ydata,\n",
+    "                    color=line.colour,\n",
+    "                    linewidth=line.width,\n",
+    "                    label=line.name)\n",
+    "        ax.legend()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Let's create a `Plotter` object, and add a couple of lines to it (note that\n",
+    "the `matplotlib` plot will open in a separate window):"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "fig     = plt.figure()\n",
+    "ax      = fig.add_subplot(111)\n",
+    "plotter = Plotter(ax)\n",
+    "l1      = plotter.addData(np.sin(np.linspace(0, 6 * np.pi, 50)))\n",
+    "l2      = plotter.addData(np.cos(np.linspace(0, 6 * np.pi, 50)))\n",
+    "\n",
+    "fig.show()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Now, when we change the properties of our `Line` instances, the plot will be\n",
+    "automatically updated:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "l1.colour = '#ff0000'\n",
+    "l2.colour = '#00ff00'\n",
+    "l1.width  = 2\n",
+    "l2.width  = 2\n",
+    "l1.name   = 'sine'\n",
+    "l2.name   = 'cosine'"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Pretty cool! However, this seems very inefficient - every time we change the\n",
+    "properties of a `Line`, the `Plotter` will refresh the plot. If we were\n",
+    "plotting large amounts of data, this would be unacceptable, as plotting would\n",
+    "simply take too long.\n",
+    "\n",
+    "\n",
+    "Wouldn't it be nice if we were able to perform batch-updates of `Line`\n",
+    "properties, and only refresh the plot when we are done? Let's add an extra\n",
+    "method to the `Plotter` class:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import contextlib\n",
+    "\n",
+    "class Plotter(object):\n",
+    "    def __init__(self, axis):\n",
+    "        self.__axis        = axis\n",
+    "        self.__lines       = []\n",
+    "        self.__holdUpdates = False\n",
+    "\n",
+    "    def addData(self, data):\n",
+    "        line = Line(data)\n",
+    "        self.__lines.append(line)\n",
+    "        line.register('plot', self.lineChanged)\n",
+    "\n",
+    "        if not self.__holdUpdates:\n",
+    "            self.draw()\n",
+    "        return line\n",
+    "\n",
+    "    def lineChanged(self):\n",
+    "        if not self.__holdUpdates:\n",
+    "            self.draw()\n",
+    "\n",
+    "    def draw(self):\n",
+    "        print('Plotter: redrawing plot')\n",
+    "\n",
+    "        ax = self.__axis\n",
+    "        ax.clear()\n",
+    "        for line in self.__lines:\n",
+    "            ax.plot(line.xdata,\n",
+    "                    line.ydata,\n",
+    "                    color=line.colour,\n",
+    "                    linewidth=line.width,\n",
+    "                    label=line.name)\n",
+    "        ax.legend()\n",
+    "\n",
+    "    @contextlib.contextmanager\n",
+    "    def holdUpdates(self):\n",
+    "        self.__holdUpdates = True\n",
+    "        try:\n",
+    "            yield\n",
+    "            self.draw()\n",
+    "        finally:\n",
+    "            self.__holdUpdates = False"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "This new `holdUpdates` method allows us to temporarily suppress notifications\n",
+    "from all `Line` instances. So now, we can update many `Line` properties\n",
+    "without performing any redundant redraws:"
    ]
   },
   {
@@ -457,7 +719,21 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "TODO suppress notification example"
+    "fig     = plt.figure()\n",
+    "ax      = fig.add_subplot(111)\n",
+    "plotter = Plotter(ax)\n",
+    "\n",
+    "plt.show()\n",
+    "\n",
+    "with plotter.holdUpdates():\n",
+    "    l1        = plotter.addData(np.sin(np.linspace(0, 6 * np.pi, 50)))\n",
+    "    l2        = plotter.addData(np.cos(np.linspace(0, 6 * np.pi, 50)))\n",
+    "    l1.colour = '#0000ff'\n",
+    "    l2.colour = '#ffff00'\n",
+    "    l1.width  = 1\n",
+    "    l2.width  = 1\n",
+    "    l1.name   = '$sin(x)$'\n",
+    "    l2.name   = '$cos(x)$'"
    ]
   },
   {
diff --git a/advanced_topics/05_context_managers.md b/advanced_topics/05_context_managers.md
index f32df44..13ff3dd 100644
--- a/advanced_topics/05_context_managers.md
+++ b/advanced_topics/05_context_managers.md
@@ -271,8 +271,8 @@ with MyContextManager():
 In fact, there is another way to create context managers in Python. The
 built-in [`contextlib`
 module](https://docs.python.org/3.5/library/contextlib.html#contextlib.contextmanager)
-has a decorator called `@contextmanager`, which allows us to turn __any__
-function into a context manager.  The only requirement is that the function
+has a decorator called `@contextmanager`, which allows us to turn __any
+function__ into a context manager.  The only requirement is that the function
 must have a `yield` statement<sup>1</sup>. So we could rewrite our `TempDir`
 class from above as a function:
 
@@ -319,12 +319,240 @@ print('Back in directory: {}'.format(os.getcwd()))
 > beyond the scope of this practical.
 
 
+## Methods as context managers
+
+
 Since it is possible to write a function which is a context manager, it is of
-course also possible to write a _method_ which is a context manager.
+course also possible to write a _method_ which is a context manager. Let's
+play with another example. We have a `Notifier` class which can be used to
+notify interested listeners when an event occurs. Listeners can be registered
+for notification via the `register` method:
+
+
+```
+from collections import OrderedDict
+
+class Notifier(object):
+    def __init__(self):
+        super().__init__()
+        self.listeners = OrderedDict()
 
+    def register(self, name, func):
+        self.listeners[name] = func
 
+    def notify(self):
+        for listener in self.listeners.values():
+            listener()
 ```
-TODO suppress notification example
+
+
+Now, let's build a little plotting application. First of all, we have a `Line`
+class, which represents a line plot. The `Line` class is a sub-class of
+`Notifier`, so whenever its display properties (`colour`, `width`, or `name`)
+change, it emits a notification, and whatever is drawing it can refresh the
+display:
+
+
+```
+import numpy as np
+
+class Line(Notifier):
+
+    def __init__(self, data):
+        super().__init__()
+        self.__data   = data
+        self.__colour = '#000000'
+        self.__width  = 1
+        self.__name   = 'line'
+
+    @property
+    def xdata(self):
+        return np.arange(len(self.__data))
+
+    @property
+    def ydata(self):
+        return np.copy(self.__data)
+
+    @property
+    def colour(self):
+        return self.__colour
+
+    @colour.setter
+    def colour(self, newColour):
+        self.__colour = newColour
+        print('Line: colour changed: {}'.format(newColour))
+        self.notify()
+
+    @property
+    def width(self):
+        return self.__width
+
+    @width.setter
+    def width(self, newWidth):
+        self.__width = newWidth
+        print('Line: width changed: {}'.format(newWidth))
+        self.notify()
+
+    @property
+    def name(self):
+        return self.__name
+
+    @name.setter
+    def name(self, newName):
+        self.__name = newName
+        print('Line: name changed: {}'.format(newName))
+        self.notify()
+```
+
+
+Now let's write a `Plotter` class, which can plot one or more `Line`
+instances:
+
+
+```
+import matplotlib.pyplot as plt
+
+# this line is only necessary when
+# working in jupyer notebook/ipython
+%matplotlib
+
+class Plotter(object):
+    def __init__(self, axis):
+        self.__axis   = axis
+        self.__lines  = []
+
+    def addData(self, data):
+        line = Line(data)
+        self.__lines.append(line)
+        line.register('plot', self.lineChanged)
+        self.draw()
+        return line
+
+    def lineChanged(self):
+        self.draw()
+
+    def draw(self):
+        print('Plotter: redrawing plot')
+
+        ax = self.__axis
+        ax.clear()
+        for line in self.__lines:
+            ax.plot(line.xdata,
+                    line.ydata,
+                    color=line.colour,
+                    linewidth=line.width,
+                    label=line.name)
+        ax.legend()
+```
+
+
+Let's create a `Plotter` object, and add a couple of lines to it (note that
+the `matplotlib` plot will open in a separate window):
+
+
+```
+fig     = plt.figure()
+ax      = fig.add_subplot(111)
+plotter = Plotter(ax)
+l1      = plotter.addData(np.sin(np.linspace(0, 6 * np.pi, 50)))
+l2      = plotter.addData(np.cos(np.linspace(0, 6 * np.pi, 50)))
+
+fig.show()
+```
+
+
+Now, when we change the properties of our `Line` instances, the plot will be
+automatically updated:
+
+
+```
+l1.colour = '#ff0000'
+l2.colour = '#00ff00'
+l1.width  = 2
+l2.width  = 2
+l1.name   = 'sine'
+l2.name   = 'cosine'
+```
+
+
+Pretty cool! However, this seems very inefficient - every time we change the
+properties of a `Line`, the `Plotter` will refresh the plot. If we were
+plotting large amounts of data, this would be unacceptable, as plotting would
+simply take too long.
+
+
+Wouldn't it be nice if we were able to perform batch-updates of `Line`
+properties, and only refresh the plot when we are done? Let's add an extra
+method to the `Plotter` class:
+
+
+```
+import contextlib
+
+class Plotter(object):
+    def __init__(self, axis):
+        self.__axis        = axis
+        self.__lines       = []
+        self.__holdUpdates = False
+
+    def addData(self, data):
+        line = Line(data)
+        self.__lines.append(line)
+        line.register('plot', self.lineChanged)
+
+        if not self.__holdUpdates:
+            self.draw()
+        return line
+
+    def lineChanged(self):
+        if not self.__holdUpdates:
+            self.draw()
+
+    def draw(self):
+        print('Plotter: redrawing plot')
+
+        ax = self.__axis
+        ax.clear()
+        for line in self.__lines:
+            ax.plot(line.xdata,
+                    line.ydata,
+                    color=line.colour,
+                    linewidth=line.width,
+                    label=line.name)
+        ax.legend()
+
+    @contextlib.contextmanager
+    def holdUpdates(self):
+        self.__holdUpdates = True
+        try:
+            yield
+            self.draw()
+        finally:
+            self.__holdUpdates = False
+```
+
+
+This new `holdUpdates` method allows us to temporarily suppress notifications
+from all `Line` instances. So now, we can update many `Line` properties
+without performing any redundant redraws:
+
+
+```
+fig     = plt.figure()
+ax      = fig.add_subplot(111)
+plotter = Plotter(ax)
+
+plt.show()
+
+with plotter.holdUpdates():
+    l1        = plotter.addData(np.sin(np.linspace(0, 6 * np.pi, 50)))
+    l2        = plotter.addData(np.cos(np.linspace(0, 6 * np.pi, 50)))
+    l1.colour = '#0000ff'
+    l2.colour = '#ffff00'
+    l1.width  = 1
+    l2.width  = 1
+    l1.name   = '$sin(x)$'
+    l2.name   = '$cos(x)$'
 ```
 
 
-- 
GitLab