Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • fsl/pytreat-practicals-2020
  • mchiew/pytreat-practicals-2020
  • ndcn0236/pytreat-practicals-2020
  • nichols/pytreat-practicals-2020
4 results
Show changes
Commits on Source (268)
Showing
with 519 additions and 222 deletions
# 2018 WIN PyTreat
# 2020 WIN PyTreat
This repository contains Jupyter notebooks and data for the 2018 WIN PyTreat.
It contains two sets of practicals:
This repository contains Jupyter notebooks and data for the 2020 WIN PyTreat.
It contains the following:
- The `talks` directory contains some (but not all) of the _Topyc_ talks that
will be given throughout the week.
- The `getting_started` directory contains a series of practicals intended
for those of you who are new to the Python programming language, or need
......@@ -14,43 +17,39 @@ It contains two sets of practicals:
about the language.
These practicals have been written under the assumption that FSL 5.0.10 is
The practicals have been written under the assumption that FSL 6.0.3 is
installed.
## For attendees
To run these notebooks in the `fslpython` environment, you must first install
jupyter:
These notebooks can be run in the `fslpython` environment using:
```
# If your FSL installation requires administrative privileges to modify, then
# you MUST run these commands as root - don't just prefix each individual
# command with sudo, or you will probably install jupyter into the wrong
# location!
#
# One further complication - once you have become root, $FSLDIR may not be set,
# so either set it as we have ione below, or make sure that it is set, before
# proceeding.
sudo su
export FSLDIR=/usr/local/fsl
source $FSLDIR/fslpython/bin/activate fslpython
conda install jupyter
source deactivate
ln -s $FSLDIR/fslpython/envs/fslpython/bin/jupyter $FSLDIR/bin/fsljupyter
git clone https://git.fmrib.ox.ac.uk/fsl/pytreat-practicals-2020.git
cd pytreat-practicals-2020
fslpython -m notebook
```
A page should open in your web browser - to access the practicals, navigate
into one of the `getting_started` or `advanced_topics` directories, and click
on the `.ipynb` file you are interested in. Some of the talks are also
presented in notebook form - navigate to the talk you are interested in
(within the `talks` directory), and click on the `.ipynb` file to follow
along.
Then, clone this repository on your local machine, and run
`fsljupyter notebook`:
Throughout the week we might make changes to this repository. When this
happens, we will ask you to update your local clone of the repository with the
following command:
```
git clone git@git.fmrib.ox.ac.uk:fsl/pytreat-2018-practicals.git
cd pytreat-2018-practicals
fsljupyter notebook
git stash save
git pull origin master
git stash pop
```
......@@ -60,52 +59,127 @@ Have fun!
## For contributors
The upstream repository can be found at:
The main repository can be found at:
https://git.fmrib.ox.ac.uk/fsl/pytreat-practicals-2020
Updates to the master branch should occur via merge requests. You can choose
to either work on a branch within this repository (recommended), or on a fork of this
repository (advanced).
### Using a branch within this repository (recommended)
1. Make a local clone of the repository:
```
git clone https://git.fmrib.ox.ac.uk/fsl/pytreat-practicals-2020.git
```
2. Create a branch for your work:
```
git checkout -b my_cool_branch origin/master
```
3. Make your changes on this branch.
```
git add <my_new_and_changed_files>
git commit -m 'super cool updates'
```
4. Push your changes to the gitlab repository:
```
git push origin my_cool_branch
```
https://git.fmrib.ox.ac.uk/fsl/pytreat-2018-practicals
5. In gitlab, submit a merge request from your branch onto the master
branch.
https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html
To contribute to the practicals:
### Using a fork of this repository (advanced)
1. Fork the upstream repository on gitlab
2. Make a local clone of your fork:
```
git clone git@git.fmrib.ox.ac.uk:<username>/pytreat-2018-practicals
git clone https://git.fmrib.ox.ac.uk/<your_username>/pytreat-practicals-2020.git
```
3. Add the upstream repository as a remote:
```
git remote add upstream git@git.fmrib.ox.ac.uk:fsl/pytreat-2018-practicals.git
git remote add upstream https://git.fmrib.ox.ac.uk/fsl/pytreat-practicals-2020.git
```
4. Make your changes on your local repository
5. Rebase onto the upstream repository, and push your changes to your fork:
```
git add <my_new_and_changed_files>
git commit -m 'super cool updates'
```
5. Push your changes to your fork:
```
git fetch --all
git rebase upstream/master
git push --force origin master
git push origin master
```
6. In gitlab, submit a merge request from your fork back to the upstream
repository.
https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html
### Updating your local repository
To bring in the changes that other people have contributed to the main
repository into your local repository:
```
git fetch --all
```
Then, do this if you are working on a branch within the main repository:
```
# make sure you are on the correct local branch:
git checkout my_cool_branch
git merge origin/master
```
Or, do this if you are working on a fork of the main repository:
```
git checkout master
git merge upstream/master
```
> Or, if you are comfortable with git, `rebase` is so much cooler:
>
> ```
> git fetch --all
>
> # replace <branch_name> with your local branch name
> git checkout <remote_name>/master
>
> # replace <remote_name> with the main repository name
> git rebase <remote_name>/master
> ```
### Writing your talk as a Jupyter notebook
When you install `jupyter` above, you may also wish to install
[`notedown`](https://github.com/aaren/notedown):
You may wish to install [`notedown`](https://github.com/aaren/notedown):
```
# .
# see instructions above
# .
conda install jupyter
pip install notedown
source deactivate
ln -s $FSLDIR/fslpython/envs/fslpython/bin/jupyter $FSLDIR/bin/fsljupyter
$FSLDIR/fslpython/bin/conda install -n fslpython -c conda-forge notedown
ln -s $FSLDIR/fslpython/envs/fslpython/bin/notedown $FSLDIR/bin/fslnotedown
```
......
%% Cell type:markdown id: tags:
# Modules and packages
Python gives you a lot of flexibility in how you organise your code. If you
want, you can write a Python program just as you would write a Bash script.
You don't _have_ to use functions, classes, modules or packages if you don't
want to, or if the script's task does not require them.
want to, or if the script task does not require them.
But when your code starts to grow beyond what can reasonably be defined in a
single file, you will (hopefully) want to start arranging it in a more
understandable manner.
For this practical we have prepared a handful of example files - you can find
them alongside this notebook file, in a directory called
`modules_and_packages/`.
`02_modules_and_packages/`.
## Contents
* [What is a module?](#what-is-a-module)
* [Importing modules](#importing-modules)
* [Importing specific items from a module](#importing-specific-items-from-a-module)
* [Importing everything from a module](#importing-everything-from-a-module)
* [Module aliases](#module-aliases)
* [What happens when I import a module?](#what-happens-when-i-import-a-module)
* [How can I make my own modules importable?](#how-can-i-make-my-own-modules-importable)
* [Modules versus scripts](#modules-versus-scripts)
* [What is a package?](#what-is-a-package)
* [`__init__.py`](#init-py)
* [Useful references](#useful-references)
%% Cell type:code id: tags:
```
import os
os.chdir('modules_and_packages')
os.chdir('02_modules_and_packages')
```
%% Cell type:markdown id: tags:
<a class="anchor" id="what-is-a-module"></a>
## What is a module?
Any file ending with `.py` is considered to be a module in Python. Take a look
at `modules_and_packages/numfuncs.py` - either open it in your editor, or run
this code block:
at `02_modules_and_packages/numfuncs.py` - either open it in your editor, or
run this code block:
%% Cell type:code id: tags:
```
with open('numfuncs.py', 'rt') as f:
for line in f:
print(line.rstrip())
```
%% Cell type:markdown id: tags:
This is a perfectly valid Python module, although not a particularly useful
one. It contains an attribute called `PI`, and a function `add`.
<a class="anchor" id="importing-modules"></a>
## Importing modules
Before we can use our module, we must `import` it. Importing a module in
Python will make its contents available to the local scope. We can import the
contents of `mymodule` like so:
Python will make its contents available in the local scope. We can import the
contents of `numfuncs` like so:
%% Cell type:code id: tags:
```
import numfuncs
```
%% Cell type:markdown id: tags:
This imports `numfuncs` into the local scope - everything defined in the
`numfuncs` module can be accessed by prefixing it with `numfuncs.`:
%% Cell type:code id: tags:
```
print('PI:', numfuncs.PI)
print(numfuncs.add(1, 50))
```
%% Cell type:markdown id: tags:
There are a couple of other ways to import items from a module...
<a class="anchor" id="importing-specific-items-from-a-module"></a>
### Importing specific items from a module
If you only want to use one, or a few items from a module, you can import just
those items - a reference to just those items will be created in the local
scope:
%% Cell type:code id: tags:
```
from numfuncs import add
print(add(1, 3))
```
%% Cell type:markdown id: tags:
<a class="anchor" id="importing-everything-from-a-module"></a>
### Importing everything from a module
It is possible to import _everything_ that is defined in a module like so:
%% Cell type:code id: tags:
```
from numfuncs import *
print('PI: ', PI)
print(add(1, 5))
```
%% Cell type:markdown id: tags:
__PLEASE DON'T DO THIS!__ Because every time you do, somewhere in the world, a
software developer will will spontaneously stub his/her toe, and start crying.
software developer will spontaneously stub his/her toe, and start crying.
Using this approach can make more complicated programs very difficult to read,
because it is not possible to determine the origin of the functions and
attributes that are being used.
And naming collisions are inevitable when importing multiple modules in this
way, making it very difficult for somebody else to figure out what your code
is doing:
%% Cell type:code id: tags:
```
from numfuncs import *
from strfuncs import *
print(add(1, 5))
```
%% Cell type:markdown id: tags:
Instead, it is better to give modules a name when you import them. While this
requires you to type more code, the benefits of doing this far outweigh the
hassle of typing a few extra characters - it becomes much easier to read and
trace through code when the functions you use are accessed through a namespace
for each module:
%% Cell type:code id: tags:
```
import numfuncs
import strfuncs
print('number add: ', numfuncs.add(1, 2))
print('string add: ', strfuncs.add(1, 2))
```
%% Cell type:markdown id: tags:
<a class="anchor" id="module-aliases"></a>
### Module aliases
And Python allows you to define an _alias_ for a module when you import it,
so you don't necessarily need to type out the full module name each time
you want to access something inside:
%% Cell type:code id: tags:
```
import numfuncs as nf
import strfuncs as sf
print('number add: ', nf.add(1, 2))
print('string add: ', sf.add(1, 2))
```
%% Cell type:markdown id: tags:
You have already seen this in the earlier practicals - here are a few
aliases which have become a de-facto standard for commonly used Python
modules:
%% Cell type:code id: tags:
```
import os.path as op
import numpy as np
import nibabel as nib
import matplotlib as mpl
import matplotlib.pyplot as plt
```
%% Cell type:markdown id: tags:
<a class="anchor" id="what-happens-when-i-import-a-module"></a>
### What happens when I import a module?
When you `import` a module, the contents of the module file are literally
executed by the Python runtime, exactly the same as if you had typed its
contents into `ipython`. Any attributes, functions, or classes which are
defined in the module will be bundled up into an object that represents the
module, and through which you can access the module's contents.
When we typed `import numfuncs` in the examples above, the following events
occurred:
1. Python created a `module` object to represent the module.
2. The `numfuncs.py` file was read and executed, and all of the items defined
inside `numfuncs.py` (i.e. the `PI` attribute and the `add` function) were
added to the `module` object.
3. A local variable called `numfuncs`, pointing to the `module` object,
was added to the local scope.
Because module files are literally executed on import, any statements in the
module file which are not encapsulated inside a class or function will be
executed. As an example, take a look at the file `sideeffects.py`. Let's
import it and see what happens:
%% Cell type:code id: tags:
```
import sideeffects
```
%% Cell type:markdown id: tags:
Ok, hopefully that wasn't too much of a surprise. Something which may be less
intuitive, however, is that a module's contents will only be executed on the
_first_ time that it is imported. After the first import, Python caches the
module's contents (all loaded modules are accessible through
[`sys.modules`](https://docs.python.org/3.5/library/sys.html#sys.modules)). On
subsequent imports, the cached version of the module is returned:
%% Cell type:code id: tags:
```
import sideeffects
```
%% Cell type:markdown id: tags:
<a class="anchor" id="how-can-i-make-my-own-modules-importable"></a>
### How can I make my own modules importable?
When you `import` a module, Python searches for it in the following locations,
in the following order:
1. Built-in modules (e.g. `os`, `sys`, etc.).
2. In the current directory or, if a script has been executed, in the directory
containing that script.
3. In directories listed in the PYTHONPATH environment variable.
3. In directories listed in the `$PYTHONPATH` environment variable.
4. In installed third-party libraries (e.g. `numpy`).
If you are experimenting or developing your program, the quickest and easiest
way to make your module(s) importable is to add their containing directory to
the `PYTHONPATH`. But if you are developing a larger piece of software, you
should probably organise your modules into _packages_, which are [described
should probably organise your modules into *packages*, which are [described
below](#what-is-a-package).
<a class="anchor" id="modules-versus-scripts"></a>
## Modules versus scripts
You now know that Python treats all files ending in `.py` as importable
modules. But all files ending in `.py` can also be treated as scripts. In
fact, there no difference between a _module_ and a _script_ - any `.py` file
can be executed as a script, or imported as a module, or both.
Have a look at the file `modules_and_packages/module_and_script.py`:
Have a look at the file `02_modules_and_packages/module_and_script.py`:
%% Cell type:code id: tags:
```
with open('module_and_script.py', 'rt') as f:
for line in f:
print(line.rstrip())
```
%% Cell type:markdown id: tags:
This file contains two functions `mul` and `main`. The
`if __name__ == '__main__':` clause at the bottom is a standard trick in Python
that allows you to add code to a file that is _only executed when the module is
called as a script_. Try it in a terminal now:
> `python modules_and_packages/module_and_script.py`
> `python 02_modules_and_packages/module_and_script.py`
But if we `import` this module from another file, or from an interactive
session, the code within the `if __name__ == '__main__':` clause will not be
executed, and we can access its functions just like any other module that we
import.
%% Cell type:code id: tags:
```
import module_and_script as mas
a = 1.5
b = 3
print('mul({}, {}): {}'.format(a, b, mas.mul(a, b)))
print('calling main...')
mas.main([str(a), str(b)])
```
%% Cell type:markdown id: tags:
<a class="anchor" id="what-is-a-package"></a>
## What is a package?
You now know how to split your Python code up into separate files
(a.k.a. _modules_). When your code grows beyond a handful of files, you may
(a.k.a. *modules*). When your code grows beyond a handful of files, you may
wish for more fine-grained control over the namespaces in which your modules
live. Python has another feature which allows you to organise your modules
into _packages_.
into *packages*.
A package in Python is simply a directory which:
* Contains a special file called `__init__.py`
* May contain one or more module files (any other files ending in `*.py`)
* May contain other package directories.
For example, the [FSLeyes](https://git.fmrib.ox.ac.uk/fsl/fsleyes/fsleyes)
code is organised into packages and sub-packages as follows (abridged):
> ```
> fsleyes/
> __init__.py
> main.py
> frame.py
> views/
> __init__.py
> orthopanel.py
> lightboxpanel.py
> controls/
> __init__.py
> locationpanel.py
> overlaylistpanel.py
> ```
Within a package structure, we will typically still import modules directly,
via their full path within the package:
%% Cell type:code id: tags:
```
import fsleyes.main as fmain
fmain.fsleyes_main()
```
%% Cell type:markdown id: tags:
<a class="anchor" id="init-py"></a>
### `__init__.py`
Every Python package must have an `__init__.py` file. In many cases, this will
actually be an empty file, and you don't need to worry about it any more, apart
from knowing that it is needed. But you can use `__init__.py` to perform some
package-specific initialisation, and/or to customise the package's namespace.
As an example, take a look the `modules_and_packages/fsleyes/__init__.py` file
in our mock FSLeyes package. We have imported the `fsleyes_main` function from
the `fsleyes.main` module, making it available at the package level. So
As an example, take a look the `02_modules_and_packages/fsleyes/__init__.py`
file in our mock FSLeyes package. We have imported the `fsleyes_main` function
from the `fsleyes.main` module, making it available at the package level. So
instead of importing the `fsleyes.main` module, we could instead just import
the `fsleyes` package:
%% Cell type:code id: tags:
```
import fsleyes
fsleyes.fsleyes_main()
```
%% Cell type:markdown id: tags:
<a class="anchor" id="useful-references"></a>
## Useful references
* [Modules and packages in Python](https://docs.python.org/3.5/tutorial/modules.html)
* [Modules and packages in Python](https://docs.python.org/3/tutorial/modules.html)
* [Using `__init__.py`](http://mikegrouchy.com/blog/2012/05/be-pythonic-__init__py.html)
......
......@@ -4,7 +4,7 @@
Python gives you a lot of flexibility in how you organise your code. If you
want, you can write a Python program just as you would write a Bash script.
You don't _have_ to use functions, classes, modules or packages if you don't
want to, or if the script's task does not require them.
want to, or if the script task does not require them.
But when your code starts to grow beyond what can reasonably be defined in a
......@@ -14,7 +14,7 @@ understandable manner.
For this practical we have prepared a handful of example files - you can find
them alongside this notebook file, in a directory called
`modules_and_packages/`.
`02_modules_and_packages/`.
## Contents
......@@ -33,7 +33,7 @@ them alongside this notebook file, in a directory called
```
import os
os.chdir('modules_and_packages')
os.chdir('02_modules_and_packages')
```
<a class="anchor" id="what-is-a-module"></a>
......@@ -41,8 +41,8 @@ os.chdir('modules_and_packages')
Any file ending with `.py` is considered to be a module in Python. Take a look
at `modules_and_packages/numfuncs.py` - either open it in your editor, or run
this code block:
at `02_modules_and_packages/numfuncs.py` - either open it in your editor, or
run this code block:
```
......@@ -61,8 +61,8 @@ one. It contains an attribute called `PI`, and a function `add`.
Before we can use our module, we must `import` it. Importing a module in
Python will make its contents available to the local scope. We can import the
contents of `mymodule` like so:
Python will make its contents available in the local scope. We can import the
contents of `numfuncs` like so:
```
......@@ -113,7 +113,7 @@ print(add(1, 5))
__PLEASE DON'T DO THIS!__ Because every time you do, somewhere in the world, a
software developer will will spontaneously stub his/her toe, and start crying.
software developer will spontaneously stub his/her toe, and start crying.
Using this approach can make more complicated programs very difficult to read,
because it is not possible to determine the origin of the functions and
attributes that are being used.
......@@ -235,14 +235,14 @@ in the following order:
1. Built-in modules (e.g. `os`, `sys`, etc.).
2. In the current directory or, if a script has been executed, in the directory
containing that script.
3. In directories listed in the PYTHONPATH environment variable.
3. In directories listed in the `$PYTHONPATH` environment variable.
4. In installed third-party libraries (e.g. `numpy`).
If you are experimenting or developing your program, the quickest and easiest
way to make your module(s) importable is to add their containing directory to
the `PYTHONPATH`. But if you are developing a larger piece of software, you
should probably organise your modules into _packages_, which are [described
should probably organise your modules into *packages*, which are [described
below](#what-is-a-package).
......@@ -256,7 +256,7 @@ fact, there no difference between a _module_ and a _script_ - any `.py` file
can be executed as a script, or imported as a module, or both.
Have a look at the file `modules_and_packages/module_and_script.py`:
Have a look at the file `02_modules_and_packages/module_and_script.py`:
```
......@@ -272,7 +272,7 @@ that allows you to add code to a file that is _only executed when the module is
called as a script_. Try it in a terminal now:
> `python modules_and_packages/module_and_script.py`
> `python 02_modules_and_packages/module_and_script.py`
But if we `import` this module from another file, or from an interactive
......@@ -298,10 +298,10 @@ mas.main([str(a), str(b)])
You now know how to split your Python code up into separate files
(a.k.a. _modules_). When your code grows beyond a handful of files, you may
(a.k.a. *modules*). When your code grows beyond a handful of files, you may
wish for more fine-grained control over the namespaces in which your modules
live. Python has another feature which allows you to organise your modules
into _packages_.
into *packages*.
A package in Python is simply a directory which:
......@@ -351,9 +351,9 @@ from knowing that it is needed. But you can use `__init__.py` to perform some
package-specific initialisation, and/or to customise the package's namespace.
As an example, take a look the `modules_and_packages/fsleyes/__init__.py` file
in our mock FSLeyes package. We have imported the `fsleyes_main` function from
the `fsleyes.main` module, making it available at the package level. So
As an example, take a look the `02_modules_and_packages/fsleyes/__init__.py`
file in our mock FSLeyes package. We have imported the `fsleyes_main` function
from the `fsleyes.main` module, making it available at the package level. So
instead of importing the `fsleyes.main` module, we could instead just import
the `fsleyes` package:
......@@ -367,5 +367,5 @@ fsleyes.fsleyes_main()
<a class="anchor" id="useful-references"></a>
## Useful references
* [Modules and packages in Python](https://docs.python.org/3.5/tutorial/modules.html)
* [Using `__init__.py`](http://mikegrouchy.com/blog/2012/05/be-pythonic-__init__py.html)
\ No newline at end of file
* [Modules and packages in Python](https://docs.python.org/3/tutorial/modules.html)
* [Using `__init__.py`](http://mikegrouchy.com/blog/2012/05/be-pythonic-__init__py.html)
......@@ -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)).
......@@ -853,10 +944,10 @@ 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 `Operator.run` method - for a `NumberOperator` instance, the
`NumberOperator.preprocess` method will get called<sup>1</sup>.
`NumberOperator.preprocess` method will get called<sup>3</sup>.
> <sup>1</sup> When a sub-class overrides a base-class method, it is still
> <sup>3</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
......@@ -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`
......@@ -1018,13 +1109,17 @@ def capitaliseCalled(result):
so.register('mylistener', capitaliseCalled)
so = StringOperator()
so.do('capitalise')
so.do('concat', '?')
print(so.run('did you notice that functions are objects too'))
```
> Simple classes such as the `Notifier` are sometimes referred to as
> [_mixins_](https://en.wikipedia.org/wiki/Mixin).
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
......@@ -1106,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):
......@@ -1121,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
```
......@@ -1139,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'):
......@@ -1190,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):
......@@ -1209,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:
```
......@@ -1222,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()
......@@ -1290,19 +1379,27 @@ 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.
You can also define a method called `__new__` if you need to control the
creation stage, although this is very rarely needed. A brief explanation on
creation stage, although this is very rarely needed. One example of where you
might need to implement the `__new__` method is if you wish to create a
[subclass of a
`numpy.array`](https://docs.scipy.org/doc/numpy-1.14.0/user/basics.subclassing.html)
(although you might alternatively want to think about redefining your problem
so that this is not necessary).
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>
......@@ -1310,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
......@@ -1354,7 +1451,8 @@ types) is used.
However, because a Python method can be written to accept any number or type
of arguments, it is very easy to to build your own overloading logic by
writing a "dispatch" method. Here is YACE (Yet Another Contrived Example):
writing a "dispatch" method<sup>4</sup>. Here is YACE (Yet Another Contrived
Example):
```
......@@ -1384,6 +1482,11 @@ print('Add three: {}'.format(a.add(1, 2, 3)))
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/library/functools.html#functools.singledispatch),
> which is more complicated, but may allow you to write your dispatch logic in
> a more concise manner.
<a class="anchor" id="useful-references"></a>
## Useful references
......@@ -1393,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