From 1e6a4277deaac1ec798ea44d58ba5e54744f42b8 Mon Sep 17 00:00:00 2001
From: Paul McCarthy <pauldmccarthy@gmail.com>
Date: Mon, 12 Feb 2018 21:50:02 +0000
Subject: [PATCH] operator overloading practical finished

---
 advanced_topics/operator_overloading.ipynb | 269 ++++++++++++++++-----
 advanced_topics/operator_overloading.md    | 241 +++++++++++++-----
 2 files changed, 387 insertions(+), 123 deletions(-)

diff --git a/advanced_topics/operator_overloading.ipynb b/advanced_topics/operator_overloading.ipynb
index 45a7207..1b9ab7f 100644
--- a/advanced_topics/operator_overloading.ipynb
+++ b/advanced_topics/operator_overloading.ipynb
@@ -17,6 +17,22 @@
     "overloading is __very__ easy to do in Python.\n",
     "\n",
     "\n",
+    "This practical gives a brief overview of the operators which you may be most\n",
+    "interested in implementing. However, there are many operators (and other\n",
+    "special methods) which you can support in your own classes - the [official\n",
+    "documentation](https://docs.python.org/3.5/reference/datamodel.html#basic-customization)\n",
+    "is the best reference if you are interested in learning more.\n",
+    "\n",
+    "\n",
+    "* [Overview](#overview)\n",
+    "* [Arithmetic operators](#arithmetic-operators)\n",
+    "* [Equality and comparison operators](#equality-and-comparison-operators)\n",
+    "* [The indexing operator `[]`](#the-indexing-operator)\n",
+    "* [The call operator `()`](#the-call-operator)\n",
+    "* [The dot operator `.`](#the-dot-operator)\n",
+    "\n",
+    "\n",
+    "<a class=\"anchor\" id=\"overview\"></a>\n",
     "## Overview\n",
     "\n",
     "\n",
@@ -66,6 +82,7 @@
     "to do is implement a method called `__add__`.\n",
     "\n",
     "\n",
+    "<a class=\"anchor\" id=\"arithmetic-operators\"></a>\n",
     "## Arithmetic operators\n",
     "\n",
     "\n",
@@ -78,13 +95,13 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "class Vector(object):\n",
+    "class Vector2D(object):\n",
     "    def __init__(self, x, y):\n",
     "        self.x = x\n",
     "        self.y = y\n",
     "\n",
     "    def __str__(self):\n",
-    "        return 'Vector({}, {})'.format(self.x, self.y)"
+    "        return 'Vector2D({}, {})'.format(self.x, self.y)"
    ]
   },
   {
@@ -92,7 +109,7 @@
    "metadata": {},
    "source": [
     "> Note that we have implemented the special `__str__` method, which allows our\n",
-    "> `Vector` instances to be converted into strings.\n",
+    "> `Vector2D` instances to be converted into strings.\n",
     "\n",
     "\n",
     "If we try to use the `+` operator on this class, we are bound to get an error:"
@@ -104,8 +121,8 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "v1 = Vector(2, 3)\n",
-    "v2 = Vector(4, 5)\n",
+    "v1 = Vector2D(2, 3)\n",
+    "v2 = Vector2D(4, 5)\n",
     "print(v1 + v2)"
    ]
   },
@@ -123,24 +140,24 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "class Vector(object):\n",
+    "class Vector2D(object):\n",
     "    def __init__(self, x, y):\n",
     "        self.x = x\n",
     "        self.y = y\n",
     "\n",
     "    def __str__(self):\n",
-    "        return 'Vector({}, {})'.format(self.x, self.y)\n",
+    "        return 'Vector2D({}, {})'.format(self.x, self.y)\n",
     "\n",
     "    def __add__(self, other):\n",
-    "        return Vector(self.x + other.x,\n",
-    "                      self.y + other.y)"
+    "        return Vector2D(self.x + other.x,\n",
+    "                        self.y + other.y)"
    ]
   },
   {
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "And now we can use `+` on `Vector` objects - it's that easy:"
+    "And now we can use `+` on `Vector2D` objects - it's that easy:"
    ]
   },
   {
@@ -149,8 +166,8 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "v1 = Vector(2, 3)\n",
-    "v2 = Vector(4, 5)\n",
+    "v1 = Vector2D(2, 3)\n",
+    "v2 = Vector2D(4, 5)\n",
     "print('{} + {} = {}'.format(v1, v2, v1 + v2))"
    ]
   },
