diff --git a/advanced_topics/03_object_oriented_programming.ipynb b/advanced_topics/03_object_oriented_programming.ipynb index 40e01b70427412bacbd77909c88b4a88959d999f..7c8eda183d89c5dec9a0357c3c750bb2bd44b6ca 100644 --- a/advanced_topics/03_object_oriented_programming.ipynb +++ b/advanced_topics/03_object_oriented_programming.ipynb @@ -25,6 +25,7 @@ " * [We didn't specify the `self` argument - what gives?!?](#we-didnt-specify-the-self-argument)\n", "* [Attributes](#attributes)\n", "* [Methods](#methods)\n", + "* [Method chaining](#method-chaining)\n", "* [Protecting attribute access](#protecting-attribute-access)\n", " * [A better way - properties](#a-better-way-properties])\n", "* [Inheritance](#inheritance)\n", @@ -52,8 +53,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\n", - "_instances_) 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", @@ -72,8 +73,8 @@ "> ```\n", "\n", "\n", - "Now, an _object_ is not a definition, but rather a thing which resides in\n", - "memory. An object can have _attributes_ (pieces of information), and _methods_\n", + "Now, an *object* is not a definition, but rather a thing which resides in\n", + "memory. An object can have *attributes* (pieces of information), and *methods*\n", "(functions associated with the object). You can pass objects around your code,\n", "manipulate their attributes, and call their methods.\n", "\n", @@ -98,12 +99,12 @@ "Of course there are many more differences between C structs and classes (most\n", "notably [inheritance](todo), [polymorphism](todo), and [access\n", "protection](todo)). But if you can understand the difference between a\n", - "_definition_ of a C struct, and an _instantiation_ of that struct, then you\n", - "are most of the way towards understanding the difference between a _class_,\n", - "and an _object_.\n", + "*definition* of a C struct, and an *instantiation* of that struct, then you\n", + "are most of the way towards understanding the difference between a *class*,\n", + "and an *object*.\n", "\n", "\n", - "> But just to confuse you, remember that in Python, __everything__ is an\n", + "> But just to confuse you, remember that in Python, **everything** is an\n", "> object - even classes!\n", "\n", "\n", @@ -252,7 +253,7 @@ "metadata": {}, "source": [ "Refer to the [official\n", - "docs](https://docs.python.org/3.5/reference/datamodel.html#special-method-names)\n", + "docs](https://docs.python.org/3/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](appendix-init-versus-new).\n", @@ -438,8 +439,8 @@ "\n", "The idea behind this design is that our `FSLMaths` class will not actually do\n", "anything when we call the `add`, `mul` or `div` methods. Instead, it will\n", - "\"stage\" each operation, and then perform them all in one go. So let's add\n", - "another method, `run`, which actually does the work:" + "*stage* each operation, and then perform them all in one go at a later point\n", + "in time. So let's add another method, `run`, which actually does the work:" ] }, { @@ -478,7 +479,6 @@ " if isinstance(value, nib.nifti1.Nifti1Image):\n", " value = value.get_data()\n", "\n", - "\n", " if oper == 'add':\n", " data = data + value\n", " elif oper == 'mul':\n", @@ -532,6 +532,117 @@ "cell_type": "markdown", "metadata": {}, "source": [ + "<a class=\"anchor\" id=\"method-chaining\"></a>\n", + "## Method chaining\n", + "\n", + "\n", + "A neat trick, which is used by all the cool kids these days, is to write\n", + "classes that allow *method chaining* - writing one line of code which\n", + "calls more than one method on an object, e.g.:\n", + "\n", + "> ```\n", + "> fm = FSLMaths(img)\n", + "> result = fm.add(1).mul(10).run()\n", + "> ```\n", + "\n", + "Adding this feature to our budding `FSLMaths` class is easy - all we have\n", + "to do is return `self` from each method:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import nibabel as nib\n", + "\n", + "class FSLMaths(object):\n", + "\n", + " def __init__(self, inimg):\n", + " self.img = inimg\n", + " self.operations = []\n", + "\n", + " def add(self, value):\n", + " self.operations.append(('add', value))\n", + " return self\n", + "\n", + " def mul(self, value):\n", + " self.operations.append(('mul', value))\n", + " return self\n", + "\n", + " def div(self, value):\n", + " self.operations.append(('div', value))\n", + " return self\n", + "\n", + " def run(self, output=None):\n", + "\n", + " data = np.array(self.img.get_data())\n", + "\n", + " for oper, value in self.operations:\n", + "\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", + " if oper == 'add':\n", + " data = data + value\n", + " elif oper == 'mul':\n", + " data = data * value\n", + " elif oper == 'div':\n", + " data = data / value\n", + "\n", + " # turn final output into a nifti,\n", + " # and save it to disk if an\n", + " # 'output' has been specified.\n", + " outimg = nib.nifti1.Nifti1Image(data, inimg.affine)\n", + "\n", + " if output is not None:\n", + " nib.save(outimg, output)\n", + "\n", + " return outimg" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we can chain all of our method calls, and even the creation of our\n", + "`FSLMaths` object, into a single line:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "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", + "outimg = FSLMaths(inimg).mul(mask).add(-10).run()\n", + "\n", + "norigvox = (inimg .get_data() > 0).sum()\n", + "nmaskvox = (outimg.get_data() > 0).sum()\n", + "\n", + "print('Number of voxels >0 in original image: {}'.format(norigvox))\n", + "print('Number of voxels >0 in masked image: {}'.format(nmaskvox))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> In fact, this is precisely how the\n", + "> [`fsl.wrappers.fslmaths`](https://users.fmrib.ox.ac.uk/~paulmc/fsleyes/fslpy/latest/fsl.wrappers.fslmaths.html)\n", + "> function works.\n", + "\n", + "\n", "<a class=\"anchor\" id=\"protecting-attribute-access\"></a>\n", "## Protecting attribute access\n", "\n", @@ -606,9 +717,8 @@ "notion of a private attribute or method is strictly enforced by the language.\n", "\n", "\n", - "However, there are a couple of conventions in Python that are [universally\n", - "adhered\n", - "to](https://docs.python.org/3.5/tutorial/classes.html#private-variables):\n", + "However, there are a couple of conventions in Python that are\n", + "[universally adhered to](https://docs.python.org/3/tutorial/classes.html#private-variables):\n", "\n", "* Class-level attributes and methods, and module-level attributes, functions,\n", " and classes, which begin with a single underscore (`_`), should be\n", @@ -622,14 +732,13 @@ " 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", - " [mangled\n", - " name](https://docs.python.org/3.5/tutorial/classes.html#private-variables)\n", + " [mangled name](https://docs.python.org/3/tutorial/classes.html#private-variables)\n", " though).\n", "\n", "\n", "> <sup>2</sup> With the exception that module-level fields which begin with a\n", "> single underscore will not be imported into the local scope via the\n", - "> `from [module] import *` techinque.\n", + "> `from [module] import *` technique.\n", "\n", "\n", "So with all of this in mind, we can adjust our `FSLMaths` class to discourage\n", @@ -676,7 +785,7 @@ "\n", "\n", "Python has a feature called\n", - "[`properties`](https://docs.python.org/3.5/library/functions.html#property),\n", + "[`properties`](https://docs.python.org/3/library/functions.html#property),\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", @@ -851,17 +960,17 @@ "metadata": {}, "source": [ "Hopefully this example doesn't need much in the way of explanation - this\n", - "collection of classes captures a hierarchical relationship which exists in the\n", - "real world (and also captures the inherently annoying nature of\n", + "collection of classes represents a hierarchical relationship which exists in\n", + "the real world (and also represents the inherently annoying nature of\n", "chihuahuas). For example, in the real world, all dogs are animals, but not all\n", "animals are dogs. Therefore in our model, the `Dog` class has specified\n", - "`Animal` as its base class. We say that the `Dog` class _extends_, _derives\n", - "from_, or _inherits from_, the `Animal` class, and that all `Dog` instances\n", + "`Animal` as its base class. We say that the `Dog` class *extends*, *derives\n", + "from*, or *inherits from*, the `Animal` class, and that all `Dog` instances\n", "are also `Animal` instances (but not vice-versa).\n", "\n", "\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", + "on the `Animal` class, but it has been re-implemented, or *overridden* in the\n", "`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", @@ -1018,7 +1127,7 @@ "\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", + "method](https://docs.python.org/3/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](multiple-inheritance)).\n", "\n", @@ -1136,8 +1245,8 @@ "### Polymorphism\n", "\n", "\n", - "Inheritance also allows us to take advantage of _polymorphism_, which refers\n", - "to idea that, in an object-oriented language, we should be able to use an\n", + "Inheritance also allows us to take advantage of *polymorphism*, which refers\n", + "to the 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 will work on an instance of any `Operator`\n", @@ -1371,12 +1480,15 @@ "\n", " def add(self, value):\n", " self.operations.append(('add', value))\n", + " return self\n", "\n", " def mul(self, value):\n", " self.operations.append(('mul', value))\n", + " return self\n", "\n", " def div(self, value):\n", " self.operations.append(('div', value))\n", + " return self\n", "\n", " def run(self, output=None):\n", "\n", @@ -1386,12 +1498,14 @@ "\n", " # Code omitted for brevity\n", "\n", - " # Increment the usage counter\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" + " # Increment the usage counter for this operation. We can\n", + " # access class attributes (and methods) through the class\n", + " # itself, as shown here.\n", + " FSLMaths.opCounters[oper] = FSLMaths.opCounters.get(oper, 0) + 1\n", + "\n", + " # It is also possible to access class-level\n", + " # attributes via instances of the class, e.g.\n", + " # self.opCounters[oper] = self.opCounters.get(oper, 0) + 1\n" ] }, { @@ -1412,17 +1526,8 @@ "inimg = nib.load(fpath)\n", "mask = nib.load(fmask)\n", "\n", - "fm1 = FSLMaths(inimg)\n", - "fm2 = FSLMaths(inimg)\n", - "\n", - "fm1.mul(mask)\n", - "fm1.add(15)\n", - "\n", - "fm2.add(25)\n", - "fm1.div(1.5)\n", - "\n", - "fm1.run()\n", - "fm2.run()\n", + "FSLMaths(inimg).mul(mask).add(25).run()\n", + "FSLMaths(inimg).add(15).div(1.5).run()\n", "\n", "print('FSLMaths usage statistics')\n", "for oper in ('add', 'div', 'mul'):\n", @@ -1471,12 +1576,15 @@ "\n", " def add(self, value):\n", " self.operations.append(('add', value))\n", + " return self\n", "\n", " def mul(self, value):\n", " self.operations.append(('mul', value))\n", + " return self\n", "\n", " def div(self, value):\n", " self.operations.append(('div', value))\n", + " return self\n", "\n", " def run(self, output=None):\n", "\n", @@ -1493,11 +1601,11 @@ "> 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", + "> 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:" + "Calling a class method is the same as accessing a class attribute:" ] }, { @@ -1511,14 +1619,8 @@ "inimg = nib.load(fpath)\n", "mask = nib.load(fmask)\n", "\n", - "fm1 = FSLMaths(inimg)\n", - "fm2 = FSLMaths(inimg)\n", - "\n", - "fm1.mul(mask)\n", - "fm1.add(15)\n", - "\n", - "fm2.add(25)\n", - "fm1.div(1.5)\n", + "fm1 = FSLMaths(inimg).mul(mask).add(25)\n", + "fm2 = FSLMaths(inimg).add(15).div(1.5)\n", "\n", "fm1.run()\n", "fm2.run()\n", @@ -1590,9 +1692,9 @@ "## Appendix: `__init__` versus `__new__`\n", "\n", "\n", - "In Python, object creation is actually a two-stage process - _creation_, and\n", - "then _initialisation_. The `__init__` method gets called during the\n", - "_initialisation_ stage - its job is to initialise the state of the object. But\n", + "In Python, object creation is actually a two-stage process - *creation*, and\n", + "then *initialisation*. The `__init__` method gets called during the\n", + "*initialisation* stage - its job is to initialise the state of the object. But\n", "note that, by the time `__init__` gets called, the object has already been\n", "created.\n", "\n", @@ -1610,7 +1712,7 @@ "the difference between `__new__` and `__init__` can be found\n", "[here](https://www.reddit.com/r/learnpython/comments/2s3pms/what_is_the_difference_between_init_and_new/cnm186z/),\n", "and you may also wish to take a look at the [official Python\n", - "docs](https://docs.python.org/3.5/reference/datamodel.html#basic-customization).\n", + "docs](https://docs.python.org/3/reference/datamodel.html#basic-customization).\n", "\n", "\n", "<a class=\"anchor\" id=\"appendix-monkey-patching\"></a>\n", @@ -1618,24 +1720,24 @@ "\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", + "to as [*monkey-patching*](https://en.wikipedia.org/wiki/Monkey_patch) and,\n", "whilst it is allowed by the Python programming language, it is generally\n", "considered quite bad practice.\n", "\n", "\n", - "Just because you _can_ do something doesn't mean that you _should_. Python\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", + "suitable. **But** if you want to write software that will be used, adopted,\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", + "**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", + "(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 in Python](https://docs.python.org/3.5/library/unittest.mock.html).\n", + "breeze in Python](https://docs.python.org/3/library/unittest.mock.html).\n", "\n", "\n", "As another example, consider the scenario where you are dependent on a third\n", @@ -1703,7 +1805,7 @@ "metadata": {}, "source": [ "> <sup>4</sup>Another option is the [`functools.singledispatch`\n", - "> decorator](https://docs.python.org/3.5/library/functools.html#functools.singledispatch),\n", + "> decorator](https://docs.python.org/3/library/functools.html#functools.singledispatch),\n", "> which is more complicated, but may allow you to write your dispatch logic in\n", "> a more concise manner.\n", "\n", @@ -1716,8 +1818,8 @@ "workings of classes and objects, so these pages are worth a read:\n", "\n", "\n", - "* https://docs.python.org/3.5/tutorial/classes.html\n", - "* https://docs.python.org/3.5/reference/datamodel.html" + "* https://docs.python.org/3/tutorial/classes.html\n", + "* https://docs.python.org/3/reference/datamodel.html" ] } ], diff --git a/advanced_topics/03_object_oriented_programming.md b/advanced_topics/03_object_oriented_programming.md index 5ffbfc7c63b3be4b0704a717852a49bdffc1efd7..21ec9a676ead32d7d8f6577e7f91f141f9437d49 100644 --- a/advanced_topics/03_object_oriented_programming.md +++ b/advanced_topics/03_object_oriented_programming.md @@ -19,6 +19,7 @@ you use an object-oriented approach. * [We didn't specify the `self` argument - what gives?!?](#we-didnt-specify-the-self-argument) * [Attributes](#attributes) * [Methods](#methods) +* [Method chaining](#method-chaining) * [Protecting attribute access](#protecting-attribute-access) * [A better way - properties](#a-better-way-properties]) * [Inheritance](#inheritance) @@ -46,8 +47,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 @@ -66,8 +67,8 @@ layout of a chunk of memory. For example, here is a typical struct definition: > ``` -Now, an _object_ is not a definition, but rather a thing which resides in -memory. An object can have _attributes_ (pieces of information), and _methods_ +Now, an *object* is not a definition, but rather a thing which resides in +memory. An object can have *attributes* (pieces of information), and *methods* (functions associated with the object). You can pass objects around your code, manipulate their attributes, and call their methods. @@ -92,12 +93,12 @@ you create an object from that class. Of course there are many more differences between C structs and classes (most notably [inheritance](todo), [polymorphism](todo), and [access protection](todo)). But if you can understand the difference between a -_definition_ of a C struct, and an _instantiation_ of that struct, then you -are most of the way towards understanding the difference between a _class_, -and an _object_. +*definition* of a C struct, and an *instantiation* of that struct, then you +are most of the way towards understanding the difference between a *class*, +and an *object*. -> But just to confuse you, remember that in Python, __everything__ is an +> But just to confuse you, remember that in Python, **everything** is an > object - even classes! @@ -206,7 +207,7 @@ print(fm) Refer to the [official -docs](https://docs.python.org/3.5/reference/datamodel.html#special-method-names) +docs](https://docs.python.org/3/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](appendix-init-versus-new). @@ -352,8 +353,8 @@ append a tuple to that `operations` list. The idea behind this design is that our `FSLMaths` class will not actually do anything when we call the `add`, `mul` or `div` methods. Instead, it will -"stage" each operation, and then perform them all in one go. So let's add -another method, `run`, which actually does the work: +*stage* each operation, and then perform them all in one go at a later point +in time. So let's add another method, `run`, which actually does the work: ``` @@ -387,7 +388,6 @@ class FSLMaths(object): if isinstance(value, nib.nifti1.Nifti1Image): value = value.get_data() - if oper == 'add': data = data + value elif oper == 'mul': @@ -430,6 +430,99 @@ print('Number of voxels >0 in masked image: {}'.format(nmaskvox)) ``` +<a class="anchor" id="method-chaining"></a> +## Method chaining + + +A neat trick, which is used by all the cool kids these days, is to write +classes that allow *method chaining* - writing one line of code which +calls more than one method on an object, e.g.: + +> ``` +> fm = FSLMaths(img) +> result = fm.add(1).mul(10).run() +> ``` + +Adding this feature to our budding `FSLMaths` class is easy - all we have +to do is return `self` from each method: + +``` +import numpy as np +import nibabel as nib + +class FSLMaths(object): + + def __init__(self, inimg): + self.img = inimg + self.operations = [] + + def add(self, value): + self.operations.append(('add', value)) + return self + + def mul(self, value): + self.operations.append(('mul', value)) + return self + + def div(self, value): + self.operations.append(('div', value)) + return self + + def run(self, output=None): + + data = np.array(self.img.get_data()) + + for oper, value in self.operations: + + # 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() + + if oper == 'add': + data = data + value + elif oper == 'mul': + data = data * value + elif oper == 'div': + data = data / value + + # turn final output into a nifti, + # and save it to disk if an + # 'output' has been specified. + outimg = nib.nifti1.Nifti1Image(data, inimg.affine) + + if output is not None: + nib.save(outimg, output) + + return outimg +``` + + +Now we can chain all of our method calls, and even the creation of our +`FSLMaths` object, into a single line: + + +``` +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) + +outimg = FSLMaths(inimg).mul(mask).add(-10).run() + +norigvox = (inimg .get_data() > 0).sum() +nmaskvox = (outimg.get_data() > 0).sum() + +print('Number of voxels >0 in original image: {}'.format(norigvox)) +print('Number of voxels >0 in masked image: {}'.format(nmaskvox)) +``` + +> In fact, this is precisely how the +> [`fsl.wrappers.fslmaths`](https://users.fmrib.ox.ac.uk/~paulmc/fsleyes/fslpy/latest/fsl.wrappers.fslmaths.html) +> function works. + + <a class="anchor" id="protecting-attribute-access"></a> ## Protecting attribute access @@ -488,9 +581,8 @@ of an object. This is in contrast to languages like C++ and Java, where the notion of a private attribute or method is strictly enforced by the language. -However, there are a couple of conventions in Python that are [universally -adhered -to](https://docs.python.org/3.5/tutorial/classes.html#private-variables): +However, there are a couple of conventions in Python that are +[universally adhered to](https://docs.python.org/3/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 @@ -504,14 +596,13 @@ to](https://docs.python.org/3.5/tutorial/classes.html#private-variables): 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 - [mangled - name](https://docs.python.org/3.5/tutorial/classes.html#private-variables) + [mangled name](https://docs.python.org/3/tutorial/classes.html#private-variables) though). > <sup>2</sup> With the exception that module-level fields which begin with a > single underscore will not be imported into the local scope via the -> `from [module] import *` techinque. +> `from [module] import *` technique. So with all of this in mind, we can adjust our `FSLMaths` class to discourage @@ -541,7 +632,7 @@ print(fm.__img) Python has a feature called -[`properties`](https://docs.python.org/3.5/library/functions.html#property), +[`properties`](https://docs.python.org/3/library/functions.html#property), 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 @@ -676,17 +767,17 @@ class Chihuahua(Dog): Hopefully this example doesn't need much in the way of explanation - this -collection of classes captures a hierarchical relationship which exists in the -real world (and also captures the inherently annoying nature of +collection of classes represents a hierarchical relationship which exists in +the real world (and also represents the inherently annoying nature of chihuahuas). For example, in the real world, all dogs are animals, but not all animals are dogs. Therefore in our model, the `Dog` class has specified -`Animal` as its base class. We say that the `Dog` class _extends_, _derives -from_, or _inherits from_, the `Animal` class, and that all `Dog` instances +`Animal` as its base class. We say that the `Dog` class *extends*, *derives +from*, or *inherits from*, the `Animal` class, and that all `Dog` instances 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 +on the `Animal` class, but it has been re-implemented, or *overridden* in the `Dog`, [`TalkingDog`](https://twitter.com/simpsonsqotd/status/427941665836630016?lang=en), `Cat`, and `Chihuahua` classes (but not on the `Labrador` class). We can call @@ -819,7 +910,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 +method](https://docs.python.org/3/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](multiple-inheritance)). @@ -920,8 +1011,8 @@ print(so.run('python is an ok language')) ### Polymorphism -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 +Inheritance also allows us to take advantage of *polymorphism*, which refers +to the 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` @@ -1110,12 +1201,15 @@ class FSLMaths(object): def add(self, value): self.operations.append(('add', value)) + return self def mul(self, value): self.operations.append(('mul', value)) + return self def div(self, value): self.operations.append(('div', value)) + return self def run(self, output=None): @@ -1125,12 +1219,15 @@ class FSLMaths(object): # Code omitted for brevity - # Increment the usage counter - # for this operation. We can - # access class attributes (and - # methods) through the class - # itself. - FSLMaths.opCounters[oper] = self.opCounters.get(oper, 0) + 1 + # Increment the usage counter for this operation. We can + # access class attributes (and methods) through the class + # itself, as shown here. + FSLMaths.opCounters[oper] = FSLMaths.opCounters.get(oper, 0) + 1 + + # It is also possible to access class-level + # attributes via instances of the class, e.g. + # self.opCounters[oper] = self.opCounters.get(oper, 0) + 1 + ``` @@ -1143,17 +1240,8 @@ 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) - -fm1.mul(mask) -fm1.add(15) - -fm2.add(25) -fm1.div(1.5) - -fm1.run() -fm2.run() +FSLMaths(inimg).mul(mask).add(25).run() +FSLMaths(inimg).add(15).div(1.5).run() print('FSLMaths usage statistics') for oper in ('add', 'div', 'mul'): @@ -1194,12 +1282,15 @@ class FSLMaths(object): def add(self, value): self.operations.append(('add', value)) + return self def mul(self, value): self.operations.append(('mul', value)) + return self def div(self, value): self.operations.append(('div', value)) + return self def run(self, output=None): @@ -1213,11 +1304,11 @@ class FSLMaths(object): > 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_ +> 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: +Calling a class method is the same as accessing a class attribute: ``` @@ -1226,14 +1317,8 @@ 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) - -fm1.mul(mask) -fm1.add(15) - -fm2.add(25) -fm1.div(1.5) +fm1 = FSLMaths(inimg).mul(mask).add(25) +fm2 = FSLMaths(inimg).add(15).div(1.5) fm1.run() fm2.run() @@ -1294,9 +1379,9 @@ always use the new-style format. ## Appendix: `__init__` versus `__new__` -In Python, object creation is actually a two-stage process - _creation_, and -then _initialisation_. The `__init__` method gets called during the -_initialisation_ stage - its job is to initialise the state of the object. But +In Python, object creation is actually a two-stage process - *creation*, and +then *initialisation*. The `__init__` method gets called during the +*initialisation* stage - its job is to initialise the state of the object. But note that, by the time `__init__` gets called, the object has already been created. @@ -1314,7 +1399,7 @@ A brief explanation on the difference between `__new__` and `__init__` can be found [here](https://www.reddit.com/r/learnpython/comments/2s3pms/what_is_the_difference_between_init_and_new/cnm186z/), 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). +docs](https://docs.python.org/3/reference/datamodel.html#basic-customization). <a class="anchor" id="appendix-monkey-patching"></a> @@ -1322,24 +1407,24 @@ docs](https://docs.python.org/3.5/reference/datamodel.html#basic-customization). 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, +to as [*monkey-patching*](https://en.wikipedia.org/wiki/Monkey_patch) and, whilst it is allowed by the Python programming language, it is generally considered quite bad practice. -Just because you _can_ do something doesn't mean that you _should_. Python +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, +suitable. **But** if you want to write software that will be used, adopted, 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 +**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 +(and it is horrific in many cases), it can be *extremely* useful in certain circumstances. For instance, monkey-patching makes [unit testing a -breeze in Python](https://docs.python.org/3.5/library/unittest.mock.html). +breeze in Python](https://docs.python.org/3/library/unittest.mock.html). As another example, consider the scenario where you are dependent on a third @@ -1398,7 +1483,7 @@ print('Add four: {}'.format(a.add(1, 2, 3, 4))) ``` > <sup>4</sup>Another option is the [`functools.singledispatch` -> decorator](https://docs.python.org/3.5/library/functools.html#functools.singledispatch), +> decorator](https://docs.python.org/3/library/functools.html#functools.singledispatch), > which is more complicated, but may allow you to write your dispatch logic in > a more concise manner. @@ -1411,5 +1496,5 @@ 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/tutorial/classes.html -* https://docs.python.org/3.5/reference/datamodel.html +* https://docs.python.org/3/tutorial/classes.html +* https://docs.python.org/3/reference/datamodel.html