From 5740668fd4464a326e36d5589e4a246294177954 Mon Sep 17 00:00:00 2001
From: Paul McCarthy <pauldmccarthy@gmail.com>
Date: Sat, 10 Feb 2018 14:27:23 +0000
Subject: [PATCH] Now it's almost finished I think.

---
 .../object_oriented_programming.ipynb         | 205 ++++++++++--------
 .../object_oriented_programming.md            | 204 +++++++++--------
 2 files changed, 228 insertions(+), 181 deletions(-)

diff --git a/advanced_topics/object_oriented_programming.ipynb b/advanced_topics/object_oriented_programming.ipynb
index c01ed81..deca29f 100644
--- a/advanced_topics/object_oriented_programming.ipynb
+++ b/advanced_topics/object_oriented_programming.ipynb
@@ -19,22 +19,22 @@
     "\n",
     "\n",
     "* [Objects versus classes](#objects-versus-classes)\n",
-    " * [Defining a class](#defining-a-class)\n",
-    " * [Object creation - the `__init__` method](#object-creation-the-init-method)\n",
-    "  * [Our method is called `__init__`, but we didn't actually call the `__init__` method!](#our-method-is-called-init)\n",
-    "  * [We didn't specify the `self` argument - what gives?!?](#we-didnt-specify-the-self-argument)\n",
-    " * [Attributes](#attributes)\n",
-    " * [Methods](#methods)\n",
-    " * [Protecting attribute access](#protecting-attribute-access)\n",
-    "  * [A better way - properties](#a-better-way-properties])\n",
-    " * [Inheritance](#inheritance)\n",
-    "  * [The basics](#the-basics)\n",
-    "  * [Code re-use and problem decomposition](#code-re-use-and-problem-decomposition)\n",
-    "  * [Polymorphism](#polymorphism)\n",
-    "  * [Multiple inheritance](#multiple-inheritance)\n",
-    " * [Class attributes and methods](#class-attributes-and-methods)\n",
-    "  * [Class attributes](#class-attributes)\n",
-    "  * [Class methods](#class-methods)\n",
+    "* [Defining a class](#defining-a-class)\n",
+    "* [Object creation - the `__init__` method](#object-creation-the-init-method)\n",
+    " * [Our method is called `__init__`, but we didn't actually call the `__init__` method!](#our-method-is-called-init)\n",
+    " * [We didn't specify the `self` argument - what gives?!?](#we-didnt-specify-the-self-argument)\n",
+    "* [Attributes](#attributes)\n",
+    "* [Methods](#methods)\n",
+    "* [Protecting attribute access](#protecting-attribute-access)\n",
+    " * [A better way - properties](#a-better-way-properties])\n",
+    "* [Inheritance](#inheritance)\n",
+    " * [The basics](#the-basics)\n",
+    " * [Code re-use and problem decomposition](#code-re-use-and-problem-decomposition)\n",
+    " * [Polymorphism](#polymorphism)\n",
+    " * [Multiple inheritance](#multiple-inheritance)\n",
+    "* [Class attributes and methods](#class-attributes-and-methods)\n",
+    " * [Class attributes](#class-attributes)\n",
+    " * [Class methods](#class-methods)\n",
     "* [Appendix: The `object` base-class](#appendix-the-object-base-class)\n",
     "* [Appendix: `__init__` versus `__new__`](#appendix-init-versus-new)\n",
     "* [Appendix: Monkey-patching](#appendix-monkey-patching)\n",