@@ -158,10 +175,10 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "Our `__add__` method creates and returns a new `Vector` which contains the sum\n",
-    "of the `x` and `y` components of the `Vector` on which it is called, and the\n",
-    "`Vector` which is passed in.  We could also make the `__add__` method work\n",
-    "with scalars, by extending its definition a bit:"
+    "Our `__add__` method creates and returns a new `Vector2D` which contains the\n",
+    "sum of the `x` and `y` components of the `Vector2D` on which it is called, and\n",
+    "the `Vector2D` which is passed in.  We could also make the `__add__` method\n",
+    "work with scalars, by extending its definition a bit:"
    ]
   },
   {
@@ -170,27 +187,27 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "class Vector(object):\n",
+    "class Vector2D(object):\n",
     "    def __init__(self, x, y):\n",
     "        self.x = x\n",
     "        self.y = y\n",
     "\n",
     "    def __add__(self, other):\n",
-    "        if isinstance(other, Vector):\n",
-    "            return Vector(self.x + other.x,\n",
-    "                          self.y + other.y)\n",
+    "        if isinstance(other, Vector2D):\n",
+    "            return Vector2D(self.x + other.x,\n",
+    "                            self.y + other.y)\n",
     "        else:\n",
-    "            return Vector(self.x + other, self.y + other)\n",
+    "            return Vector2D(self.x + other, self.y + other)\n",
     "\n",
     "    def __str__(self):\n",
-    "        return 'Vector({}, {})'.format(self.x, self.y)"
+    "        return 'Vector2D({}, {})'.format(self.x, self.y)"
    ]
   },
   {
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "So now we can add both `Vectors` and scalars numbers together:"
+    "So now we can add both `Vector2D` instances and scalars numbers together:"
    ]
   },
   {
@@ -199,8 +216,8 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "v1 = Vector(2, 3)\n",
-    "v2 = Vector(4, 5)\n",
+    "v1 = Vector2D(2, 3)\n",
+    "v2 = Vector2D(4, 5)\n",
     "n  = 6\n",
     "\n",
     "print('{} + {} = {}'.format(v1, v2, v1 + v2))\n",
@@ -211,7 +228,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "Other nuemric and logical operators can be supported by implementing the\n",
+    "Other numeric and logical operators can be supported by implementing the\n",
     "appropriate method, for example:\n",
     "\n",
     "- Multiplication (`*`): `__mul__`\n",
@@ -227,6 +244,7 @@
     "support.\n",
     "\n",
     "\n",
+    "<a class=\"anchor\" id=\"equality-and-comparison-operators\"></a>\n",
     "## Equality and comparison operators\n",
     "\n",
     "\n",
@@ -266,7 +284,10 @@
    "source": [
     "import functools\n",
     "\n",
+    "# Don't worry about this statement\n",
+    "# just yet - it is explained below\n",
     "@functools.total_ordering\n",
+    "\n",
     "class Label(object):\n",
     "    def __init__(self, label, name, colour):\n",
     "        self.label  = label\n",
@@ -280,9 +301,11 @@
     "    def __repr__(self):\n",
     "        return str(self)\n",
     "\n",
+    "    # implement Label == Label\n",
     "    def __eq__(self, other):\n",
     "        return self.label == other.label\n",
     "\n",
+    "    # implement Label < Label\n",
     "    def __lt__(self, other):\n",
     "        return self.label < other.label"
    ]
@@ -318,17 +341,23 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "The `@functools.total_ordering` is a convenience decorator which, given a\n",
-    "class that implements equality and a single comparison function (`__lt__` in\n",
-    "the above code), will \"fill in\" the remainder of the comparison operators.  If\n",
-    "you need very specific or complicated behaviour, then you can provide methods\n",
-    "for _all_ of the comparison operators, e.g. `__gt__` for `>`, `__ge__` for\n",
-    "`>=`, etc.).\n",
+    "The\n",
+    "[`@functools.total_ordering`](https://docs.python.org/3.5/library/functools.html#functools.total_ordering)\n",
+    "is a convenience\n",
+    "[decorator](https://docs.python.org/3.5/glossary.html#term-decorator) which,\n",
+    "given a class that implements equality and a single comparison function\n",
+    "(`__lt__` in the above code), will \"fill in\" the remainder of the comparison\n",
+    "operators.  If you need very specific or complicated behaviour, then you can\n",
+    "provide methods for _all_ of the comparison operators, e.g. `__gt__` for `>`,\n",
+    "`__ge__` for `>=`, etc.).\n",
+    "\n",
+    "\n",
+    "> Decorators are introduced in another practical.\n",
     "\n",
     "\n",
     "But if you just want the operators to work in the conventional manner, you can\n",
-    "just use the `@functools.total_ordering` decorator, and provide `__eq__`, and\n",
-    "just one of `__lt__`, `__le__`, `__gt__` or `__ge__`.\n",
+    "simply use the `@functools.total_ordering` decorator, and provide `__eq__`,\n",
+    "and just one of `__lt__`, `__le__`, `__gt__` or `__ge__`.\n",
     "\n",
     "\n",
     "Refer to the [official\n",
@@ -338,10 +367,11 @@
     "\n",
     "> You may see the `__cmp__` method in older code bases - this provides a\n",
     "> C-style comparison function which returns `<0`, `0`, or `>0` based on\n",
-    "> comparing two items. This has been superseded by the rich\n",
-    "> comparison operators and is no longer used in Python 3.\n",
+    "> comparing two items. This has been superseded by the rich comparison\n",
+    "> operators introduced here, and is no longer supported in Python 3.\n",
     "\n",
     "\n",
+    "<a class=\"anchor\" id=\"the-indexing-operator\"></a>\n",
     "## The indexing operator `[]`\n",
     "\n",
     "\n",
@@ -352,8 +382,9 @@
     "At its essence, there are only three types of behaviours that are possible\n",
     "with the `[]` operator. All that is needed to support them are to implement\n",
     "three special methods in your class, regardless of whether your class will be\n",
-    "indexed by sequential integers (like a `list`) or by hashable values (like a\n",
-    "`dict`):\n",
+    "indexed by sequential integers (like a `list`) or by\n",
+    "[hashable](https://docs.python.org/3.5/glossary.html#term-hashable) values\n",
+    "(like a `dict`):\n",
     "\n",
     "\n",
     "- __Retrieval__ is performed by the `__getitem__` method\n",
@@ -362,9 +393,11 @@
     "\n",
     "\n",
     "Note that, if you implement these methods in your own class, there is no\n",
-    "requirement for them to actually provide any form. However if you don't, you\n",
-    "will probably confuse users of your code - make sure you explain it all in\n",
-    "your comprehensive documentation!\n",
+    "requirement for them to actually provide any form of data storage or\n",
+    "retrieval. However if you don't, you will probably confuse users of your code\n",
+    "who are used to how the `list` and `dict` types work. Whenever you deviate\n",
+    "from conventional behaviour, make sure you explain it well in your\n",
+    "documentation!\n",
     "\n",
     "\n",
     "The following contrived example demonstrates all three behaviours:"
@@ -384,7 +417,7 @@
     "\n",
     "    def __getitem__(self, key):\n",
     "        if key in self.__deleted:\n",
-    "            raise KeyError('{} has been deleted!')\n",
+    "            raise KeyError('{} has been deleted!'.format(key))\n",
     "        elif key in self.__assigned:\n",
     "            return self.__assigned[key]\n",
     "        else:\n",
@@ -420,8 +453,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "For some unknown reason, the `TwoTimes` class allows us to override the value\n",
-    "for a specific key:"
+    "The `TwoTimes` class allows us to override the value for a specific key:"
    ]
   },
   {
@@ -460,8 +492,8 @@
    "metadata": {},
    "source": [
     "If you wish to support the Python `start:stop:step` [slice\n",
-    "notation](https://www.pythoncentral.io/how-to-slice-listsarrays-and-tuples-in-python/),\n",
-    "you simply need to write your `__getitem__` and `__setitem__` methods so that they\n",
+    "notation](https://docs.python.org/3.5/library/functions.html#slice), you\n",
+    "simply need to write your `__getitem__` and `__setitem__` methods so that they\n",
     "can detect `slice` objects:"
    ]
   },
