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