diff --git a/advanced_topics/object_oriented_programming.ipynb b/advanced_topics/object_oriented_programming.ipynb index 38938351a0038bbfe30020abbe041435093e5294..c01ed812094cf35554bbbdbb71c72ba91ecfaf6a 100644 --- a/advanced_topics/object_oriented_programming.ipynb +++ b/advanced_topics/object_oriented_programming.ipynb @@ -18,6 +18,31 @@ "you use an object-oriented approach.\n", "\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", + "* [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", + "* [Appendix: Method overloading](#appendix-method-overloading)\n", + "* [Useful references](#useful-references)\n", + "\n", + "\n", + "<a class=\"anchor\" id=\"objects-versus-classes\"></a>\n", "## Objects versus classes\n", "\n", "\n", @@ -27,8 +52,8 @@ "\n", "\n", "If you have not done any object-oriented programming before, your first step\n", - "is to understand the difference between _objects_ (also known as _instances_)\n", - "and _classes_ (also known as _types_).\n", + "is to understand the difference between _objects_ (also known as\n", + "_instances_) and _classes_ (also known as _types_).\n", "\n", "\n", "If you have some experience in C, then you can start off by thinking of a\n", @@ -82,6 +107,7 @@ "> object - even classes!\n", "\n", "\n", + "<a class=\"anchor\" id=\"defining-a-class\"></a>\n", "## Defining a class\n", "\n", "\n", @@ -132,6 +158,7 @@ "work.\n", "\n", "\n", + "<a class=\"anchor\" id=\"object-creation-the-init-method\"></a>\n", "## Object creation - the `__init__` method\n", "\n", "\n", @@ -147,7 +174,7 @@ "source": [ "class FSLMaths(object):\n", " def __init__(self, inimg):\n", - " self.input = inimg" + " self.img = inimg" ] }, { @@ -155,7 +182,7 @@ "metadata": {}, "source": [ "Here we have added a _method_ called `__init__` to our class (remember that a\n", - "_method_ is just a function which is defined in a cliass, and which can be\n", + "_method_ is just a function which is defined in a class, and which can be\n", "called on instances of that class). This method expects two arguments -\n", "`self`, and `inimg`. So now, when we create an instance of the `FSLMaths`\n", "class, we will need to provide an input image:" @@ -170,8 +197,8 @@ "import nibabel as nib\n", "import os.path as op\n", "\n", - "input = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')\n", - "inimg = nib.load(input)\n", + "fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')\n", + "inimg = nib.load(fpath)\n", "fm = FSLMaths(inimg)" ] }, @@ -179,22 +206,65 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "There are a couple of things to note here:\n", + "There are a couple of things to note here...\n", "\n", "\n", - "__Our method is called__ `__init__`__, but we didn't actually call the__\n", - "`__init__` __method!__ `__init__` is a special method in Python - it is called\n", - "when an instance of a class is created. And recall that we can create an\n", - "instance of a class by calling the class in the same way that we call a\n", - "function.\n", + "<a class=\"anchor\" id=\"our-method-is-called-init\"></a>\n", + "### Our method is called `__init__`, but we didn't actually call the `__init__` method!\n", "\n", "\n", - "__We didn't specify the `self` argument - what gives?!?__ The `self` argument\n", - "is a special argument for methods in Python. If you are coming from C++, Java,\n", - "C# or similar, `self` in Python is equivalent to `this` in those languages.\n", + "`__init__` is a special method in Python - it is called when an instance of a\n", + "class is created. And recall that we can create an instance of a class by\n", + "calling the class in the same way that we call a function.\n", "\n", "\n", - "### The `self` argument\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", + "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", + "class like so:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class FSLMaths(object):\n", + "\n", + " def __init__(self, inimg):\n", + " self.img = inimg\n", + "\n", + " def __str__(self):\n", + " return 'FSLMaths({})'.format(self.img.get_filename())\n", + "\n", + "fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')\n", + "inimg = nib.load(fpath)\n", + "fm = FSLMaths(inimg)\n", + "\n", + "print(fm)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Refer to the [official\n", + "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", + "\n", + "\n", + "<a class=\"anchor\" id=\"we-didnt-specify-the-self-argument\"></a>\n", + "### We didn't specify the `self` argument - what gives?!?\n", + "\n", + "\n", + "The `self` argument is a special argument for methods in Python. If you are\n", + "coming from C++, Java, C# or similar, `self` in Python is equivalent to `this`\n", + "in those languages.\n", "\n", "\n", "In a method, the `self` argument is a reference to the object that the method\n", @@ -219,20 +289,21 @@ "`__init__` method has finished).\n", "\n", "\n", - "But note that we do not need to explicitly provide the `self` argument - when\n", - "you call a method on an object, or when you create a new object, the Python\n", - "runtime will take care of passing the instance as the `self` argument to the\n", - "method.\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", + "first argument to the method.\n", "\n", "\n", - "But when you are writing a class, you _do_ need to explicitly list `self` as\n", + "But when you are writing a class, you __do__ need to explicitly list `self` as\n", "the first argument to all of the methods of the class.\n", "\n", "\n", + "<a class=\"anchor\" id=\"attributes\"></a>\n", "## Attributes\n", "\n", "\n", - "In Python, the term _attribute_ is used to refer to a piece of information\n", + "In Python, the term __attribute__ is used to refer to a piece of information\n", "that is associated with an object. An attribute is generally a reference to\n", "another object (which might be a string, a number, or a list, or some other\n", "more complicated object).\n", @@ -250,10 +321,10 @@ "source": [ "class FSLMaths(object):\n", " def __init__(self, inimg):\n", - " self.input = inimg\n", + " self.img = inimg\n", "\n", - "input = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')\n", - "fm = FSLMaths(nib.load(input))" + "fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')\n", + "fm = FSLMaths(nib.load(fpath))" ] }, { @@ -261,8 +332,8 @@ "metadata": {}, "source": [ "Take a look at what is going on in the `__init__` method - we take the `inimg`\n", - "argument, and create a reference to it called `self.input`. We have added an\n", - "_attribute_ to the `FSLMaths` instance, called `input`, and we can access that\n", + "argument, and create a reference to it called `self.img`. We have added an\n", + "_attribute_ to the `FSLMaths` instance, called `img`, and we can access that\n", "attribute like so:" ] }, @@ -272,7 +343,7 @@ "metadata": {}, "outputs": [], "source": [ - "print('Input for our FSLMaths instance: {}'.format(fm.input.get_filename()))" + "print('Input for our FSLMaths instance: {}'.format(fm.img.get_filename()))" ] }, { @@ -286,8 +357,8 @@ "might be quite jarring to you if you are coming from a language with more\n", "rigid semantics, such as C++ or Java. In those languages, you must pre-specify\n", "all of the attributes and methods that are a part of a class. But Python is\n", - "more flexible - you simply add attributes to an object affer it has been\n", - "created. In fact, you can even do this outside of the class\n", + "much more flexible - you can simply add attributes to an object after it has\n", + "been created. In fact, you can even do this outside of the class\n", "definition<sup>1</sup>:" ] }, @@ -306,7 +377,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "__But...__ while attributes can be added to a Python object at any time, it is\n", + "__But ...__ while attributes can be added to a Python object at any time, it is\n", "good practice (and makes for more readable and maintainable code) to add all\n", "of an object's attributes within the `__init__` method.\n", "\n", @@ -316,6 +387,7 @@ "> extensions (Python modules that are written in C).\n", "\n", "\n", + "<a class=\"anchor\" id=\"methods\"></a>\n", "## Methods\n", "\n", "\n", @@ -333,7 +405,7 @@ "class FSLMaths(object):\n", "\n", " def __init__(self, inimg):\n", - " self.input = inimg\n", + " self.img = inimg\n", " self.operations = []\n", "\n", " def add(self, value):\n", @@ -383,7 +455,7 @@ "class FSLMaths(object):\n", "\n", " def __init__(self, inimg):\n", - " self.input = inimg\n", + " self.img = inimg\n", " self.operations = []\n", "\n", " def add(self, value):\n", @@ -397,13 +469,13 @@ "\n", " def run(self, output=None):\n", "\n", - " data = np.array(self.input.get_data())\n", + " data = np.array(self.img.get_data())\n", "\n", " for oper, value in self.operations:\n", "\n", " # Values could be an image that\n", " # has already been loaded.\n", - " elif isinstance(value, nib.nifti1.Nifti1Image):\n", + " if isinstance(value, nib.nifti1.Nifti1Image):\n", " value = value.get_data()\n", "\n", " # Otherwise we assume that\n", @@ -440,11 +512,11 @@ "metadata": {}, "outputs": [], "source": [ - "input = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')\n", - "mask = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm_brain_mask.nii.gz')\n", - "input = nib.load(input)\n", - "mask = nib.load(mask)\n", - "fm = FSLMaths(input)\n", + "fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')\n", + "fmask = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm_brain_mask.nii.gz')\n", + "inimg = nib.load(fpath)\n", + "mask = nib.load(fmask)\n", + "fm = FSLMaths(inimg)\n", "\n", "fm.mul(mask)\n", "fm.add(-10)\n", @@ -462,17 +534,18 @@ "cell_type": "markdown", "metadata": {}, "source": [ + "<a class=\"anchor\" id=\"protecting-attribute-access\"></a>\n", "## Protecting attribute access\n", "\n", "\n", "In our `FSLMaths` class, the input image was added as an attribute called\n", - "`input` to `FSLMaths` objects. We saw that it is easy to read the attributes\n", + "`img` to `FSLMaths` objects. We saw that it is easy to read the attributes\n", "of an object - if we have a `FSLMaths` instance called `fm`, we can read its\n", - "input image via `fm.input`.\n", + "input image via `fm.img`.\n", "\n", "\n", "But it is just as easy to write the attributes of an object. What's to stop\n", - "some sloppy research assistant from overwriting our `input` attribute?" + "some sloppy research assistant from overwriting our `img` attribute?" ] }, { @@ -483,7 +556,7 @@ "source": [ "inimg = nib.load(op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz'))\n", "fm = FSLMaths(inimg)\n", - "fm.input = None\n", + "fm.img = None\n", "fm.run()" ] }, @@ -521,7 +594,11 @@ "metadata": {}, "source": [ "But you really shouldn't get into the habit of doing devious things like\n", - "this - take a look at the appendix for a [brief discussion on this topic](todo).\n", + "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", "\n", "\n", "Python tends to assume that programmers are \"responsible adults\", and hence\n", @@ -536,19 +613,19 @@ "\n", "* Class-level attributes and methods, and module-level attributes, functions,\n", " and classes, which begin with a single underscore (`_`), should be\n", - " considered _protected_ - they are intended for internal use only, and should\n", - " not be considered part of the public API of a class or module. This is not\n", - " enforced by the language in any way<sup>2</sup> - remember, we are all\n", - " responsible adults here!\n", + " considered __protected__ - they are intended for internal use only, and\n", + " should not be considered part of the public API of a class or module. This\n", + " is not enforced by the language in any way<sup>2</sup> - remember, we are\n", + " all responsible adults here!\n", "\n", "* Class-level attributes and methods which begin with a double-underscore\n", - " (`__`) should be considered _private_. Python provides a weak form of\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", - " not accessible through its original name. It is still accessible via its\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", - " though.\n", + " though).\n", "\n", "\n", "> <sup>2</sup> With the exception that module-level fields which begin with a\n", @@ -557,7 +634,7 @@ "\n", "\n", "So with all of this in mind, we can adjust our `FSLMaths` class to discourage\n", - "our sloppy research assistant from overwriting the `input` attribute:" + "our sloppy research assistant from overwriting the `img` attribute:" ] }, { @@ -569,7 +646,7 @@ "# remainder of definition omitted for brevity\n", "class FSLMaths(object):\n", " def __init__(self, inimg):\n", - " self.__input = inimg\n", + " self.__img = inimg\n", " self.__operations = []" ] }, @@ -577,7 +654,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "But now we have lost the ability to read our `__input` attribute:" + "But now we have lost the ability to read our `__img` attribute:" ] }, { @@ -588,19 +665,20 @@ "source": [ "inimg = nib.load(op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz'))\n", "fm = FSLMaths(inimg)\n", - "print(fm.__input)" + "print(fm.__img)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ + "<a class=\"anchor\" id=\"a-better-way-properties\"></a>\n", "### A better way - properties\n", "\n", "\n", "Python has a feature called\n", "[`properties`](https://docs.python.org/3.5/library/functions.html#property),\n", - "which is a nice means of controlling access to the attributes of an object. We\n", + "which is a nice way of controlling access to the attributes of an object. We\n", "can use properties by defining a \"getter\" method which can be used to access\n", "our attributes, and \"decorating\" them with the `@property` decorator (we will\n", "cover decorators in a later practical)." @@ -614,20 +692,20 @@ "source": [ "class FSLMaths(object):\n", " def __init__(self, inimg):\n", - " self.__input = inimg\n", + " self.__img = inimg\n", " self.__operations = []\n", "\n", " @property\n", - " def input(self):\n", - " return self.__input" + " def img(self):\n", + " return self.__img" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "So we are still storing our input image as a private attribute, but now\n", - "we have made it available in a read-only manner via the `input` property:" + "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:" ] }, { @@ -636,18 +714,18 @@ "metadata": {}, "outputs": [], "source": [ - "input = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')\n", - "inimg = nib.load(input)\n", + "fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')\n", + "inimg = nib.load(fpath)\n", "fm = FSLMaths(inimg)\n", "\n", - "print(fm.input.get_filename())" + "print(fm.img.get_filename())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Note that, even though we have defined `input` as a method, we can access it\n", + "Note that, even though we have defined `img` as a method, we can access it\n", "like an attribute - this is due to the magic behind the `@property` decorator.\n", "\n", "\n", @@ -664,19 +742,19 @@ "source": [ "class FSLMaths(object):\n", " def __init__(self, inimg):\n", - " self.__input = None\n", + " self.__img = None\n", " self.__operations = []\n", - " self.input = inimg\n", + " self.img = inimg\n", "\n", " @property\n", - " def input(self):\n", + " def img(self):\n", " return self.__input\n", "\n", - " @input.setter\n", - " def input(self, value):\n", + " @img.setter\n", + " def img(self, value):\n", " if not isinstance(value, nib.nifti1.Nifti1Image):\n", " raise ValueError('value must be a NIFTI image!')\n", - " self.__input = value" + " self.__img = value" ] }, { @@ -694,32 +772,33 @@ "metadata": {}, "outputs": [], "source": [ - "input = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')\n", - "inimg = nib.load(input)\n", + "fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')\n", + "inimg = nib.load(fpath)\n", "fm = FSLMaths(inimg)\n", "\n", - "print('Input: ', fm.input.get_filename())\n", + "print('Input: ', fm.img.get_filename())\n", "\n", "# let's change the input\n", "# to a different image\n", - "input2 = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm_brain.nii.gz')\n", - "inimg2 = nib.load(input2)\n", - "fm.input = inimg2\n", + "fpath2 = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm_brain.nii.gz')\n", + "inimg2 = nib.load(fpath2)\n", + "fm.img = inimg2\n", "\n", - "print('New input: ', fm.input.get_filename())\n", + "print('New input: ', fm.img.get_filename())\n", "\n", "# this is going to explode\n", - "fm.input = 'abcde'" + "fm.img = 'abcde'" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "> Note also that we used the `input` setter method within `__init__` to\n", + "> 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", "## Inheritance\n", "\n", "\n", @@ -728,12 +807,13 @@ "classes and instances.\n", "\n", "\n", + "<a class=\"anchor\" id=\"the-basics\"></a>\n", "### The basics\n", "\n", "\n", "For example, a veterinary surgery might be running some Python code which\n", - "looks like the following. Perhaps it is used to assist the nurses in\n", - "identifying an animal when it arrives at the surgery:" + "looks like the following, to assist the nurses in identifying an animal when\n", + "it arrives at the surgery:" ] }, { @@ -804,16 +884,18 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Code re-use and problem decomposition\n", + "Note that calling the `noiseMade` method on a `Labrador` instance resulted in\n", + "the `Dog.noiseMade` implementation being called.\n", "\n", "\n", - "Inheritance allows us to split a problem into smaller problems, and to re-use\n", - "code. Let's demonstrate this with a more involved example. Imagine that a\n", - "former colleague had written a class called `Operator`:\n", + "<a class=\"anchor\" id=\"code-re-use-and-problem-decomposition\"></a>\n", + "### Code re-use and problem decomposition\n", "\n", "\n", - "> I know this is a little abstract (and quite contrived), but bear with me\n", - "> here." + "Inheritance allows us to split a problem into smaller problems, and to re-use\n", + "code. Let's demonstrate this with a more involved (and even more contrived)\n", + "example. Imagine that a former colleague had written a class called\n", + "`Operator`:" ] }, { @@ -825,6 +907,7 @@ "class Operator(object):\n", "\n", " def __init__(self):\n", + " super().__init__()\n", " self.__operations = []\n", " self.__opFuncs = {}\n", "\n", @@ -916,7 +999,7 @@ "source": [ "The `NumberOperator` is a sub-class of `Operator`, which we can use for basic\n", "numerical calculations. It provides a handful of simple numerical methods, but\n", - "the most interesting stuff is inside `__init__`:\n", + "the most interesting stuff is inside `__init__`.\n", "\n", "\n", "> ```\n", @@ -925,12 +1008,30 @@ "\n", "\n", "This line invokes `Operator.__init__` - the initialisation method for the\n", - "`Operator` base-class. In Python, we can use the [built-in `super`\n", + "`Operator` base-class.\n", + "\n", + "\n", + "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", "\n", "\n", + "> The `super` function is one thing which changed between Python 2 and 3 -\n", + "> in Python 2, it was necessary to pass both the type and the instance\n", + "> to `super`. So it is common to see code that looks like this:\n", + ">\n", + "> ```\n", + "> def __init__(self):\n", + "> super(NumberOperator, self).__init__()\n", + "> ```\n", + ">\n", + "> Fortunately things are a lot cleaner in Python 3.\n", + "\n", + "\n", + "Let's move on to the next few lines in `__init__`:\n", + "\n", + "\n", "> ```\n", "> self.addFunction('add', self.add)\n", "> self.addFunction('mul', self.mul)\n", @@ -939,7 +1040,7 @@ "\n", "\n", "Here we are registering all of the functionality that is provided by the\n", - "`NumberOperator` class, via the `Opoerator.addFunction` method.\n", + "`NumberOperator` class, via the `Operator.addFunction` method.\n", "\n", "\n", "The `NumberOperator` class has also overridden the `preprocess` method, to\n", @@ -947,12 +1048,11 @@ "called within the `run` method - for a `NumberOperator` instance, the\n", "`NumberOperator.preprocess` method will get called<sup>1</sup>.\n", "\n", - "> <sup>1</sup> We can still [access overridden base-class methods](todo link)\n", - "> via the `super()` function, or by explicitly calling the base-class\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", "\n", - "\n", "Now let's see what our `NumberOperator` class does:" ] }, @@ -1023,6 +1123,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ + "<a class=\"anchor\" id=\"polymorphism\"></a>\n", "### Polymorphism\n", "\n", "\n", @@ -1030,9 +1131,9 @@ "to idea that, in an object-oriented language, we should be able to use an\n", "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 should work on an instance of any `Operator`\n", - "sub-classs. For example, we can write a function which prints a summary\n", - "of an `Operator` instance:" + "`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:" ] }, { @@ -1075,15 +1176,135 @@ "cell_type": "markdown", "metadata": {}, "source": [ + "<a class=\"anchor\" id=\"multiple-inheritance\"></a>\n", "### Multiple inheritance\n", "\n", "\n", - "Mention the MRO\n", + "Python allows you to define a class which has multiple base classes - this is\n", + "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:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class Notifier(object):\n", + "\n", + " def __init__(self):\n", + " super().__init__()\n", + " self.__listeners = {}\n", + "\n", + " def register(self, name, func):\n", + " self.__listeners[name] = func\n", + "\n", + " def notify(self, *args, **kwargs):\n", + " for func in self.__listeners.values():\n", + " func(*args, **kwargs)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's modify the `StringOperator` class to use the functionality of the\n", + "`Notifier ` class:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class StringOperator(Operator, Notifier):\n", + "\n", + " def __init__(self):\n", + " super().__init__()\n", + " self.addFunction('capitalise', self.capitalise)\n", + " self.addFunction('concat', self.concat)\n", + "\n", + " def preprocess(self, value):\n", + " return str(value)\n", "\n", + " def capitalise(self, s):\n", + " result = ' '.join([w[0].upper() + w[1:] for w in s.split()])\n", + " self.notify(result)\n", + " return result\n", "\n", + " def concat(self, s1, s2):\n", + " return s1 + s2" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, anything which is interested in uses of the `capitalise` method can\n", + "register as a listener on a `StringOperator` instance:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "so = StringOperator()\n", + "\n", + "def capitaliseCalled(result):\n", + " print('Capitalise operation called: {}'.format(result))\n", + "\n", + "so.register('mylistener', capitaliseCalled)\n", + "\n", + "so = StringOperator()\n", + "so.do('capitalise')\n", + "so.do('concat', '?')\n", + "\n", + "print(so.run('did you notice that functions are objects too'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If you wish to use multiple inheritance in your design, it is important to be\n", + "aware of the mechanism that Python uses to determine how base class methods\n", + "are called (and which base class method will be called, in the case of naming\n", + "conflicts). This is referred to as the Method Resolution Order (MRO) - further\n", + "details on the topic can be found\n", + "[here](https://www.python.org/download/releases/2.3/mro/), and a more concise\n", + "summary\n", + "[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", + "methods instead of using `super()`. For example:\n", "\n", + "\n", + "> ```\n", + "> class StringOperator(Operator, Notifier):\n", + "> def __init__(self):\n", + "> Operator.__init__(self)\n", + "> Notifier.__init__(self)\n", + "> ```\n", + "\n", + "\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", + "\n", + "\n", + "<a class=\"anchor\" id=\"class-attributes-and-methods\"></a>\n", "## Class attributes and methods\n", "\n", "\n", @@ -1105,11 +1326,11 @@ "performance of the `add` operation.\n", "\n", "\n", + "<a class=\"anchor\" id=\"class-attributes\"></a>\n", "### Class attributes\n", "\n", "\n", "Let's add a `dict` as a class attribute to the `FSLMaths` class - this `dict`\n", - "\n", "called on a `FSLMaths` object, that object will increment the class-level\n", "counters for each operation that is applied:" ] @@ -1132,7 +1353,7 @@ " opCounters = {}\n", "\n", " def __init__(self, inimg):\n", - " self.input = inimg\n", + " self.img = inimg\n", " self.operations = []\n", "\n", " def add(self, value):\n", @@ -1146,7 +1367,7 @@ "\n", " def run(self, output=None):\n", "\n", - " data = np.array(self.input.get_data())\n", + " data = np.array(self.img.get_data())\n", "\n", " for oper, value in self.operations:\n", "\n", @@ -1170,9 +1391,10 @@ "metadata": {}, "outputs": [], "source": [ - "input = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')\n", - "mask = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm_brain_mask.nii.gz')\n", - "inimg = nib.load(input)\n", + "fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')\n", + "fmask = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm_brain_mask.nii.gz')\n", + "inimg = nib.load(fpath)\n", + "mask = nib.load(fmask)\n", "\n", "fm1 = FSLMaths(inimg)\n", "fm2 = FSLMaths(inimg)\n", @@ -1195,11 +1417,18 @@ "cell_type": "markdown", "metadata": {}, "source": [ + "<a class=\"anchor\" id=\"class-methods\"></a>\n", "### Class methods\n", "\n", "\n", "It is just as easy to add a method to a class - let's take our reporting code\n", - "from above, and add it as a method to the `FSLMaths` class:" + "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':" ] }, { @@ -1212,12 +1441,6 @@ "\n", " opCounters = {}\n", "\n", - " # We use the @classmethod decorator to denote a class\n", - " # method. Also note that, where a regular method which\n", - " # is called on an instance will be passed the instance\n", - " # as its first argument ('self'), a class method will\n", - " # be passed the class itself as the first argument -\n", - " # the standard convention is to call this argument 'cls'.\n", " @classmethod\n", " def usage(cls):\n", " print('FSLMaths usage statistics')\n", @@ -1225,7 +1448,7 @@ " print(' {} : {}'.format(oper, FSLMaths.opCounters.get(oper, 0)))\n", "\n", " def __init__(self, inimg):\n", - " self.input = inimg\n", + " self.img = inimg\n", " self.operations = []\n", "\n", " def add(self, value):\n", @@ -1239,7 +1462,7 @@ "\n", " def run(self, output=None):\n", "\n", - " data = np.array(self.input.get_data())\n", + " data = np.array(self.img.get_data())\n", "\n", " for oper, value in self.operations:\n", "\n", @@ -1254,7 +1477,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "alling a class method is the same as accessing a class attribute:" + "Calling a class method is the same as accessing a class attribute:" ] }, { @@ -1263,9 +1486,10 @@ "metadata": {}, "outputs": [], "source": [ - "input = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')\n", - "mask = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm_brain_mask.nii.gz')\n", - "inimg = nib.load(input)\n", + "fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')\n", + "fmask = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm_brain_mask.nii.gz')\n", + "inimg = nib.load(fpath)\n", + "mask = nib.load(fmask)\n", "\n", "fm1 = FSLMaths(inimg)\n", "fm2 = FSLMaths(inimg)\n", @@ -1304,6 +1528,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ + "<a class=\"anchor\" id=\"appendix-the-object-base-class\"></a>\n", "## Appendix: The `object` base-class\n", "\n", "\n", @@ -1311,6 +1536,7 @@ "your class inherits. If your class does not inherit from a particular class,\n", "then it should inherit from the built-in `object` class:\n", "\n", + "\n", "> ```\n", "> class MyClass(object):\n", "> ...\n", @@ -1320,21 +1546,27 @@ "However, in older code bases, you might see class definitions that look like\n", "this, without explicitly inheriting from the `object` base class:\n", "\n", + "\n", "> ```\n", "> class MyClass:\n", "> ...\n", "> ```\n", "\n", + "\n", "This syntax is a [throwback to older versions of\n", "Python](https://docs.python.org/2/reference/datamodel.html#new-style-and-classic-classes).\n", - "In Python 3 there is actually no difference between in whether you define your\n", - "class in the way we have shown in this tutorial, or the old-style way.\n", + "In Python 3 there is actually no difference in defining classes in the\n", + "\"new-style\" way we have used throughout this tutorial, or the \"old-style\" way\n", + "mentioned in this appendix.\n", "\n", "\n", "But if you are writing code which needs to run on both Python 2 and 3, you\n", - "_must_ define your classes to explicitly inherit from the `object` base class.\n", + "__must__ define your classes in the new-style convention, i.e. by explicitly\n", + "inheriting from the `object` base class. Therefore, the safest approach is to\n", + "always use the new-style format.\n", "\n", "\n", + "<a class=\"anchor\" id=\"appendix-init-versus-new\"></a>\n", "## Appendix: `__init__` versus `__new__`\n", "\n", "\n", @@ -1353,37 +1585,39 @@ "docs](https://docs.python.org/3.5/reference/datamodel.html#basic-customization).\n", "\n", "\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", - "considered quite rude practice.\n", + "considered quite bad practice.\n", "\n", "\n", "Just because you _can_ do something doesn't mean that you _should_. Python\n", "gives you the flexibility to write your software in whatever manner you deem\n", "suitable. __But__ if you want to write software that will be used, adopted,\n", - "and maintained by other people, you should be polite, write your code in a\n", - "clear, readable fashion, and avoid the use of devious tactics such as\n", + "maintained, and enjoyed by other people, you should be polite, write your code\n", + "in a clear, readable fashion, and avoid the use of devious tactics such as\n", "monkey-patching.\n", "\n", "\n", "__However__, while monkey-patching may seem like a horrific programming\n", "practice to those of you coming from the realms of C++, Java, and the like,\n", "(and it is horrific in many cases), it can be _extremely_ useful in certain\n", - "circumstances. For instance, monkey-patching makes unit testing [a\n", - "breeze](https://docs.python.org/3.5/library/unittest.mock.html) in Python.\n", + "circumstances. For instance, monkey-patching makes [unit testing a\n", + "breeze in Python](https://docs.python.org/3.5/library/unittest.mock.html).\n", "\n", "\n", "As another example, consider the scenario where you are dependent on a third\n", "party library which has bugs in it. No problem - while you are waiting for the\n", "library author to release a new version of the library, you can write your own\n", "working implementation and [monkey-patch it\n", - "in](https://git.fmrib.ox.ac.uk/fsl/fsleyes/fsleyes/blob/0.21.0/fsleyes/views/viewpanel.py#L726)!\n", + "in!](https://git.fmrib.ox.ac.uk/fsl/fsleyes/fsleyes/blob/0.21.0/fsleyes/views/viewpanel.py#L726)\n", "\n", "\n", + "<a class=\"anchor\" id=\"appendix-method-overloading\"></a>\n", "## Appendix: Method overloading\n", "\n", "\n", @@ -1433,19 +1667,16 @@ "cell_type": "markdown", "metadata": {}, "source": [ + "<a class=\"anchor\" id=\"useful-references\"></a>\n", "## Useful references\n", "\n", "\n", - "https://docs.python.org/3.5/library/unittest.mock.html\n", - "https://docs.python.org/3.5/tutorial/classes.html\n", - "https://docs.python.org/3.5/library/functions.html\n", - "https://docs.python.org/2/reference/datamodel.html\n", - "https://www.reddit.com/r/learnpython/comments/2s3pms/what_is_the_difference_between_init_and_new/cnm186z/\n", - "https://docs.python.org/3.5/reference/datamodel.html\n", - "http://www.jesshamrick.com/2011/05/18/an-introduction-to-classes-and-inheritance-in-python/\n", - "https://www.digitalocean.com/community/tutorials/understanding-class-inheritance-in-python-3\n", + "The official Python documentation has a wealth of information on the internal\n", + "workings of classes and objects, so these pages are worth a read:\n", + "\n", "\n", - "https://docs.python.org/3.5/library/functions.html#super" + "* https://docs.python.org/3.5/tutorial/classes.html\n", + "* https://docs.python.org/3.5/reference/datamodel.html" ] } ], diff --git a/advanced_topics/object_oriented_programming.md b/advanced_topics/object_oriented_programming.md index adf8e16850e85f5d051b68d70f1e526a658d62ab..891a20821848bb0cf4a70867c985360786930754 100644 --- a/advanced_topics/object_oriented_programming.md +++ b/advanced_topics/object_oriented_programming.md @@ -12,6 +12,31 @@ done. But some problems are just easier to solve, and to reason about, when 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) +* [Appendix: The `object` base-class](#appendix-the-object-base-class) +* [Appendix: `__init__` versus `__new__`](#appendix-init-versus-new) +* [Appendix: Monkey-patching](#appendix-monkey-patching) +* [Appendix: Method overloading](#appendix-method-overloading) +* [Useful references](#useful-references) + + +<a class="anchor" id="objects-versus-classes"></a> ## Objects versus classes @@ -21,8 +46,8 @@ section. If you have not done any object-oriented programming before, your first step -is to understand the difference between _objects_ (also known as _instances_) -and _classes_ (also known as _types_). +is to understand the difference between _objects_ (also known as +_instances_) and _classes_ (also known as _types_). If you have some experience in C, then you can start off by thinking of a @@ -76,6 +101,7 @@ and an _object_. > object - even classes! +<a class="anchor" id="defining-a-class"></a> ## Defining a class @@ -110,6 +136,7 @@ Although these objects are not of much use at this stage. Let's do some more work. +<a class="anchor" id="object-creation-the-init-method"></a> ## Object creation - the `__init__` method @@ -120,12 +147,12 @@ It makes sense to pass this in when we create an `FSLMaths` object: ``` class FSLMaths(object): def __init__(self, inimg): - self.input = inimg + self.img = inimg ``` Here we have added a _method_ called `__init__` to our class (remember that a -_method_ is just a function which is defined in a cliass, and which can be +_method_ is just a function which is defined in a class, and which can be called on instances of that class). This method expects two arguments - `self`, and `inimg`. So now, when we create an instance of the `FSLMaths` class, we will need to provide an input image: @@ -135,28 +162,63 @@ class, we will need to provide an input image: import nibabel as nib import os.path as op -input = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz') -inimg = nib.load(input) +fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz') +inimg = nib.load(fpath) fm = FSLMaths(inimg) ``` -There are a couple of things to note here: +There are a couple of things to note here... -__Our method is called__ `__init__`__, but we didn't actually call the__ -`__init__` __method!__ `__init__` is a special method in Python - it is called -when an instance of a class is created. And recall that we can create an -instance of a class by calling the class in the same way that we call a -function. +<a class="anchor" id="our-method-is-called-init"></a> +### Our method is called `__init__`, but we didn't actually call the `__init__` method! -__We didn't specify the `self` argument - what gives?!?__ The `self` argument -is a special argument for methods in Python. If you are coming from C++, Java, -C# or similar, `self` in Python is equivalent to `this` in those languages. +`__init__` is a special method in Python - it is called when an instance of a +class is created. And recall that we can create an instance of a class by +calling the class in the same way that we call a function. -### The `self` argument +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 +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` +class like so: + + +``` +class FSLMaths(object): + + def __init__(self, inimg): + self.img = inimg + + def __str__(self): + return 'FSLMaths({})'.format(self.img.get_filename()) + +fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz') +inimg = nib.load(fpath) +fm = FSLMaths(inimg) + +print(fm) +``` + + +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). + + +<a class="anchor" id="we-didnt-specify-the-self-argument"></a> +### We didn't specify the `self` argument - what gives?!? + + +The `self` argument is a special argument for methods in Python. If you are +coming from C++, Java, C# or similar, `self` in Python is equivalent to `this` +in those languages. In a method, the `self` argument is a reference to the object that the method @@ -173,20 +235,21 @@ that has been created (and is then assigned to the `fm` variable, after the `__init__` method has finished). -But note that we 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 as the `self` argument to the -method. +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 +first argument to the method. -But when you are writing a class, you _do_ need to explicitly list `self` as +But when you are writing a class, you __do__ need to explicitly list `self` as the first argument to all of the methods of the class. +<a class="anchor" id="attributes"></a> ## Attributes -In Python, the term _attribute_ is used to refer to a piece of information +In Python, the term __attribute__ is used to refer to a piece of information that is associated with an object. An attribute is generally a reference to another object (which might be a string, a number, or a list, or some other more complicated object). @@ -199,21 +262,21 @@ image on creation: ``` class FSLMaths(object): def __init__(self, inimg): - self.input = inimg + self.img = inimg -input = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz') -fm = FSLMaths(nib.load(input)) +fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz') +fm = FSLMaths(nib.load(fpath)) ``` Take a look at what is going on in the `__init__` method - we take the `inimg` -argument, and create a reference to it called `self.input`. We have added an -_attribute_ to the `FSLMaths` instance, called `input`, and we can access that +argument, and create a reference to it called `self.img`. We have added an +_attribute_ to the `FSLMaths` instance, called `img`, and we can access that attribute like so: ``` -print('Input for our FSLMaths instance: {}'.format(fm.input.get_filename())) +print('Input for our FSLMaths instance: {}'.format(fm.img.get_filename())) ``` @@ -224,8 +287,8 @@ Just kidding. But it really is that simple. This is one aspect of Python which might be quite jarring to you if you are coming from a language with more rigid semantics, such as C++ or Java. In those languages, you must pre-specify all of the attributes and methods that are a part of a class. But Python is -more flexible - you simply add attributes to an object affer it has been -created. In fact, you can even do this outside of the class +much more flexible - you can simply add attributes to an object after it has +been created. In fact, you can even do this outside of the class definition<sup>1</sup>: @@ -236,7 +299,7 @@ print(fm.another_attribute) ``` -__But...__ while attributes can be added to a Python object at any time, it is +__But ...__ while attributes can be added to a Python object at any time, it is good practice (and makes for more readable and maintainable code) to add all of an object's attributes within the `__init__` method. @@ -246,6 +309,7 @@ of an object's attributes within the `__init__` method. > extensions (Python modules that are written in C). +<a class="anchor" id="methods"></a> ## Methods @@ -258,7 +322,7 @@ functionality: class FSLMaths(object): def __init__(self, inimg): - self.input = inimg + self.img = inimg self.operations = [] def add(self, value): @@ -300,7 +364,7 @@ import nibabel as nib class FSLMaths(object): def __init__(self, inimg): - self.input = inimg + self.img = inimg self.operations = [] def add(self, value): @@ -314,13 +378,13 @@ class FSLMaths(object): def run(self, output=None): - data = np.array(self.input.get_data()) + data = np.array(self.img.get_data()) for oper, value in self.operations: # Values could be an image that # has already been loaded. - elif isinstance(value, nib.nifti1.Nifti1Image): + if isinstance(value, nib.nifti1.Nifti1Image): value = value.get_data() # Otherwise we assume that @@ -349,11 +413,11 @@ We now have a useable (but not very useful) `FSLMaths` class! ``` -input = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz') -mask = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm_brain_mask.nii.gz') -input = nib.load(input) -mask = nib.load(mask) -fm = FSLMaths(input) +fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz') +fmask = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm_brain_mask.nii.gz') +inimg = nib.load(fpath) +mask = nib.load(fmask) +fm = FSLMaths(inimg) fm.mul(mask) fm.add(-10) @@ -368,23 +432,24 @@ print('Number of voxels >0 in masked image: {}'.format(nmaskvox)) ``` +<a class="anchor" id="protecting-attribute-access"></a> ## Protecting attribute access In our `FSLMaths` class, the input image was added as an attribute called -`input` to `FSLMaths` objects. We saw that it is easy to read the attributes +`img` to `FSLMaths` objects. We saw that it is easy to read the attributes of an object - if we have a `FSLMaths` instance called `fm`, we can read its -input image via `fm.input`. +input image via `fm.img`. But it is just as easy to write the attributes of an object. What's to stop -some sloppy research assistant from overwriting our `input` attribute? +some sloppy research assistant from overwriting our `img` attribute? ``` inimg = nib.load(op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')) fm = FSLMaths(inimg) -fm.input = None +fm.img = None fm.run() ``` @@ -411,7 +476,11 @@ fm.mul(123) But you really shouldn't get into the habit of doing devious things like -this - take a look at the appendix for a [brief discussion on this topic](todo). +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). Python tends to assume that programmers are "responsible adults", and hence @@ -426,19 +495,19 @@ to](https://docs.python.org/3.5/tutorial/classes.html#private-variables): * Class-level attributes and methods, and module-level attributes, functions, and classes, which begin with a single underscore (`_`), should be - considered _protected_ - they are intended for internal use only, and should - not be considered part of the public API of a class or module. This is not - enforced by the language in any way<sup>2</sup> - remember, we are all - responsible adults here! + considered __protected__ - they are intended for internal use only, and + should not be considered part of the public API of a class or module. This + is not enforced by the language in any way<sup>2</sup> - remember, we are + all responsible adults here! * Class-level attributes and methods which begin with a double-underscore - (`__`) should be considered _private_. Python provides a weak form of + (`__`) 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 - not accessible through its original name. It is still accessible via its + 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) - though. + though). > <sup>2</sup> With the exception that module-level fields which begin with a @@ -447,33 +516,34 @@ to](https://docs.python.org/3.5/tutorial/classes.html#private-variables): So with all of this in mind, we can adjust our `FSLMaths` class to discourage -our sloppy research assistant from overwriting the `input` attribute: +our sloppy research assistant from overwriting the `img` attribute: ``` # remainder of definition omitted for brevity class FSLMaths(object): def __init__(self, inimg): - self.__input = inimg + self.__img = inimg self.__operations = [] ``` -But now we have lost the ability to read our `__input` attribute: +But now we have lost the ability to read our `__img` attribute: ``` inimg = nib.load(op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')) fm = FSLMaths(inimg) -print(fm.__input) +print(fm.__img) ``` +<a class="anchor" id="a-better-way-properties"></a> ### A better way - properties Python has a feature called [`properties`](https://docs.python.org/3.5/library/functions.html#property), -which is a nice means of controlling access to the attributes of an object. We +which is a nice way of controlling access to the attributes of an object. We can use properties by defining a "getter" method which can be used to access our attributes, and "decorating" them with the `@property` decorator (we will cover decorators in a later practical). @@ -482,29 +552,29 @@ cover decorators in a later practical). ``` class FSLMaths(object): def __init__(self, inimg): - self.__input = inimg + self.__img = inimg self.__operations = [] @property - def input(self): - return self.__input + def img(self): + return self.__img ``` -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 `input` property: +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: ``` -input = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz') -inimg = nib.load(input) +fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz') +inimg = nib.load(fpath) fm = FSLMaths(inimg) -print(fm.input.get_filename()) +print(fm.img.get_filename()) ``` -Note that, even though we have defined `input` as a method, we can access it +Note that, even though we have defined `img` as a method, we can access it like an attribute - this is due to the magic behind the `@property` decorator. @@ -516,19 +586,19 @@ image after creation. ``` class FSLMaths(object): def __init__(self, inimg): - self.__input = None + self.__img = None self.__operations = [] - self.input = inimg + self.img = inimg @property - def input(self): + def img(self): return self.__input - @input.setter - def input(self, value): + @img.setter + def img(self, value): if not isinstance(value, nib.nifti1.Nifti1Image): raise ValueError('value must be a NIFTI image!') - self.__input = value + self.__img = value ``` @@ -538,28 +608,29 @@ the new input is a NIFTI image: ``` -input = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz') -inimg = nib.load(input) +fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz') +inimg = nib.load(fpath) fm = FSLMaths(inimg) -print('Input: ', fm.input.get_filename()) +print('Input: ', fm.img.get_filename()) # let's change the input # to a different image -input2 = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm_brain.nii.gz') -inimg2 = nib.load(input2) -fm.input = inimg2 +fpath2 = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm_brain.nii.gz') +inimg2 = nib.load(fpath2) +fm.img = inimg2 -print('New input: ', fm.input.get_filename()) +print('New input: ', fm.img.get_filename()) # this is going to explode -fm.input = 'abcde' +fm.img = 'abcde' ``` -> Note also that we used the `input` setter method within `__init__` to +> 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 ## Inheritance @@ -568,12 +639,13 @@ _inheritance_ - the ability to define hierarchical relationships between classes and instances. +<a class="anchor" id="the-basics"></a> ### The basics For example, a veterinary surgery might be running some Python code which -looks like the following. Perhaps it is used to assist the nurses in -identifying an animal when it arrives at the surgery: +looks like the following, to assist the nurses in identifying an animal when +it arrives at the surgery: ``` @@ -628,22 +700,25 @@ print('Noise made by chihuahuas: {}'.format(ch.noiseMade())) ``` -### Code re-use and problem decomposition +Note that calling the `noiseMade` method on a `Labrador` instance resulted in +the `Dog.noiseMade` implementation being called. -Inheritance allows us to split a problem into smaller problems, and to re-use -code. Let's demonstrate this with a more involved example. Imagine that a -former colleague had written a class called `Operator`: +<a class="anchor" id="code-re-use-and-problem-decomposition"></a> +### Code re-use and problem decomposition -> I know this is a little abstract (and quite contrived), but bear with me -> here. +Inheritance allows us to split a problem into smaller problems, and to re-use +code. Let's demonstrate this with a more involved (and even more contrived) +example. Imagine that a former colleague had written a class called +`Operator`: ``` class Operator(object): def __init__(self): + super().__init__() self.__operations = [] self.__opFuncs = {} @@ -724,7 +799,7 @@ class NumberOperator(Operator): The `NumberOperator` is a sub-class of `Operator`, which we can use for basic numerical calculations. It provides a handful of simple numerical methods, but -the most interesting stuff is inside `__init__`: +the most interesting stuff is inside `__init__`. > ``` @@ -733,12 +808,30 @@ the most interesting stuff is inside `__init__`: This line invokes `Operator.__init__` - the initialisation method for the -`Operator` base-class. In Python, we can use the [built-in `super` +`Operator` base-class. + + +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)). +> The `super` function is one thing which changed between Python 2 and 3 - +> in Python 2, it was necessary to pass both the type and the instance +> to `super`. So it is common to see code that looks like this: +> +> ``` +> def __init__(self): +> super(NumberOperator, self).__init__() +> ``` +> +> Fortunately things are a lot cleaner in Python 3. + + +Let's move on to the next few lines in `__init__`: + + > ``` > self.addFunction('add', self.add) > self.addFunction('mul', self.mul) @@ -747,7 +840,7 @@ classes, in the case of [multiple inheritance](todo)). Here we are registering all of the functionality that is provided by the -`NumberOperator` class, via the `Opoerator.addFunction` method. +`NumberOperator` class, via the `Operator.addFunction` method. The `NumberOperator` class has also overridden the `preprocess` method, to @@ -755,12 +848,11 @@ ensure that all values handled by the `Operator` are numbers. This method gets called within the `run` method - for a `NumberOperator` instance, the `NumberOperator.preprocess` method will get called<sup>1</sup>. -> <sup>1</sup> We can still [access overridden base-class methods](todo link) -> via the `super()` function, or by explicitly calling the base-class +> <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. - Now let's see what our `NumberOperator` class does: @@ -814,7 +906,7 @@ so.do('concat', '!') print(so.run('python is an ok language')) ``` - +<a class="anchor" id="polymorphism"></a> ### Polymorphism @@ -822,9 +914,9 @@ Inheritance also allows us to take advantage of _polymorphism_, which refers 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 should work on an instance of any `Operator` -sub-classs. For example, we can write a function which prints a summary -of an `Operator` instance: +`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: ``` @@ -851,15 +943,110 @@ operatorSummary(so) ``` +<a class="anchor" id="multiple-inheritance"></a> ### Multiple inheritance -Mention the MRO +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: + + +``` +class Notifier(object): + + def __init__(self): + super().__init__() + self.__listeners = {} + + def register(self, name, func): + self.__listeners[name] = func + + def notify(self, *args, **kwargs): + for func in self.__listeners.values(): + func(*args, **kwargs) +``` + + +Let's modify the `StringOperator` class to use the functionality of the +`Notifier ` class: +``` +class StringOperator(Operator, Notifier): + + def __init__(self): + super().__init__() + self.addFunction('capitalise', self.capitalise) + self.addFunction('concat', self.concat) + def preprocess(self, value): + return str(value) + def capitalise(self, s): + result = ' '.join([w[0].upper() + w[1:] for w in s.split()]) + self.notify(result) + return result + def concat(self, s1, s2): + return s1 + s2 +``` + + +Now, anything which is interested in uses of the `capitalise` method can +register as a listener on a `StringOperator` instance: + + +``` +so = StringOperator() + +def capitaliseCalled(result): + print('Capitalise operation called: {}'.format(result)) + +so.register('mylistener', capitaliseCalled) + +so = StringOperator() +so.do('capitalise') +so.do('concat', '?') + +print(so.run('did you notice that functions are objects too')) +``` + +If you wish to use multiple inheritance in your design, it is important to be +aware of the mechanism that Python uses to determine how base class methods +are called (and which base class method will be called, in the case of naming +conflicts). This is referred to as the Method Resolution Order (MRO) - further +details on the topic can be found +[here](https://www.python.org/download/releases/2.3/mro/), and a more concise +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__` +methods instead of using `super()`. For example: + + +> ``` +> class StringOperator(Operator, Notifier): +> def __init__(self): +> Operator.__init__(self) +> Notifier.__init__(self) +> ``` + + +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. + + +<a class="anchor" id="class-attributes-and-methods"></a> ## Class attributes and methods @@ -881,11 +1068,11 @@ application to show evidence that more research is needed to optimise the performance of the `add` operation. +<a class="anchor" id="class-attributes"></a> ### 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: @@ -903,7 +1090,7 @@ class FSLMaths(object): opCounters = {} def __init__(self, inimg): - self.input = inimg + self.img = inimg self.operations = [] def add(self, value): @@ -917,7 +1104,7 @@ class FSLMaths(object): def run(self, output=None): - data = np.array(self.input.get_data()) + data = np.array(self.img.get_data()) for oper, value in self.operations: @@ -933,9 +1120,10 @@ So let's see it in action: ``` -input = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz') -mask = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm_brain_mask.nii.gz') -inimg = nib.load(input) +fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz') +fmask = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm_brain_mask.nii.gz') +inimg = nib.load(fpath) +mask = nib.load(fmask) fm1 = FSLMaths(inimg) fm2 = FSLMaths(inimg) @@ -955,11 +1143,18 @@ for oper in ('add', 'div', 'mul'): ``` +<a class="anchor" id="class-methods"></a> ### Class methods 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: +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': ``` @@ -967,12 +1162,6 @@ class FSLMaths(object): opCounters = {} - # We use the @classmethod decorator to denote a class - # method. Also 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'. @classmethod def usage(cls): print('FSLMaths usage statistics') @@ -980,7 +1169,7 @@ class FSLMaths(object): print(' {} : {}'.format(oper, FSLMaths.opCounters.get(oper, 0))) def __init__(self, inimg): - self.input = inimg + self.img = inimg self.operations = [] def add(self, value): @@ -994,7 +1183,7 @@ class FSLMaths(object): def run(self, output=None): - data = np.array(self.input.get_data()) + data = np.array(self.img.get_data()) for oper, value in self.operations: @@ -1006,13 +1195,14 @@ class FSLMaths(object): ``` -alling a class method is the same as accessing a class attribute: +Calling a class method is the same as accessing a class attribute: ``` -input = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz') -mask = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm_brain_mask.nii.gz') -inimg = nib.load(input) +fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz') +fmask = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm_brain_mask.nii.gz') +inimg = nib.load(fpath) +mask = nib.load(fmask) fm1 = FSLMaths(inimg) fm2 = FSLMaths(inimg) @@ -1029,6 +1219,7 @@ fm2.run() FSLMaths.usage() ``` + Note that it is also possible to access class attributes and methods through instances: @@ -1039,6 +1230,7 @@ print(fm1.usage()) ``` +<a class="anchor" id="appendix-the-object-base-class"></a> ## Appendix: The `object` base-class @@ -1046,6 +1238,7 @@ When you are defining a class, you need to specify the base-class from which your class inherits. If your class does not inherit from a particular class, then it should inherit from the built-in `object` class: + > ``` > class MyClass(object): > ... @@ -1055,21 +1248,27 @@ then it should inherit from the built-in `object` class: However, in older code bases, you might see class definitions that look like this, without explicitly inheriting from the `object` base class: + > ``` > class MyClass: > ... > ``` + This syntax is a [throwback to older versions of Python](https://docs.python.org/2/reference/datamodel.html#new-style-and-classic-classes). -In Python 3 there is actually no difference between in whether you define your -class in the way we have shown in this tutorial, or the old-style way. +In Python 3 there is actually no difference in defining classes in the +"new-style" way we have used throughout this tutorial, or the "old-style" way +mentioned in this appendix. But if you are writing code which needs to run on both Python 2 and 3, you -_must_ define your classes to explicitly inherit from the `object` base class. +__must__ define your classes in the new-style convention, i.e. by explicitly +inheriting from the `object` base class. Therefore, the safest approach is to +always use the new-style format. +<a class="anchor" id="appendix-init-versus-new"></a> ## Appendix: `__init__` versus `__new__` @@ -1088,37 +1287,39 @@ 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 ## 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 -considered quite rude practice. +considered quite bad practice. Just because you _can_ do something doesn't mean that you _should_. Python gives you the flexibility to write your software in whatever manner you deem suitable. __But__ if you want to write software that will be used, adopted, -and maintained by other people, you should be polite, write your code in a -clear, readable fashion, and avoid the use of devious tactics such as +maintained, and enjoyed by other people, you should be polite, write your code +in a clear, readable fashion, and avoid the use of devious tactics such as monkey-patching. __However__, while monkey-patching may seem like a horrific programming practice to those of you coming from the realms of C++, Java, and the like, (and it is horrific in many cases), it can be _extremely_ useful in certain -circumstances. For instance, monkey-patching makes unit testing [a -breeze](https://docs.python.org/3.5/library/unittest.mock.html) in Python. +circumstances. For instance, monkey-patching makes [unit testing a +breeze in Python](https://docs.python.org/3.5/library/unittest.mock.html). As another example, consider the scenario where you are dependent on a third party library which has bugs in it. No problem - while you are waiting for the library author to release a new version of the library, you can write your own working implementation and [monkey-patch it -in](https://git.fmrib.ox.ac.uk/fsl/fsleyes/fsleyes/blob/0.21.0/fsleyes/views/viewpanel.py#L726)! +in!](https://git.fmrib.ox.ac.uk/fsl/fsleyes/fsleyes/blob/0.21.0/fsleyes/views/viewpanel.py#L726) +<a class="anchor" id="appendix-method-overloading"></a> ## Appendix: Method overloading @@ -1159,16 +1360,14 @@ print('Add three: {}'.format(a.add(1, 2, 3))) print('Add four: {}'.format(a.add(1, 2, 3, 4))) ``` + +<a class="anchor" id="useful-references"></a> ## Useful references -https://docs.python.org/3.5/library/unittest.mock.html -https://docs.python.org/3.5/tutorial/classes.html -https://docs.python.org/3.5/library/functions.html -https://docs.python.org/2/reference/datamodel.html -https://www.reddit.com/r/learnpython/comments/2s3pms/what_is_the_difference_between_init_and_new/cnm186z/ -https://docs.python.org/3.5/reference/datamodel.html -http://www.jesshamrick.com/2011/05/18/an-introduction-to-classes-and-inheritance-in-python/ -https://www.digitalocean.com/community/tutorials/understanding-class-inheritance-in-python-3 +The official Python documentation has a wealth of information on the internal +workings of classes and objects, so these pages are worth a read: + -https://docs.python.org/3.5/library/functions.html#super \ No newline at end of file +* https://docs.python.org/3.5/tutorial/classes.html +* https://docs.python.org/3.5/reference/datamodel.html