@@ -130,8 +130,8 @@
    "metadata": {},
    "source": [
     "In this statement, we defined a new class called `FSLMaths`, which inherits\n",
-    "from the built-in `object` base-class (see [below](todo) for more details on\n",
-    "inheritance).\n",
+    "from the built-in `object` base-class (see [below](inheritance) for more\n",
+    "details on inheritance).\n",
     "\n",
     "\n",
     "Now that we have defined our class, we can create objects - instances of that\n",
@@ -219,7 +219,7 @@
     "\n",
     "\n",
     "There are a number of \"special\" methods that you can add to a class in Python\n",
-    "to control various aspects of how instances of the class behave.  One of the\n",
+    "to customise various aspects of how instances of the class behave.  One of the\n",
     "first ones you may come across is the `__str__` method, which defines how an\n",
     "object should be printed (more specifically, how an object gets converted into\n",
     "a string). For example, we could add a `__str__` method to our `FSLMaths`\n",
@@ -255,7 +255,7 @@
     "docs](https://docs.python.org/3.5/reference/datamodel.html#special-method-names)\n",
     "for details on all of the special methods that can be defined in a class. And\n",
     "take a look at the appendix for some more details on [how Python objects get\n",
-    "created](todo).\n",
+    "created](appendix-init-versus-new).\n",
     "\n",
     "\n",
     "<a class=\"anchor\" id=\"we-didnt-specify-the-self-argument\"></a>\n",
@@ -291,7 +291,7 @@
     "\n",
     "But note that you __do not__ need to explicitly provide the `self` argument\n",
     "when you call a method on an object, or when you create a new object. The\n",
-    "Python runtime will take care of passing the instance to its method, as as the\n",
+    "Python runtime will take care of passing the instance to its method, as the\n",
     "first argument to the method.\n",
     "\n",
     "\n",
@@ -422,10 +422,9 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "Woah woah, [slow down egg-head, you're going a mile a\n",
-    "minute!](https://www.youtube.com/watch?v=yz-TemWooa4)  We've modified\n",
-    "`__init__` so that a second attribute called `operations` is added to our\n",
-    "object - this `operations` attribute is simply a list.\n",
+    "Woah woah, [slow down egg-head!](https://www.youtube.com/watch?v=yz-TemWooa4)\n",
+    "We've modified `__init__` so that a second attribute called `operations` is\n",
+    "added to our object - this `operations` attribute is simply a list.\n",
     "\n",
     "\n",
     "Then, we added a handful of methods - `add`, `mul`, and `div` - which each\n",
@@ -473,13 +472,12 @@
     "\n",
     "        for oper, value in self.operations:\n",
     "\n",
-    "            # Values could be an image that\n",
-    "            # has already been loaded.\n",
+    "            # Value could be an image.\n",
+    "            # If not, we assume that\n",
+    "            # it is a scalar/numpy array.\n",
     "            if isinstance(value, nib.nifti1.Nifti1Image):\n",
     "                value = value.get_data()\n",
     "\n",
-    "            # Otherwise we assume that\n",
-    "            # values are scalars.\n",
     "\n",
     "            if oper == 'add':\n",
     "                data = data + value\n",
@@ -597,8 +595,9 @@
     "this. Think of the poor souls who inherit your code years after you have left\n",
     "the lab - if you go around overwriting all of the methods and attributes of\n",
     "your objects, they are not going to have a hope in hell of understanding what\n",
-    "your code is actually doing. Take a look at the appendix for a [brief\n",
-    "discussion on this topic](todo).\n",
+    "your code is actually doing, and they are not going to like you very\n",
+    "much. Take a look at the appendix for a [brief discussion on this\n",
+    "topic](appendix-monkey-patching).\n",
     "\n",
     "\n",
     "Python tends to assume that programmers are \"responsible adults\", and hence\n",
@@ -621,7 +620,7 @@
     "* Class-level attributes and methods which begin with a double-underscore\n",
     "  (`__`) should be considered __private__. Python provides a weak form of\n",
     "  enforcement for this rule - any attribute or method with such a name will\n",
-    "  actually be __renamed_ (in a standardised manner) at runtime, so that it is\n",
+    "  actually be _renamed_ (in a standardised manner) at runtime, so that it is\n",
     "  not accessible through its original name (it is still accessible via its\n",
     "  [mangled\n",
     "  name](https://docs.python.org/3.5/tutorial/classes.html#private-variables)\n",
@@ -705,7 +704,7 @@
    "metadata": {},
    "source": [
     "So we are still storing our input image as a private attribute, but now we\n",
-    "have made it available in a read-only manner via the `img` property:"
+    "have made it available in a read-only manner via the public `img` property:"
    ]
   },
   {
@@ -748,7 +747,7 @@
     "\n",
     "    @property\n",
     "    def img(self):\n",
-    "        return self.__input\n",
+    "        return self.__img\n",
     "\n",
     "    @img.setter\n",
     "    def img(self, value):\n",
@@ -761,9 +760,13 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "Property setters are a nice way to add validation logic when an attribute is\n",
-    "assigned a value. We are doing this in the above example, by making sure that\n",
-    "the new input is a NIFTI image:"
+    "> Note that we used the `img` setter method within `__init__` to validate the\n",
+    "> initial `inimg` that was passed in during creation.\n",
+    "\n",
+    "\n",
+    "Property setters are a nice way to add validation logic for when an attribute\n",
+    "is assigned a value. In this example, an error will be raised if the new input\n",
+    "is not a NIFTI image."
    ]
   },
   {
@@ -786,7 +789,7 @@
     "\n",
     "print('New input: ', fm.img.get_filename())\n",
     "\n",
-    "# this is going to explode\n",
+    "print('This is going to explode')\n",
     "fm.img = 'abcde'"
    ]
   },
@@ -794,11 +797,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "> Note also that we used the `img` setter method within `__init__` to\n",
-    "> validate the initial `inimg` that was passed in during creation.\n",
-    "\n",
-    "\n",
-    "<a class=\"anchor\" id=\"inheritance\"></>a\n",
+    "<a class=\"anchor\" id=\"inheritance\"></a>\n",
     "## Inheritance\n",
     "\n",
     "\n",
@@ -811,9 +810,9 @@
     "### The basics\n",
     "\n",
     "\n",
