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