@@ -489,6 +521,13 @@
     "        return [i * 2 for i in range(start, stop, step)]"
    ]
   },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Now we can \"slice\" a `TwoTimes` instance:"
+   ]
+  },
   {
    "cell_type": "code",
    "execution_count": null,
@@ -517,6 +556,7 @@
     "> module.\n",
     "\n",
     "\n",
+    "<a class=\"anchor\" id=\"the-call-operator\"></a>\n",
     "## The call operator `()`\n",
     "\n",
     "\n",
@@ -527,16 +567,96 @@
     "\n",
     "\n",
     "For example, the `TimedFunction` class allows us to calculate the execution\n",
-    "time of any function:\n",
+    "time of any function:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import time\n",
+    "\n",
+    "class TimedFunction(object):\n",
     "\n",
+    "    def __init__(self, func):\n",
+    "        self.func = func\n",
     "\n",
+    "    def __call__(self, *args, **kwargs):\n",
+    "        print('Timing {}...'.format(self.func.__name__))\n",
+    "\n",
+    "        start  = time.time()\n",
+    "        retval = self.func(*args, **kwargs)\n",
+    "        end    = time.time()\n",
+    "\n",
+    "        print('Elapsed time: {:0.2f} seconds'.format(end - start))\n",
+    "        return retval"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Let's see how the `TimedFunction` behaves:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import numpy        as np\n",
+    "import numpy.linalg as npla\n",
+    "\n",
+    "def inverse(data):\n",
+    "    return npla.inv(data)\n",
+    "\n",
+    "tf   = TimedFunction(inverse)\n",
+    "data = np.random.random((5000, 5000))\n",
+    "\n",
+    "# Wait a few seconds after\n",
+    "# running this code block!\n",
+    "inv = tf(data)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "> The `TimedFunction` class is conceptually very similar to a\n",
+    "> [decorator](https://docs.python.org/3.5/glossary.html#term-decorator) -\n",
+    "> decorators are covered in another practical.\n",
+    "\n",
+    "\n",
+    "<a class=\"anchor\" id=\"the-dot-operator\"></a>\n",
     "## The dot operator `.`\n",
     "\n",
     "\n",
     "Python allows us to override the `.` (dot) operator which is used to access\n",
-    "the attributes and methods of an object.  attributes and methods of an\n",
-    "object. This is a fairly niche feature, and you need to be careful that you\n",
-    "don't unintentionally introduce recursive attribute lookups into your code."
+    "the attributes and methods of an object.  This is very powerful, but is also\n",
+    "quite a niche feature, and it is easy to trip yourself up, so if you wish to\n",
+    "use this in your own project, make sure that you carefully read (and\n",
+    "understand) [the\n",
+    "documentation](https://docs.python.org/3.5/reference/datamodel.html#customizing-attribute-access),\n",
+    "and test your code comprehensively!\n",
+    "\n",
+    "\n",
+    "For this example, we need a little background information.  OpenGL includes\n",
+    "the native data types `vec2`, `vec3`, and `vec4`, which can be used to\n",
+    "represent 2, 3, or 4 component vectors respectively. These data types have a\n",
+    "neat feature called [_swizzling_][glslref], which allows you to access any\n",
+    "component (`x`,`y`, `z`, `w` for vectors, or `r`, `g`, `b`, `a` for colours)\n",
+    "in any order, with a syntax similar to attribute access in Python.\n",
+    "\n",
+    "\n",
+    "[glslref]: https://www.khronos.org/opengl/wiki/Data_Type_(GLSL)#Swizzling\n",
+    "\n",
+    "\n",
+    "So here is an example which implements this swizzle-style attribute access on\n",
+    "a class called `Vector`, in which we have customised the behaviour of the `.`\n",
+    "operator:"
    ]
   },
   {
@@ -550,35 +670,58 @@
     "        self.__xyz = list(xyz)\n",
     "\n",
     "    def __str__(self):\n",
-    "        return 'Vector({})'.format(', '.join(self.__xyz))\n",
+    "        return 'Vector({})'.format(self.__xyz)\n",
     "\n",
     "    def __getattr__(self, key):\n",
+    "\n",
+    "        # Swizzling behaviour only occurs when\n",
+    "        # the attribute name is entirely comprised\n",
+    "        # of 'x', 'y', and 'z'.\n",
+    "        if not all([c in 'xyz' for c in key]):\n",
+    "            raise AttributeError(key)\n",
+    "\n",
     "        key = ['xyz'.index(c) for c in key]\n",
     "        return [self.__xyz[c] for c in key]\n",
     "\n",
     "    def __setattr__(self, key, value):\n",
-    "        pass        # key = ['xyz'.index(c) for c in key]"
+    "\n",
+    "        # Restrict swizzling behaviour as above\n",
+    "        if not all([c in 'xyz' for c in key]):\n",
+    "            return super().__setattr__(key, value)\n",
+    "\n",
+    "        if len(key) == 1:\n",
+    "            value = (value,)\n",
+    "\n",
+    "        idxs = ['xyz'.index(c) for c in key]\n",
+    "\n",
+    "        for i, v in sorted(zip(idxs, value)):\n",
+    "            self.__xyz[i] = v\n"
    ]
   },
   {
-   "cell_type": "code",
-   "execution_count": null,
+   "cell_type": "markdown",
    "metadata": {},
-   "outputs": [],
    "source": [
-    "v = Vector((4, 5, 6))\n",
-    "\n",
-    "print('xyz: ', v.xyz)\n",
-    "print('yz:  ', v.yz)\n",
-    "print('zxy: ', v.xzy)\n",
-    "print('y:   ', v.y)"
+    "And here it is in action:"
    ]
   },
   {
-   "cell_type": "markdown",
+   "cell_type": "code",
+   "execution_count": null,
    "metadata": {},
+   "outputs": [],
    "source": [
-    "## Other special methods"
+    "v = Vector((1, 2, 3))\n",
+    "\n",
+    "print('v:   ', v)\n",
+    "print('xyz: ', v.xyz)\n",
+    "print('yz:  ', v.zy)\n",
+    "print('xx:  ', v.xx)\n",
+    "\n",
+    "v.xz = 10, 30\n",
+    "print(v)\n",
+    "v.y = 20\n",
+    "print(v)"
    ]
   }
  ],