-    "For example, a veterinary surgery might be running some Python code which\n",
-    "looks like the following, to assist the nurses in identifying an animal when\n",
-    "it arrives at the surgery:"
+    "My local veterinary surgery runs some Python code which looks like the\n",
+    "following, to assist the nurses in identifying an animal when it arrives at\n",
+    "the surgery:"
    ]
   },
   {
@@ -824,12 +823,17 @@
    "source": [
     "class Animal(object):\n",
     "    def noiseMade(self):\n",
-    "        raise NotImplementedError('This method is implemented by sub-classes')\n",
+    "        raise NotImplementedError('This method must be '\n",
+    "                                  'implemented by sub-classes')\n",
     "\n",
     "class Dog(Animal):\n",
     "    def noiseMade(self):\n",
     "        return 'Woof'\n",
     "\n",
+    "class TalkingDog(Dog):\n",
+    "    def noiseMade(self):\n",
+    "        return 'Hi Homer, find your soulmate!'\n",
+    "\n",
     "class Cat(Animal):\n",
     "    def noiseMade(self):\n",
     "        return 'Meow'\n",
@@ -858,9 +862,11 @@
     "\n",
     "What does that `noiseMade` method do?  There is a `noiseMade` method defined\n",
     "on the `Animal` class, but it has been re-implemented, or _overridden_ in the\n",
-    "`Dog`, `Cat`, and `Chihuahua` classes (but not on the `Labrador` class).  We\n",
-    "can call the `noiseMade` method on any `Animal` instance, but the specific\n",
-    "behaviour that we get is dependent on the specific type of animal."
+    "`Dog`,\n",
+    "[`TalkingDog`](https://twitter.com/simpsonsqotd/status/427941665836630016?lang=en),\n",
+    "`Cat`, and `Chihuahua` classes (but not on the `Labrador` class).  We can call\n",
+    "the `noiseMade` method on any `Animal` instance, but the specific behaviour\n",
+    "that we get is dependent on the specific type of animal."
    ]
   },
   {
@@ -907,7 +913,7 @@
     "class Operator(object):\n",
     "\n",
     "    def __init__(self):\n",
-    "        super().__init__()\n",
+    "        super().__init__() # this line will be explained later\n",
     "        self.__operations = []\n",
     "        self.__opFuncs    = {}\n",
     "\n",
@@ -1014,7 +1020,7 @@
     "In Python, we can use the [built-in `super`\n",
     "method](https://docs.python.org/3.5/library/functions.html#super) to take care\n",
     "of correctly calling methods that are defined in an object's base-class (or\n",
-    "classes, in the case of [multiple inheritance](todo)).\n",
+    "classes, in the case of [multiple inheritance](multiple-inheritance)).\n",
     "\n",
     "\n",
     "> The `super` function is one thing which changed between Python 2 and 3 -\n",
@@ -1045,12 +1051,15 @@
     "\n",
     "The `NumberOperator` class has also overridden the `preprocess` method, to\n",
     "ensure that all values handled by the `Operator` are numbers. This method gets\n",
-    "called within the `run` method - for a `NumberOperator` instance, the\n",
+    "called within the `Operator.run` method - for a `NumberOperator` instance, the\n",
     "`NumberOperator.preprocess` method will get called<sup>1</sup>.\n",
     "\n",
-    "> <sup>1</sup> It is possible to [access overridden base-class methods](todo\n",
-    "> link) via the `super()` function, or by explicitly calling the base-class\n",
-    "> implementation.\n",
+    "\n",
+    "> <sup>1</sup> When a sub-class overrides a base-class method, it is still\n",
+    "> possible to access the base-class implementation [via the `super()`\n",
+    "> function](https://stackoverflow.com/a/4747427) (the preferred method), or by\n",
+    "> [explicitly calling the base-class\n",
+    "> implementation](https://stackoverflow.com/a/2421325).\n",
     "\n",
     "\n",
     "Now let's see what our `NumberOperator` class does:"
@@ -1132,8 +1141,8 @@
     "object without having complete knowledge about the class, or type, of that\n",
     "object. For example, we should be able to write a function which expects an\n",
     "`Operator` instance, but which will work on an instance of any `Operator`\n",
-    "sub-classs. For example, we could write a function which prints a summary of\n",
-    "an `Operator` instance:"
+    "sub-classs. As an example, let's write a function which prints a summary of an\n",
+    "`Operator` instance:"
    ]
   },
   {
@@ -1159,7 +1168,7 @@
    "source": [
     "Because the `operatorSummary` function only uses methods that are defined\n",
     "in the `Operator` base-class, we can use it on _any_ `Operator` instance,\n",
-    "regardless of its type:"
+    "regardless of its specific type:"
    ]
   },
   {
@@ -1184,8 +1193,8 @@
     "known as _multiple inheritance_. For example, we might want to build a\n",
     "notification mechanisim into our `StringOperator` class, so that listeners can\n",
     "be notified whenever the `capitalise` method gets called. It so happens that\n",
-    "we already have a `Notifier` class which allows listeners to register to be\n",
-    "notified when an event occurs:"
+    "our old colleague of `Operator` class fame also wrote a `Notifier` class which\n",
+    "allows listeners to register to be notified when an event occurs:"
    ]
   },
   {
@@ -1283,11 +1292,11 @@
     "[here](http://python-history.blogspot.co.uk/2010/06/method-resolution-order.html).\n",
     "\n",
     "\n",
-    "Note also that in for base class `__init__` methods to work in a design which\n",
-    "uses multiple inheritance, _all_ classes in the hierarchy must invoke\n",
-    "`super().__init__()`. This can become complicated when some base classes\n",
-    "expect to be passed arguments to their `__init__` method. In scenarios like\n",
-    "this it may be prefereable to manually invoke the base class `__init__`\n",
+    "Note also that for base class `__init__` methods to be correctly called in a\n",
+    "design which uses multiple inheritance, _all_ classes in the hierarchy must\n",
+    "invoke `super().__init__()`. This can become complicated when some base\n",
+    "classes expect to be passed arguments to their `__init__` method. In scenarios\n",
+    "like this it may be prefereable to manually invoke the base class `__init__`\n",
     "methods instead of using `super()`. For example:\n",
     "\n",
     "\n",
@@ -1301,7 +1310,8 @@
     "\n",
     "This approach has the disadvantage that if the base classes change, you will\n",
     "have to change these invocations. But the advantage is that you know exactly\n",
-    "how the class hierarchy will be initialised.\n",
+    "how the class hierarchy will be initialised. In general though, doing\n",
+    "everything with `super()` will result in more maintainable code.\n",
     "\n",
     "\n",
     "<a class=\"anchor\" id=\"class-attributes-and-methods\"></a>\n",
@@ -1315,7 +1325,7 @@
     "\n",
     "Class attributes and methods can be accessed without having to create an\n",
     "instance of the class - they are not associated with individual objects, but\n",
-    "rather to the class itself.\n",
+    "rather with the class itself.\n",
     "\n",
     "\n",
     "Class methods and attributes can be useful in several scenarios - as a\n",
@@ -1330,9 +1340,9 @@
     "### Class attributes\n",
     "\n",
     "\n",
-    "Let's add a `dict` as a class attribute to the `FSLMaths` class - this `dict`\n",
-    "called on a `FSLMaths` object, that object will increment the class-level\n",
-    "counters for each operation that is applied:"
+    "Let's add a `dict` called `opCounters` as a class attribute to the `FSLMaths`\n",
+    "class - whenever an operation is called on a `FSLMaths` instance, the counter\n",
+    "for that operation will be incremented:"
    ]
   },
   {
@@ -1374,7 +1384,10 @@
     "            # Code omitted for brevity\n",
     "\n",
     "            # Increment the usage counter\n",
-    "            # for this operation.\n",
+    "            # for this operation. We can\n",
+    "            # access class attributes (and\n",
+    "            # methods) through the class\n",
+    "            # itself.\n",
     "            FSLMaths.opCounters[oper] = self.opCounters.get(oper, 0) + 1"
    ]
   },
