From 9f3718f08173db574a67f5c8efbe9e4969060f8b Mon Sep 17 00:00:00 2001
From: Paul McCarthy <pauldmccarthy@gmail.com>
Date: Mon, 12 Feb 2018 18:10:52 +0000
Subject: [PATCH] New practical on operator overloading. Needs work

---
 advanced_topics/operator_overloading.ipynb | 588 +++++++++++++++++++++
 advanced_topics/operator_overloading.md    | 434 +++++++++++++++
 2 files changed, 1022 insertions(+)
 create mode 100644 advanced_topics/operator_overloading.ipynb
 create mode 100644 advanced_topics/operator_overloading.md

diff --git a/advanced_topics/operator_overloading.ipynb b/advanced_topics/operator_overloading.ipynb
new file mode 100644
index 0000000..45a7207
--- /dev/null
+++ b/advanced_topics/operator_overloading.ipynb
@@ -0,0 +1,588 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Operator overloading\n",
+    "\n",
+    "\n",
+    "> This practical assumes you are familiar with the basics of object-oriented\n",
+    "> programming in Python.\n",
+    "\n",
+    "\n",
+    "Operator overloading, in an object-oriented programming language, is the\n",
+    "process of customising the behaviour of _operators_ (e.g. `+`, `*`, `/` and\n",
+    "`-`) on user-defined types. This practical aims to show you that operator\n",
+    "overloading is __very__ easy to do in Python.\n",
+    "\n",
+    "\n",
+    "## Overview\n",
+    "\n",
+    "\n",
+    "In Python, when you add two numbers together:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "a = 5\n",
+    "b = 10\n",
+    "r = a + b\n",
+    "print(r)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "What actually goes on behind the scenes is this:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "r = a.__add__(b)\n",
+    "print(r)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "In other words, whenever you use the `+` operator on two variables (the\n",
+    "operands to the `+` operator), the Python interpreter calls the `__add__`\n",
+    "method of the first operand (`a`), and passes the second operand (`b`) as an\n",
+    "argument.\n",
+    "\n",
+    "\n",
+    "So it is very easy to use the `+` operator with our own classes - all we have\n",
+    "to do is implement a method called `__add__`.\n",
+    "\n",
+    "\n",
+    "## Arithmetic operators\n",
+    "\n",
+    "\n",
+    "Let's play with an example - a class which represents a 2D vector:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class Vector(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)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "> Note that we have implemented the special `__str__` method, which allows our\n",
+    "> `Vector` 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:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "v1 = Vector(2, 3)\n",
+    "v2 = Vector(4, 5)\n",
+    "print(v1 + v2)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "But all we need to do to support the `+` operator is to implement a method\n",
+    "called `__add__`:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class Vector(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",
+    "\n",
+    "    def __add__(self, other):\n",
+    "        return Vector(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:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "v1 = Vector(2, 3)\n",
+    "v2 = Vector(4, 5)\n",
+    "print('{} + {} = {}'.format(v1, v2, v1 + v2))"
+   ]
+  },
+  {
+   "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:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class Vector(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",
+    "        else:\n",
+    "            return Vector(self.x + other, self.y + other)\n",
+    "\n",
+    "    def __str__(self):\n",
+    "        return 'Vector({}, {})'.format(self.x, self.y)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "So now we can add both `Vectors` and scalars numbers together:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "v1 = Vector(2, 3)\n",
+    "v2 = Vector(4, 5)\n",
+    "n  = 6\n",
+    "\n",
+    "print('{} + {} = {}'.format(v1, v2, v1 + v2))\n",
+    "print('{} + {} = {}'.format(v1, n,  v1 + n))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Other nuemric and logical operators can be supported by implementing the\n",
+    "appropriate method, for example:\n",
+    "\n",
+    "- Multiplication (`*`): `__mul__`\n",
+    "- Division (`/`): `__div__`\n",
+    "- Negation (`-`): `__neg__`\n",
+    "- In-place addition (`+=`): `__iadd__`\n",
+    "- Exclusive or (`^`): `__xor__`\n",
+    "\n",
+    "\n",
+    "Take a look at the [official\n",
+    "documentation](https://docs.python.org/3.5/reference/datamodel.html#emulating-numeric-types)\n",
+    "for a full list of the arithmetic and logical operators that your classes can\n",
+    "support.\n",
+    "\n",
+    "\n",
+    "## Equality and comparison operators\n",
+    "\n",
+    "\n",
+    "Adding support for equality (`==`, `!=`) and comparison (e.g. `>=`) operators\n",
+    "is just as easy. Imagine that we have a class called `Label`, which represents\n",
+    "a label in a lookup table. Our `Label` has an integer label, a name, and an\n",
+    "RGB colour:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class Label(object):\n",
+    "    def __init__(self, label, name, colour):\n",
+    "        self.label  = label\n",
+    "        self.name   = name\n",
+    "        self.colour = colour"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "In order to ensure that a list of `Label` objects is ordered by their label\n",
+    "values, we can implement a set of functions, so that `Label` classes can be\n",
+    "compared using the standard comparison operators:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import functools\n",
+    "\n",
+    "@functools.total_ordering\n",
+    "class Label(object):\n",
+    "    def __init__(self, label, name, colour):\n",
+    "        self.label  = label\n",
+    "        self.name   = name\n",
+    "        self.colour = colour\n",
+    "\n",
+    "    def __str__(self):\n",
+    "        rgb = ''.join(['{:02x}'.format(c) for c in self.colour])\n",
+    "        return 'Label({}, {}, #{})'.format(self.label, self.name, rgb)\n",
+    "\n",
+    "    def __repr__(self):\n",
+    "        return str(self)\n",
+    "\n",
+    "    def __eq__(self, other):\n",
+    "        return self.label == other.label\n",
+    "\n",
+    "    def __lt__(self, other):\n",
+    "        return self.label < other.label"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "> We also added `__str__` and `__repr__` methods to the `Label` class so that\n",
+    "> `Label` instances will be printed nicely.\n",
+    "\n",
+    "\n",
+    "Now we can compare and sort our `Label` instances:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "l1 = Label(1, 'Parietal',  (255,   0,   0))\n",
+    "l2 = Label(2, 'Occipital', (  0, 255,   0))\n",
+    "l3 = Label(3, 'Temporal',  (  0,   0, 255))\n",
+    "\n",
+    "print('{} >  {}: {}'.format(l1, l2, l1  > l2))\n",
+    "print('{} <  {}: {}'.format(l1, l3, l1  < l3))\n",
+    "print('{} != {}: {}'.format(l2, l3, l2 != l3))\n",
+    "print(sorted((l3, l1, l2)))"
+   ]
+  },
+  {
+   "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",
+    "\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",
+    "\n",
+    "\n",
+    "Refer to the [official\n",
+    "documentation](https://docs.python.org/3.5/reference/datamodel.html#object.__lt__)\n",
+    "for all of the details on supporting comparison operators.\n",
+    "\n",
+    "\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",
+    "\n",
+    "\n",
+    "## The indexing operator `[]`\n",
+    "\n",
+    "\n",
+    "The indexing operator (`[]`) is generally used by \"container\" types, such as\n",
+    "the built-in `list` and `dict` classes.\n",
+    "\n",
+    "\n",
+    "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",
+    "\n",
+    "\n",
+    "- __Retrieval__ is performed by the `__getitem__` method\n",
+    "- __Assignment__ is performed by the `__setitem__` method\n",
+    "- __Deletion__ is performed by the `__delitem__` method\n",
+    "\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",
+    "\n",
+    "\n",
+    "The following contrived example demonstrates all three behaviours:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class TwoTimes(object):\n",
+    "\n",
+    "    def __init__(self):\n",
+    "        self.__deleted  = set()\n",
+    "        self.__assigned = {}\n",
+    "\n",
+    "    def __getitem__(self, key):\n",
+    "        if key in self.__deleted:\n",
+    "            raise KeyError('{} has been deleted!')\n",
+    "        elif key in self.__assigned:\n",
+    "            return self.__assigned[key]\n",
+    "        else:\n",
+    "            return key * 2\n",
+    "\n",
+    "    def __setitem__(self, key, value):\n",
+    "        self.__assigned[key] = value\n",
+    "\n",
+    "    def __delitem__(self, key):\n",
+    "        self.__deleted.add(key)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Guess what happens whenever we index a `TwoTimes` object:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "tt = TwoTimes()\n",
+    "print('TwoTimes[{}] = {}'.format(2,     tt[2]))\n",
+    "print('TwoTimes[{}] = {}'.format(6,     tt[6]))\n",
+    "print('TwoTimes[{}] = {}'.format('abc', tt['abc']))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "For some unknown reason, the `TwoTimes` class allows us to override the value\n",
+    "for a specific key:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(tt[4])\n",
+    "tt[4] = 'this is not 4 * 4'\n",
+    "print(tt[4])"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "And we can also \"delete\" keys:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(tt['12345'])\n",
+    "del tt['12345']\n",
+    "\n",
+    "# this is going to raise an error\n",
+    "print(tt['12345'])"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "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",
+    "can detect `slice` objects:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class TwoTimes(object):\n",
+    "\n",
+    "    def __init__(self, max):\n",
+    "        self.__max = max\n",
+    "\n",
+    "    def __getitem__(self, key):\n",
+    "        if isinstance(key, slice):\n",
+    "            start = key.start or 0\n",
+    "            stop  = key.stop  or self.__max\n",
+    "            step  = key.step  or 1\n",
+    "        else:\n",
+    "            start = key\n",
+    "            stop  = key + 1\n",
+    "            step  = 1\n",
+    "\n",
+    "        return [i * 2 for i in range(start, stop, step)]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "tt = TwoTimes(10)\n",
+    "\n",
+    "print(tt[5])\n",
+    "print(tt[3:7])\n",
+    "print(tt[::2])"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "> It is possible to sub-class the built-in `list` and `dict` classes if you\n",
+    "> wish to extend their functionality in some way. However, if you are writing\n",
+    "> a class that should mimic the one of the `list` or `dict` classes, but work\n",
+    "> in a different way internally (e.g. a `dict`-like object which uses a\n",
+    "> different hashing algorithm), the `Sequence` and `MutableMapping` classes\n",
+    "> are [a better choice](https://stackoverflow.com/a/7148602) - you can find\n",
+    "> them in the\n",
+    "> [`collections.abc`](https://docs.python.org/3.5/library/collections.abc.html)\n",
+    "> module.\n",
+    "\n",
+    "\n",
+    "## The call operator `()`\n",
+    "\n",
+    "\n",
+    "Remember how everything in Python is an object, even functions? When you call\n",
+    "a function, a method called `__call__` is called on the function object. We can\n",
+    "implement the `__call__` method on our own class, which will allow us to \"call\"\n",
+    "objects as if they are functions.\n",
+    "\n",
+    "\n",
+    "For example, the `TimedFunction` class allows us to calculate the execution\n",
+    "time of any function:\n",
+    "\n",
+    "\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."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class Vector(object):\n",
+    "    def __init__(self, xyz):\n",
+    "        self.__xyz = list(xyz)\n",
+    "\n",
+    "    def __str__(self):\n",
+    "        return 'Vector({})'.format(', '.join(self.__xyz))\n",
+    "\n",
+    "    def __getattr__(self, key):\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]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "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)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Other special methods"
+   ]
+  }
+ ],
+ "metadata": {},
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/advanced_topics/operator_overloading.md b/advanced_topics/operator_overloading.md
new file mode 100644
index 0000000..3e78721
--- /dev/null
+++ b/advanced_topics/operator_overloading.md
@@ -0,0 +1,434 @@
+# Operator overloading
+
+
+> This practical assumes you are familiar with the basics of object-oriented
+> programming in Python.
+
+
+Operator overloading, in an object-oriented programming language, is the
+process of customising the behaviour of _operators_ (e.g. `+`, `*`, `/` and
+`-`) on user-defined types. This practical aims to show you that operator
+overloading is __very__ easy to do in Python.
+
+
+## Overview
+
+
+In Python, when you add two numbers together:
+
+
+```
+a = 5
+b = 10
+r = a + b
+print(r)
+```
+
+
+What actually goes on behind the scenes is this:
+
+
+```
+r = a.__add__(b)
+print(r)
+```
+
+
+In other words, whenever you use the `+` operator on two variables (the
+operands to the `+` operator), the Python interpreter calls the `__add__`
+method of the first operand (`a`), and passes the second operand (`b`) as an
+argument.
+
+
+So it is very easy to use the `+` operator with our own classes - all we have
+to do is implement a method called `__add__`.
+
+
+## Arithmetic operators
+
+
+Let's play with an example - a class which represents a 2D vector:
+
+
+```
+class Vector(object):
+    def __init__(self, x, y):
+        self.x = x
+        self.y = y
+
+    def __str__(self):
+        return 'Vector({}, {})'.format(self.x, self.y)
+```
+
+
+> Note that we have implemented the special `__str__` method, which allows our
+> `Vector` 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)
+print(v1 + v2)
+```
+
+
+But all we need to do to support the `+` operator is to implement a method
+called `__add__`:
+
+
+```
+class Vector(object):
+    def __init__(self, x, y):
+        self.x = x
+        self.y = y
+
+    def __str__(self):
+        return 'Vector({}, {})'.format(self.x, self.y)
+
+    def __add__(self, other):
+        return Vector(self.x + other.x,
+                      self.y + other.y)
+```
+
+
+And now we can use `+` on `Vector` objects - it's that easy:
+
+
+```
+v1 = Vector(2, 3)
+v2 = Vector(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:
+
+
+```
+class Vector(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)
+        else:
+            return Vector(self.x + other, self.y + other)
+
+    def __str__(self):
+        return 'Vector({}, {})'.format(self.x, self.y)
+```
+
+
+So now we can add both `Vectors` and scalars numbers together:
+
+
+```
+v1 = Vector(2, 3)
+v2 = Vector(4, 5)
+n  = 6
+
+print('{} + {} = {}'.format(v1, v2, v1 + v2))
+print('{} + {} = {}'.format(v1, n,  v1 + n))
+```
+
+
+Other nuemric and logical operators can be supported by implementing the
+appropriate method, for example:
+
+- Multiplication (`*`): `__mul__`
+- Division (`/`): `__div__`
+- Negation (`-`): `__neg__`
+- In-place addition (`+=`): `__iadd__`
+- Exclusive or (`^`): `__xor__`
+
+
+Take a look at the [official
+documentation](https://docs.python.org/3.5/reference/datamodel.html#emulating-numeric-types)
+for a full list of the arithmetic and logical operators that your classes can
+support.
+
+
+## Equality and comparison operators
+
+
+Adding support for equality (`==`, `!=`) and comparison (e.g. `>=`) operators
+is just as easy. Imagine that we have a class called `Label`, which represents
+a label in a lookup table. Our `Label` has an integer label, a name, and an
+RGB colour:
+
+
+```
+class Label(object):
+    def __init__(self, label, name, colour):
+        self.label  = label
+        self.name   = name
+        self.colour = colour
+```
+
+
+In order to ensure that a list of `Label` objects is ordered by their label
+values, we can implement a set of functions, so that `Label` classes can be
+compared using the standard comparison operators:
+
+
+```
+import functools
+
+@functools.total_ordering
+class Label(object):
+    def __init__(self, label, name, colour):
+        self.label  = label
+        self.name   = name
+        self.colour = colour
+
+    def __str__(self):
+        rgb = ''.join(['{:02x}'.format(c) for c in self.colour])
+        return 'Label({}, {}, #{})'.format(self.label, self.name, rgb)
+
+    def __repr__(self):
+        return str(self)
+
+    def __eq__(self, other):
+        return self.label == other.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.
+
+
+Now we can compare and sort our `Label` instances:
+
+
+```
+l1 = Label(1, 'Parietal',  (255,   0,   0))
+l2 = Label(2, 'Occipital', (  0, 255,   0))
+l3 = Label(3, 'Temporal',  (  0,   0, 255))
+
+print('{} >  {}: {}'.format(l1, l2, l1  > l2))
+print('{} <  {}: {}'.format(l1, l3, l1  < l3))
+print('{} != {}: {}'.format(l2, l3, l2 != l3))
+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.).
+
+
+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__`.
+
+
+Refer to the [official
+documentation](https://docs.python.org/3.5/reference/datamodel.html#object.__lt__)
+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.
+
+
+## The indexing operator `[]`
+
+
+The indexing operator (`[]`) is generally used by "container" types, such as
+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`):
+
+
+- __Retrieval__ is performed by the `__getitem__` method
+- __Assignment__ is performed by the `__setitem__` method
+- __Deletion__ is performed by the `__delitem__` method
+
+
+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!
+
+
+The following contrived example demonstrates all three behaviours:
+
+
+```
+class TwoTimes(object):
+
+    def __init__(self):
+        self.__deleted  = set()
+        self.__assigned = {}
+
+    def __getitem__(self, key):
+        if key in self.__deleted:
+            raise KeyError('{} has been deleted!')
+        elif key in self.__assigned:
+            return self.__assigned[key]
+        else:
+            return key * 2
+
+    def __setitem__(self, key, value):
+        self.__assigned[key] = value
+
+    def __delitem__(self, key):
+        self.__deleted.add(key)
+```
+
+
+Guess what happens whenever we index a `TwoTimes` object:
+
+
+```
+tt = TwoTimes()
+print('TwoTimes[{}] = {}'.format(2,     tt[2]))
+print('TwoTimes[{}] = {}'.format(6,     tt[6]))
+print('TwoTimes[{}] = {}'.format('abc', tt['abc']))
+```
+
+
+For some unknown reason, the `TwoTimes` class allows us to override the value
+for a specific key:
+
+
+```
+print(tt[4])
+tt[4] = 'this is not 4 * 4'
+print(tt[4])
+```
+
+
+And we can also "delete" keys:
+
+
+```
+print(tt['12345'])
+del tt['12345']
+
+# this is going to raise an error
+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
+can detect `slice` objects:
+
+
+```
+class TwoTimes(object):
+
+    def __init__(self, max):
+        self.__max = max
+
+    def __getitem__(self, key):
+        if isinstance(key, slice):
+            start = key.start or 0
+            stop  = key.stop  or self.__max
+            step  = key.step  or 1
+        else:
+            start = key
+            stop  = key + 1
+            step  = 1
+
+        return [i * 2 for i in range(start, stop, step)]
+```
+
+```
+tt = TwoTimes(10)
+
+print(tt[5])
+print(tt[3:7])
+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
+> in a different way internally (e.g. a `dict`-like object which uses a
+> different hashing algorithm), the `Sequence` and `MutableMapping` classes
+> are [a better choice](https://stackoverflow.com/a/7148602) - you can find
+> them in the
+> [`collections.abc`](https://docs.python.org/3.5/library/collections.abc.html)
+> module.
+
+
+## The call operator `()`
+
+
+Remember how everything in Python is an object, even functions? When you call
+a function, a method called `__call__` is called on the function object. We can
+implement the `__call__` method on our own class, which will allow us to "call"
+objects as if they are functions.
+
+
+For example, the `TimedFunction` class allows us to calculate the execution
+time of any function:
+
+
+## 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.
+
+
+
+
+
+```
+class Vector(object):
+    def __init__(self, xyz):
+        self.__xyz = list(xyz)
+
+    def __str__(self):
+        return 'Vector({})'.format(', '.join(self.__xyz))
+
+    def __getattr__(self, 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]
+```
+
+
+```
+v = Vector((4, 5, 6))
+
+print('xyz: ', v.xyz)
+print('yz:  ', v.yz)
+print('zxy: ', v.xzy)
+print('y:   ', v.y)
+```
+
+
+## Other special methods
-- 
GitLab