diff --git a/advanced_topics/operator_overloading.md b/advanced_topics/operator_overloading.md
index 3e78721..142368f 100644
--- a/advanced_topics/operator_overloading.md
+++ b/advanced_topics/operator_overloading.md
@@ -11,6 +11,22 @@ process of customising the behaviour of _operators_ (e.g. `+`, `*`, `/` and
 overloading is __very__ easy to do in Python.
 
 
+This practical gives a brief overview of the operators which you may be most
+interested in implementing. However, there are many operators (and other
+special methods) which you can support in your own classes - the [official
+documentation](https://docs.python.org/3.5/reference/datamodel.html#basic-customization)
+is the best reference if you are interested in learning more.
+
+
+* [Overview](#overview)
+* [Arithmetic operators](#arithmetic-operators)
+* [Equality and comparison operators](#equality-and-comparison-operators)
+* [The indexing operator `[]`](#the-indexing-operator)
+* [The call operator `()`](#the-call-operator)
+* [The dot operator `.`](#the-dot-operator)
+
+
+<a class="anchor" id="overview"></a>
 ## Overview
 
 
@@ -44,6 +60,7 @@ So it is very easy to use the `+` operator with our own classes - all we have
 to do is implement a method called `__add__`.
 
 
+<a class="anchor" id="arithmetic-operators"></a>
 ## Arithmetic operators
 
 
@@ -51,26 +68,26 @@ Let's play with an example - a class which represents a 2D vector:
 
 
 ```
-class Vector(object):
+class Vector2D(object):
     def __init__(self, x, y):
         self.x = x
         self.y = y
 
     def __str__(self):
-        return 'Vector({}, {})'.format(self.x, self.y)
+        return 'Vector2D({}, {})'.format(self.x, self.y)
 ```
 
 
 > Note that we have implemented the special `__str__` method, which allows our
-> `Vector` instances to be converted into strings.
+> `Vector2D` instances to be converted into strings.
 
 
 If we try to use the `+` operator on this class, we are bound to get an error:
 
 
 ```
-v1 = Vector(2, 3)
-v2 = Vector(4, 5)
+v1 = Vector2D(2, 3)
+v2 = Vector2D(4, 5)
 print(v1 + v2)
 ```
 
@@ -80,60 +97,60 @@ called `__add__`:
 
 
 ```
-class Vector(object):
+class Vector2D(object):
     def __init__(self, x, y):
         self.x = x
         self.y = y
 
     def __str__(self):
-        return 'Vector({}, {})'.format(self.x, self.y)
+        return 'Vector2D({}, {})'.format(self.x, self.y)
 
     def __add__(self, other):
-        return Vector(self.x + other.x,
-                      self.y + other.y)
+        return Vector2D(self.x + other.x,
+                        self.y + other.y)
 ```
 
 
-And now we can use `+` on `Vector` objects - it's that easy:
+And now we can use `+` on `Vector2D` objects - it's that easy:
 
 
 ```
-v1 = Vector(2, 3)
-v2 = Vector(4, 5)
+v1 = Vector2D(2, 3)
+v2 = Vector2D(4, 5)
 print('{} + {} = {}'.format(v1, v2, v1 + v2))
 ```
 
 
-Our `__add__` method creates and returns a new `Vector` which contains the sum
-of the `x` and `y` components of the `Vector` on which it is called, and the
-`Vector` which is passed in.  We could also make the `__add__` method work
-with scalars, by extending its definition a bit:
+Our `__add__` method creates and returns a new `Vector2D` which contains the
+sum of the `x` and `y` components of the `Vector2D` on which it is called, and
+the `Vector2D` which is passed in.  We could also make the `__add__` method
+work with scalars, by extending its definition a bit:
 
 
 ```
-class Vector(object):
+class Vector2D(object):
     def __init__(self, x, y):
         self.x = x
         self.y = y
 
     def __add__(self, other):
-        if isinstance(other, Vector):
-            return Vector(self.x + other.x,
-                          self.y + other.y)
+        if isinstance(other, Vector2D):
+            return Vector2D(self.x + other.x,
+                            self.y + other.y)
         else:
-            return Vector(self.x + other, self.y + other)
+            return Vector2D(self.x + other, self.y + other)
 
     def __str__(self):
-        return 'Vector({}, {})'.format(self.x, self.y)
+        return 'Vector2D({}, {})'.format(self.x, self.y)
 ```
 
 
-So now we can add both `Vectors` and scalars numbers together:
+So now we can add both `Vector2D` instances and scalars numbers together:
 
 
 ```
-v1 = Vector(2, 3)
-v2 = Vector(4, 5)
+v1 = Vector2D(2, 3)
+v2 = Vector2D(4, 5)
 n  = 6
 
 print('{} + {} = {}'.format(v1, v2, v1 + v2))
@@ -141,7 +158,7 @@ print('{} + {} = {}'.format(v1, n,  v1 + n))
 ```
 
 
-Other nuemric and logical operators can be supported by implementing the
+Other numeric and logical operators can be supported by implementing the
 appropriate method, for example:
 
 - Multiplication (`*`): `__mul__`
@@ -157,6 +174,7 @@ for a full list of the arithmetic and logical operators that your classes can
 support.
 
 
+<a class="anchor" id="equality-and-comparison-operators"></a>
 ## Equality and comparison operators
 
 
@@ -183,7 +201,10 @@ compared using the standard comparison operators:
 ```
 import functools
 
+# Don't worry about this statement
+# just yet - it is explained below
 @functools.total_ordering
+
 class Label(object):
     def __init__(self, label, name, colour):
         self.label  = label
@@ -197,13 +218,16 @@ class Label(object):
     def __repr__(self):
         return str(self)
 
+    # implement Label == Label
     def __eq__(self, other):
         return self.label == other.label
 
+    # implement Label < Label
     def __lt__(self, other):
         return self.label < other.label
 ```
 
+
 > We also added `__str__` and `__repr__` methods to the `Label` class so that
 > `Label` instances will be printed nicely.
 
@@ -223,17 +247,23 @@ print(sorted((l3, l1, l2)))
 ```
 
 
-The `@functools.total_ordering` is a convenience decorator which, given a
-class that implements equality and a single comparison function (`__lt__` in
-the above code), will "fill in" the remainder of the comparison operators.  If
-you need very specific or complicated behaviour, then you can provide methods
-for _all_ of the comparison operators, e.g. `__gt__` for `>`, `__ge__` for
-`>=`, etc.).
+The
+[`@functools.total_ordering`](https://docs.python.org/3.5/library/functools.html#functools.total_ordering)
+is a convenience
+[decorator](https://docs.python.org/3.5/glossary.html#term-decorator) which,
+given a class that implements equality and a single comparison function
+(`__lt__` in the above code), will "fill in" the remainder of the comparison
+operators.  If you need very specific or complicated behaviour, then you can
+provide methods for _all_ of the comparison operators, e.g. `__gt__` for `>`,
+`__ge__` for `>=`, etc.).
+
+
+> Decorators are introduced in another practical.
 
 
 But if you just want the operators to work in the conventional manner, you can
-just use the `@functools.total_ordering` decorator, and provide `__eq__`, and
-just one of `__lt__`, `__le__`, `__gt__` or `__ge__`.
+simply use the `@functools.total_ordering` decorator, and provide `__eq__`,
+and just one of `__lt__`, `__le__`, `__gt__` or `__ge__`.
 
 
 Refer to the [official
@@ -243,10 +273,11 @@ for all of the details on supporting comparison operators.
 
 > You may see the `__cmp__` method in older code bases - this provides a
 > C-style comparison function which returns `<0`, `0`, or `>0` based on
-> comparing two items. This has been superseded by the rich
-> comparison operators and is no longer used in Python 3.
+> comparing two items. This has been superseded by the rich comparison
+> operators introduced here, and is no longer supported in Python 3.
 
 
+<a class="anchor" id="the-indexing-operator"></a>
 ## The indexing operator `[]`
 
 
@@ -257,8 +288,9 @@ the built-in `list` and `dict` classes.
 At its essence, there are only three types of behaviours that are possible
 with the `[]` operator. All that is needed to support them are to implement
 three special methods in your class, regardless of whether your class will be
-indexed by sequential integers (like a `list`) or by hashable values (like a
-`dict`):
+indexed by sequential integers (like a `list`) or by
+[hashable](https://docs.python.org/3.5/glossary.html#term-hashable) values
+(like a `dict`):
 
 
 - __Retrieval__ is performed by the `__getitem__` method
@@ -267,9 +299,11 @@ indexed by sequential integers (like a `list`) or by hashable values (like a
 
 
 Note that, if you implement these methods in your own class, there is no
-requirement for them to actually provide any form. However if you don't, you
-will probably confuse users of your code - make sure you explain it all in
-your comprehensive documentation!
+requirement for them to actually provide any form of data storage or
+retrieval. However if you don't, you will probably confuse users of your code
+who are used to how the `list` and `dict` types work. Whenever you deviate
+from conventional behaviour, make sure you explain it well in your
+documentation!
 
 
 The following contrived example demonstrates all three behaviours:
@@ -284,7 +318,7 @@ class TwoTimes(object):
 
     def __getitem__(self, key):
         if key in self.__deleted:
-            raise KeyError('{} has been deleted!')
+            raise KeyError('{} has been deleted!'.format(key))
         elif key in self.__assigned:
             return self.__assigned[key]
         else:
@@ -309,8 +343,7 @@ print('TwoTimes[{}] = {}'.format('abc', tt['abc']))
 ```
 
 
-For some unknown reason, the `TwoTimes` class allows us to override the value
-for a specific key:
+The `TwoTimes` class allows us to override the value for a specific key:
 
 
 ```
@@ -333,8 +366,8 @@ print(tt['12345'])
 
 
 If you wish to support the Python `start:stop:step` [slice
-notation](https://www.pythoncentral.io/how-to-slice-listsarrays-and-tuples-in-python/),
-you simply need to write your `__getitem__` and `__setitem__` methods so that they
+notation](https://docs.python.org/3.5/library/functions.html#slice), you
+simply need to write your `__getitem__` and `__setitem__` methods so that they
 can detect `slice` objects:
 
 
@@ -357,6 +390,10 @@ class TwoTimes(object):
         return [i * 2 for i in range(start, stop, step)]
 ```
 
+
+Now we can "slice" a `TwoTimes` instance:
+
+
 ```
 tt = TwoTimes(10)
 
@@ -366,8 +403,6 @@ print(tt[::2])
 ```
 
 
-
-
 > It is possible to sub-class the built-in `list` and `dict` classes if you
 > wish to extend their functionality in some way. However, if you are writing
 > a class that should mimic the one of the `list` or `dict` classes, but work
@@ -379,6 +414,7 @@ print(tt[::2])
 > module.
 
 
+<a class="anchor" id="the-call-operator"></a>
 ## The call operator `()`
 
 
@@ -392,16 +428,77 @@ For example, the `TimedFunction` class allows us to calculate the execution
 time of any function:
 
 
+```
+import time
+
+class TimedFunction(object):
+
+    def __init__(self, func):
+        self.func = func
+
+    def __call__(self, *args, **kwargs):
+        print('Timing {}...'.format(self.func.__name__))
+
+        start  = time.time()
+        retval = self.func(*args, **kwargs)
+        end    = time.time()
+
+        print('Elapsed time: {:0.2f} seconds'.format(end - start))
+        return retval
+```
+
+
+Let's see how the `TimedFunction` behaves:
+
+
+```
+import numpy        as np
+import numpy.linalg as npla
+
+def inverse(data):
+    return npla.inv(data)
+
+tf   = TimedFunction(inverse)
+data = np.random.random((5000, 5000))
+
+# Wait a few seconds after
+# running this code block!
+inv = tf(data)
+```
+
+
+> The `TimedFunction` class is conceptually very similar to a
+> [decorator](https://docs.python.org/3.5/glossary.html#term-decorator) -
+> decorators are covered in another practical.
+
+
+<a class="anchor" id="the-dot-operator"></a>
 ## The dot operator `.`
 
 
 Python allows us to override the `.` (dot) operator which is used to access
-the attributes and methods of an object.  attributes and methods of an
-object. This is a fairly niche feature, and you need to be careful that you
-don't unintentionally introduce recursive attribute lookups into your code.
+the attributes and methods of an object.  This is very powerful, but is also
+quite a niche feature, and it is easy to trip yourself up, so if you wish to
+use this in your own project, make sure that you carefully read (and
+understand) [the
+documentation](https://docs.python.org/3.5/reference/datamodel.html#customizing-attribute-access),
+and test your code comprehensively!
+
+
+For this example, we need a little background information.  OpenGL includes
+the native data types `vec2`, `vec3`, and `vec4`, which can be used to
+represent 2, 3, or 4 component vectors respectively. These data types have a
+neat feature called [_swizzling_][glslref], which allows you to access any
+component (`x`,`y`, `z`, `w` for vectors, or `r`, `g`, `b`, `a` for colours)
+in any order, with a syntax similar to attribute access in Python.
+
 
+[glslref]: https://www.khronos.org/opengl/wiki/Data_Type_(GLSL)#Swizzling
 
 
+So here is an example which implements this swizzle-style attribute access on
+a class called `Vector`, in which we have customised the behaviour of the `.`
+operator:
 
 
 ```
@@ -410,25 +507,49 @@ class Vector(object):
         self.__xyz = list(xyz)
 
     def __str__(self):
-        return 'Vector({})'.format(', '.join(self.__xyz))
+        return 'Vector({})'.format(self.__xyz)
 
     def __getattr__(self, key):
+
+        # Swizzling behaviour only occurs when
+        # the attribute name is entirely comprised
+        # of 'x', 'y', and 'z'.
+        if not all([c in 'xyz' for c in key]):
+            raise AttributeError(key)
+
         key = ['xyz'.index(c) for c in key]
         return [self.__xyz[c] for c in key]
 
     def __setattr__(self, key, value):
-        pass        # key = ['xyz'.index(c) for c in key]
-```
 
+        # Restrict swizzling behaviour as above
+        if not all([c in 'xyz' for c in key]):
+            return super().__setattr__(key, value)
+
+        if len(key) == 1:
+            value = (value,)
+
+        idxs = ['xyz'.index(c) for c in key]
+
+        for i, v in sorted(zip(idxs, value)):
+            self.__xyz[i] = v
 
 ```
-v = Vector((4, 5, 6))
 
-print('xyz: ', v.xyz)
-print('yz:  ', v.yz)
-print('zxy: ', v.xzy)
-print('y:   ', v.y)
+
+And here it is in action:
+
+
 ```
+v = Vector((1, 2, 3))
 
+print('v:   ', v)
+print('xyz: ', v.xyz)
+print('yz:  ', v.zy)
+print('xx:  ', v.xx)
 
-## Other special methods
+v.xz = 10, 30
+print(v)
+v.y = 20
+print(v)
+```
-- 
GitLab