@@ -1425,10 +1438,12 @@
     "from above, and add it as a method to the `FSLMaths` class.\n",
     "\n",
     "\n",
-    "A class method is denoted by the `@classmethod` decorator. Note that, where a\n",
-    "regular method which is called on an instance will be passed the instance as\n",
-    "its first argument ('self'), a class method will be passed the class itself as\n",
-    "the first argument - the standard convention is to call this argument 'cls':"
+    "A class method is denoted by the\n",
+    "[`@classmethod`](https://docs.python.org/3.5/library/functions.html#classmethod)\n",
+    "decorator. Note that, where a regular method which is called on an instance\n",
+    "will be passed the instance as its first argument (`self`), a class method\n",
+    "will be passed the class itself as the first argument - the standard\n",
+    "convention is to call this argument `cls`:"
    ]
   },
   {
@@ -1465,11 +1480,6 @@
     "        data = np.array(self.img.get_data())\n",
     "\n",
     "        for oper, value in self.operations:\n",
-    "\n",
-    "            # Code omitted for brevity\n",
-    "\n",
-    "            # Increment the usage counter\n",
-    "            # for this operation.\n",
     "            FSLMaths.opCounters[oper] = self.opCounters.get(oper, 0) + 1"
    ]
   },
@@ -1477,7 +1487,14 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "Calling a class method is the same as accessing a class attribute:"
+    "> There is another decorator -\n",
+    "> [`@staticmethod`](https://docs.python.org/3.5/library/functions.html#staticmethod) -\n",
+    "> which can be used on methods defined within a class. The difference\n",
+    "> between a `@classmethod` and a `@staticmethod` is that the latter will _not_\n",
+    "> be passed the class (`cls`).\n",
+    "\n",
+    "\n",
+    "calling a class method is the same as accessing a class attribute:"
    ]
   },
   {
@@ -1521,7 +1538,7 @@
    "outputs": [],
    "source": [
     "print(fm1.opCounters)\n",
-    "print(fm1.usage())"
+    "fm1.usage()"
    ]
   },
   {
@@ -1585,13 +1602,13 @@
     "docs](https://docs.python.org/3.5/reference/datamodel.html#basic-customization).\n",
     "\n",
     "\n",
-    "<a class=\"anchor\" id=\"appendix-monkey-patching\"></>a\n",
+    "<a class=\"anchor\" id=\"appendix-monkey-patching\"></a>\n",
     "## Appendix: Monkey-patching\n",
     "\n",
     "\n",
     "The act of run-time modification of objects or class definitions is referred\n",
     "to as [_monkey-patching_](https://en.wikipedia.org/wiki/Monkey_patch) and,\n",
-    "while it is allowed by the Python programming language, it is generally\n",
+    "whilst it is allowed by the Python programming language, it is generally\n",
     "considered quite bad practice.\n",
     "\n",
     "\n",
@@ -1621,12 +1638,15 @@
     "## Appendix: Method overloading\n",
     "\n",
     "\n",
-    "Method overloading (defining multiple methods on a class, each accepting\n",
-    "different arguments) is one of the only object-oriented features that is not\n",
-    "present in Python. Becuase Python does not perform any runtime checks on the\n",
-    "types of arguments that are passed to a method, or the compatibility of the\n",
-    "method to accept the arguments, it would not be possible to determine which\n",
-    "implementation of a method is to be called.\n",
+    "Method overloading (defining multiple methods with the same name in a class,\n",
+    "but each accepting different arguments) is one of the only object-oriented\n",
+    "features that is not present in Python. Becuase Python does not perform any\n",
+    "runtime checks on the types of arguments that are passed to a method, or the\n",
+    "compatibility of the method to accept the arguments, it would not be possible\n",
+    "to determine which implementation of a method is to be called. In other words,\n",
+    "in Python only the name of a method is used to identify that method, unlike in\n",
+    "C++ and Java, where the full method signature (name, input types and return\n",
+    "types) is used.\n",
     "\n",
     "\n",
     "However, because a Python method can be written to accept any number or type\n",
@@ -1646,6 +1666,9 @@
     "        if   len(args) == 2: return self.__add2(*args)\n",
     "        elif len(args) == 3: return self.__add3(*args)\n",
     "        elif len(args) == 4: return self.__add4(*args)\n",
+    "        else:\n",
+    "            raise AttributeError('No method available to accept {} '\n",
+    "                                 'arguments'.format(len(args)))\n",
     "\n",
     "    def __add2(self, a, b):\n",
     "        return a + b\n",
diff --git a/advanced_topics/object_oriented_programming.md b/advanced_topics/object_oriented_programming.md
index 891a208..59aa3e9 100644
--- a/advanced_topics/object_oriented_programming.md
+++ b/advanced_topics/object_oriented_programming.md
@@ -13,22 +13,22 @@ you use an object-oriented approach.
 
 
 * [Objects versus classes](#objects-versus-classes)
- * [Defining a class](#defining-a-class)
- * [Object creation - the `__init__` method](#object-creation-the-init-method)
-  * [Our method is called `__init__`, but we didn't actually call the `__init__` method!](#our-method-is-called-init)
-  * [We didn't specify the `self` argument - what gives?!?](#we-didnt-specify-the-self-argument)
- * [Attributes](#attributes)
- * [Methods](#methods)
- * [Protecting attribute access](#protecting-attribute-access)
-  * [A better way - properties](#a-better-way-properties])
- * [Inheritance](#inheritance)
-  * [The basics](#the-basics)
-  * [Code re-use and problem decomposition](#code-re-use-and-problem-decomposition)
-  * [Polymorphism](#polymorphism)
-  * [Multiple inheritance](#multiple-inheritance)
- * [Class attributes and methods](#class-attributes-and-methods)
-  * [Class attributes](#class-attributes)
-  * [Class methods](#class-methods)
+* [Defining a class](#defining-a-class)
+* [Object creation - the `__init__` method](#object-creation-the-init-method)
+ * [Our method is called `__init__`, but we didn't actually call the `__init__` method!](#our-method-is-called-init)
+ * [We didn't specify the `self` argument - what gives?!?](#we-didnt-specify-the-self-argument)
+* [Attributes](#attributes)
+* [Methods](#methods)
+* [Protecting attribute access](#protecting-attribute-access)
+ * [A better way - properties](#a-better-way-properties])
+* [Inheritance](#inheritance)
+ * [The basics](#the-basics)
+ * [Code re-use and problem decomposition](#code-re-use-and-problem-decomposition)
+ * [Polymorphism](#polymorphism)
+ * [Multiple inheritance](#multiple-inheritance)
+* [Class attributes and methods](#class-attributes-and-methods)
+ * [Class attributes](#class-attributes)
+ * [Class methods](#class-methods)
 * [Appendix: The `object` base-class](#appendix-the-object-base-class)
 * [Appendix: `__init__` versus `__new__`](#appendix-init-versus-new)
 * [Appendix: Monkey-patching](#appendix-monkey-patching)
@@ -116,8 +116,8 @@ class FSLMaths(object):
 
 
 In this statement, we defined a new class called `FSLMaths`, which inherits
-from the built-in `object` base-class (see [below](todo) for more details on
-inheritance).
+from the built-in `object` base-class (see [below](inheritance) for more
+details on inheritance).
 
 
 Now that we have defined our class, we can create objects - instances of that
@@ -181,7 +181,7 @@ calling the class in the same way that we call a function.
 
 
 There are a number of "special" methods that you can add to a class in Python
-to control various aspects of how instances of the class behave.  One of the
+to customise various aspects of how instances of the class behave.  One of the
 first ones you may come across is the `__str__` method, which defines how an
 object should be printed (more specifically, how an object gets converted into
 a string). For example, we could add a `__str__` method to our `FSLMaths`
@@ -209,7 +209,7 @@ Refer to the [official
 docs](https://docs.python.org/3.5/reference/datamodel.html#special-method-names)
 for details on all of the special methods that can be defined in a class. And
 take a look at the appendix for some more details on [how Python objects get
-created](todo).
+created](appendix-init-versus-new).
 
 
 <a class="anchor" id="we-didnt-specify-the-self-argument"></a>
@@ -237,7 +237,7 @@ that has been created (and is then assigned to the `fm` variable, after the
 
 But note that you __do not__ need to explicitly provide the `self` argument
 when you call a method on an object, or when you create a new object. The
-Python runtime will take care of passing the instance to its method, as as the
+Python runtime will take care of passing the instance to its method, as the
 first argument to the method.
 
 
@@ -336,10 +336,9 @@ class FSLMaths(object):
 ```
 
 
-Woah woah, [slow down egg-head, you're going a mile a
-minute!](https://www.youtube.com/watch?v=yz-TemWooa4)  We've modified
-`__init__` so that a second attribute called `operations` is added to our
-object - this `operations` attribute is simply a list.
+Woah woah, [slow down egg-head!](https://www.youtube.com/watch?v=yz-TemWooa4)
+We've modified `__init__` so that a second attribute called `operations` is
+added to our object - this `operations` attribute is simply a list.
 
 
 Then, we added a handful of methods - `add`, `mul`, and `div` - which each
@@ -382,13 +381,12 @@ class FSLMaths(object):
 
         for oper, value in self.operations:
 
-            # Values could be an image that
-            # has already been loaded.
+            # Value could be an image.
+            # If not, we assume that
+            # it is a scalar/numpy array.
             if isinstance(value, nib.nifti1.Nifti1Image):
                 value = value.get_data()
 
-            # Otherwise we assume that
-            # values are scalars.
 
             if oper == 'add':
                 data = data + value
@@ -479,8 +477,9 @@ But you really shouldn't get into the habit of doing devious things like
 this. Think of the poor souls who inherit your code years after you have left
 the lab - if you go around overwriting all of the methods and attributes of
 your objects, they are not going to have a hope in hell of understanding what
-your code is actually doing. Take a look at the appendix for a [brief
-discussion on this topic](todo).
+your code is actually doing, and they are not going to like you very
+much. Take a look at the appendix for a [brief discussion on this
+topic](appendix-monkey-patching).
 
 
 Python tends to assume that programmers are "responsible adults", and hence
@@ -503,7 +502,7 @@ to](https://docs.python.org/3.5/tutorial/classes.html#private-variables):
 * Class-level attributes and methods which begin with a double-underscore
   (`__`) should be considered __private__. Python provides a weak form of
   enforcement for this rule - any attribute or method with such a name will
-  actually be __renamed_ (in a standardised manner) at runtime, so that it is
+  actually be _renamed_ (in a standardised manner) at runtime, so that it is
   not accessible through its original name (it is still accessible via its
   [mangled
   name](https://docs.python.org/3.5/tutorial/classes.html#private-variables)
@@ -562,7 +561,7 @@ class FSLMaths(object):
 
 
 So we are still storing our input image as a private attribute, but now we
-have made it available in a read-only manner via the `img` property:
+have made it available in a read-only manner via the public `img` property:
 
 
 ```
@@ -592,7 +591,7 @@ class FSLMaths(object):
 
     @property
     def img(self):
-        return self.__input
+        return self.__img
 
     @img.setter
     def img(self, value):
@@ -602,9 +601,13 @@ class FSLMaths(object):
 ```
 
 
-Property setters are a nice way to add validation logic when an attribute is
-assigned a value. We are doing this in the above example, by making sure that
-the new input is a NIFTI image:
+> Note that we used the `img` setter method within `__init__` to validate the
+> initial `inimg` that was passed in during creation.
+
+
+Property setters are a nice way to add validation logic for when an attribute
+is assigned a value. In this example, an error will be raised if the new input
+is not a NIFTI image.
 
 
 ```
@@ -622,15 +625,12 @@ fm.img = inimg2
 
 print('New input: ', fm.img.get_filename())
 
-# this is going to explode
+print('This is going to explode')
 fm.img = 'abcde'
 ```
 
-> Note also that we used the `img` setter method within `__init__` to
-> validate the initial `inimg` that was passed in during creation.
-
 
-<a class="anchor" id="inheritance"></>a
+<a class="anchor" id="inheritance"></a>
 ## Inheritance
 
 
@@ -643,20 +643,25 @@ classes and instances.
 ### The basics
 
 
-For example, a veterinary surgery might be running some Python code which
-looks like the following, to assist the nurses in identifying an animal when
-it arrives at the surgery:
+My local veterinary surgery runs some Python code which looks like the
+following, to assist the nurses in identifying an animal when it arrives at
+the surgery:
 
 
 ```
 class Animal(object):
     def noiseMade(self):
-        raise NotImplementedError('This method is implemented by sub-classes')
+        raise NotImplementedError('This method must be '
+                                  'implemented by sub-classes')
 
 class Dog(Animal):
     def noiseMade(self):
         return 'Woof'
 
+class TalkingDog(Dog):
+    def noiseMade(self):
+        return 'Hi Homer, find your soulmate!'
+
 class Cat(Animal):
     def noiseMade(self):
         return 'Meow'
@@ -682,9 +687,11 @@ are also `Animal` instances (but not vice-versa).
 
 What does that `noiseMade` method do?  There is a `noiseMade` method defined
 on the `Animal` class, but it has been re-implemented, or _overridden_ in the
-`Dog`, `Cat`, and `Chihuahua` classes (but not on the `Labrador` class).  We
-can call the `noiseMade` method on any `Animal` instance, but the specific
-behaviour that we get is dependent on the specific type of animal.
+`Dog`,
+[`TalkingDog`](https://twitter.com/simpsonsqotd/status/427941665836630016?lang=en),
+`Cat`, and `Chihuahua` classes (but not on the `Labrador` class).  We can call
+the `noiseMade` method on any `Animal` instance, but the specific behaviour
+that we get is dependent on the specific type of animal.
 
 
 ```
@@ -718,7 +725,7 @@ example.  Imagine that a former colleague had written a class called
 class Operator(object):
 
     def __init__(self):
-        super().__init__()
+        super().__init__() # this line will be explained later
         self.__operations = []
         self.__opFuncs    = {}
 
@@ -814,7 +821,7 @@ This line invokes `Operator.__init__` - the initialisation method for the
 In Python, we can use the [built-in `super`
 method](https://docs.python.org/3.5/library/functions.html#super) to take care
 of correctly calling methods that are defined in an object's base-class (or
-classes, in the case of [multiple inheritance](todo)).
+classes, in the case of [multiple inheritance](multiple-inheritance)).
 
 
 > The `super` function is one thing which changed between Python 2 and 3 -
@@ -845,12 +852,15 @@ Here we are registering all of the functionality that is provided by the
 
 The `NumberOperator` class has also overridden the `preprocess` method, to
 ensure that all values handled by the `Operator` are numbers. This method gets
-called within the `run` method - for a `NumberOperator` instance, the
+called within the `Operator.run` method - for a `NumberOperator` instance, the
 `NumberOperator.preprocess` method will get called<sup>1</sup>.
 
-> <sup>1</sup> It is possible to [access overridden base-class methods](todo
-> link) via the `super()` function, or by explicitly calling the base-class
-> implementation.
+
+> <sup>1</sup> When a sub-class overrides a base-class method, it is still
+> possible to access the base-class implementation [via the `super()`
+> function](https://stackoverflow.com/a/4747427) (the preferred method), or by
+> [explicitly calling the base-class
+> implementation](https://stackoverflow.com/a/2421325).
 
 
 Now let's see what our `NumberOperator` class does:
@@ -915,8 +925,8 @@ to idea that, in an object-oriented language, we should be able to use an
 object without having complete knowledge about the class, or type, of that
 object. For example, we should be able to write a function which expects an
 `Operator` instance, but which will work on an instance of any `Operator`
-sub-classs. For example, we could write a function which prints a summary of
-an `Operator` instance:
+sub-classs. As an example, let's write a function which prints a summary of an
+`Operator` instance:
 
 
 ```
@@ -934,7 +944,7 @@ def operatorSummary(o):
 
 Because the `operatorSummary` function only uses methods that are defined
 in the `Operator` base-class, we can use it on _any_ `Operator` instance,
-regardless of its type:
+regardless of its specific type:
 
 
 ```
@@ -951,8 +961,8 @@ Python allows you to define a class which has multiple base classes - this is
 known as _multiple inheritance_. For example, we might want to build a
 notification mechanisim into our `StringOperator` class, so that listeners can
 be notified whenever the `capitalise` method gets called. It so happens that
-we already have a `Notifier` class which allows listeners to register to be
-notified when an event occurs:
+our old colleague of `Operator` class fame also wrote a `Notifier` class which
+allows listeners to register to be notified when an event occurs:
 
 
 ```
@@ -1025,11 +1035,11 @@ summary
 [here](http://python-history.blogspot.co.uk/2010/06/method-resolution-order.html).
 
 
-Note also that in for base class `__init__` methods to work in a design which
-uses multiple inheritance, _all_ classes in the hierarchy must invoke
-`super().__init__()`. This can become complicated when some base classes
-expect to be passed arguments to their `__init__` method. In scenarios like
-this it may be prefereable to manually invoke the base class `__init__`
+Note also that for base class `__init__` methods to be correctly called in a
+design which uses multiple inheritance, _all_ classes in the hierarchy must
+invoke `super().__init__()`. This can become complicated when some base
+classes expect to be passed arguments to their `__init__` method. In scenarios
+like this it may be prefereable to manually invoke the base class `__init__`
 methods instead of using `super()`. For example:
 
 
@@ -1043,7 +1053,8 @@ methods instead of using `super()`. For example:
 
 This approach has the disadvantage that if the base classes change, you will
 have to change these invocations. But the advantage is that you know exactly
-how the class hierarchy will be initialised.
+how the class hierarchy will be initialised. In general though, doing
+everything with `super()` will result in more maintainable code.
 
 
 <a class="anchor" id="class-attributes-and-methods"></a>
@@ -1057,7 +1068,7 @@ _object_. But it is also possible to add methods and attributes to a _class_
 
 Class attributes and methods can be accessed without having to create an
 instance of the class - they are not associated with individual objects, but
-rather to the class itself.
+rather with the class itself.
 
 
 Class methods and attributes can be useful in several scenarios - as a
@@ -1072,9 +1083,9 @@ performance of the `add` operation.
 ### Class attributes
 
 
-Let's add a `dict` as a class attribute to the `FSLMaths` class - this `dict`
-called on a `FSLMaths` object, that object will increment the class-level
-counters for each operation that is applied:
+Let's add a `dict` called `opCounters` as a class attribute to the `FSLMaths`
+class - whenever an operation is called on a `FSLMaths` instance, the counter
+for that operation will be incremented:
 
 
 ```
@@ -1111,7 +1122,10 @@ class FSLMaths(object):
             # Code omitted for brevity
 
             # Increment the usage counter
-            # for this operation.
+            # for this operation. We can
+            # access class attributes (and
+            # methods) through the class
+            # itself.
             FSLMaths.opCounters[oper] = self.opCounters.get(oper, 0) + 1
 ```
 
@@ -1151,10 +1165,12 @@ It is just as easy to add a method to a class - let's take our reporting code
 from above, and add it as a method to the `FSLMaths` class.
 
 
-A class method is denoted by the `@classmethod` decorator. Note that, where a
-regular method which is called on an instance will be passed the instance as
-its first argument ('self'), a class method will be passed the class itself as
-the first argument - the standard convention is to call this argument 'cls':
+A class method is denoted by the
+[`@classmethod`](https://docs.python.org/3.5/library/functions.html#classmethod)
+decorator. Note that, where a regular method which is called on an instance
+will be passed the instance as its first argument (`self`), a class method
+will be passed the class itself as the first argument - the standard
+convention is to call this argument `cls`:
 
 
 ```
@@ -1186,16 +1202,18 @@ class FSLMaths(object):
         data = np.array(self.img.get_data())
 
         for oper, value in self.operations:
-
-            # Code omitted for brevity
-
-            # Increment the usage counter
-            # for this operation.
             FSLMaths.opCounters[oper] = self.opCounters.get(oper, 0) + 1
 ```
 
 
-Calling a class method is the same as accessing a class attribute:
+> There is another decorator -
+> [`@staticmethod`](https://docs.python.org/3.5/library/functions.html#staticmethod) -
+> which can be used on methods defined within a class. The difference
+> between a `@classmethod` and a `@staticmethod` is that the latter will _not_
+> be passed the class (`cls`).
+
+
+calling a class method is the same as accessing a class attribute:
 
 
 ```
@@ -1226,7 +1244,7 @@ instances:
 
 ```
 print(fm1.opCounters)
-print(fm1.usage())
+fm1.usage()
 ```
 
 
@@ -1287,13 +1305,13 @@ and you may also wish to take a look at the [official Python
 docs](https://docs.python.org/3.5/reference/datamodel.html#basic-customization).
 
 
-<a class="anchor" id="appendix-monkey-patching"></>a
+<a class="anchor" id="appendix-monkey-patching"></a>
 ## Appendix: Monkey-patching
 
 
 The act of run-time modification of objects or class definitions is referred
 to as [_monkey-patching_](https://en.wikipedia.org/wiki/Monkey_patch) and,
-while it is allowed by the Python programming language, it is generally
+whilst it is allowed by the Python programming language, it is generally
 considered quite bad practice.
 
 
@@ -1323,12 +1341,15 @@ in!](https://git.fmrib.ox.ac.uk/fsl/fsleyes/fsleyes/blob/0.21.0/fsleyes/views/vi
 ## Appendix: Method overloading
 
 
-Method overloading (defining multiple methods on a class, each accepting
-different arguments) is one of the only object-oriented features that is not
-present in Python. Becuase Python does not perform any runtime checks on the
-types of arguments that are passed to a method, or the compatibility of the
-method to accept the arguments, it would not be possible to determine which
-implementation of a method is to be called.
+Method overloading (defining multiple methods with the same name in a class,
+but each accepting different arguments) is one of the only object-oriented
+features that is not present in Python. Becuase Python does not perform any
+runtime checks on the types of arguments that are passed to a method, or the
+compatibility of the method to accept the arguments, it would not be possible
+to determine which implementation of a method is to be called. In other words,
+in Python only the name of a method is used to identify that method, unlike in
+C++ and Java, where the full method signature (name, input types and return
+types) is used.
 
 
 However, because a Python method can be written to accept any number or type
@@ -1343,6 +1364,9 @@ class Adder(object):
         if   len(args) == 2: return self.__add2(*args)
         elif len(args) == 3: return self.__add3(*args)
         elif len(args) == 4: return self.__add4(*args)
+        else:
+            raise AttributeError('No method available to accept {} '
+                                 'arguments'.format(len(args)))
 
     def __add2(self, a, b):
         return a + b
-- 
GitLab