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. This repository contains Jupyter notebooks and data for the 2020 WIN PyTreat.
It contains two sets of practicals: 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 - The `getting_started` directory contains a series of practicals intended
for those of you who are new to the Python programming language, or need for those of you who are new to the Python programming language, or need
...@@ -14,43 +17,39 @@ It contains two sets of practicals: ...@@ -14,43 +17,39 @@ It contains two sets of practicals:
about the language. 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. installed.
## For attendees ## For attendees
To run these notebooks in the `fslpython` environment, you must first install These notebooks can be run in the `fslpython` environment using:
jupyter:
``` ```
# If your FSL installation requires administrative privileges to modify, then git clone https://git.fmrib.ox.ac.uk/fsl/pytreat-practicals-2020.git
# you MUST run these commands as root - don't just prefix each individual cd pytreat-practicals-2020
# command with sudo, or you will probably install jupyter into the wrong fslpython -m notebook
# 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
``` ```
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 Throughout the week we might make changes to this repository. When this
`fsljupyter notebook`: 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 git stash save
cd pytreat-2018-practicals git pull origin master
fsljupyter notebook git stash pop
``` ```
...@@ -60,52 +59,127 @@ Have fun! ...@@ -60,52 +59,127 @@ Have fun!
## For contributors ## 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 1. Fork the upstream repository on gitlab
2. Make a local clone of your fork: 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: 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 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 push origin master
git rebase upstream/master
git push --force origin master
``` ```
6. In gitlab, submit a merge request from your fork back to the upstream 6. In gitlab, submit a merge request from your fork back to the upstream
repository. 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 You may wish to install [`notedown`](https://github.com/aaren/notedown):
[`notedown`](https://github.com/aaren/notedown):
``` ```
# . $FSLDIR/fslpython/bin/conda install -n fslpython -c conda-forge notedown
# see instructions above
# .
conda install jupyter
pip install notedown
source deactivate
ln -s $FSLDIR/fslpython/envs/fslpython/bin/jupyter $FSLDIR/bin/fsljupyter
ln -s $FSLDIR/fslpython/envs/fslpython/bin/notedown $FSLDIR/bin/fslnotedown ln -s $FSLDIR/fslpython/envs/fslpython/bin/notedown $FSLDIR/bin/fslnotedown
``` ```
......
%% Cell type:markdown id: tags: %% Cell type:markdown id: tags:
# Modules and packages # Modules and packages
Python gives you a lot of flexibility in how you organise your code. If you 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. 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 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 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 single file, you will (hopefully) want to start arranging it in a more
understandable manner. understandable manner.
For this practical we have prepared a handful of example files - you can find For this practical we have prepared a handful of example files - you can find
them alongside this notebook file, in a directory called them alongside this notebook file, in a directory called
`modules_and_packages/`. `02_modules_and_packages/`.
## Contents ## Contents
* [What is a module?](#what-is-a-module) * [What is a module?](#what-is-a-module)
* [Importing modules](#importing-modules) * [Importing modules](#importing-modules)
* [Importing specific items from a module](#importing-specific-items-from-a-module) * [Importing specific items from a module](#importing-specific-items-from-a-module)
* [Importing everything from a module](#importing-everything-from-a-module) * [Importing everything from a module](#importing-everything-from-a-module)
* [Module aliases](#module-aliases) * [Module aliases](#module-aliases)
* [What happens when I import a module?](#what-happens-when-i-import-a-module) * [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) * [How can I make my own modules importable?](#how-can-i-make-my-own-modules-importable)
* [Modules versus scripts](#modules-versus-scripts) * [Modules versus scripts](#modules-versus-scripts)
* [What is a package?](#what-is-a-package) * [What is a package?](#what-is-a-package)
* [`__init__.py`](#init-py) * [`__init__.py`](#init-py)
* [Useful references](#useful-references) * [Useful references](#useful-references)
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` ```
import os import os
os.chdir('modules_and_packages') os.chdir('02_modules_and_packages')
``` ```
%% Cell type:markdown id: tags: %% Cell type:markdown id: tags:
<a class="anchor" id="what-is-a-module"></a> <a class="anchor" id="what-is-a-module"></a>
## What is a module? ## What is a module?
Any file ending with `.py` is considered to be a module in Python. Take a look 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 at `02_modules_and_packages/numfuncs.py` - either open it in your editor, or
this code block: run this code block:
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` ```
with open('numfuncs.py', 'rt') as f: with open('numfuncs.py', 'rt') as f:
for line in f: for line in f:
print(line.rstrip()) print(line.rstrip())
``` ```
%% Cell type:markdown id: tags: %% Cell type:markdown id: tags:
This is a perfectly valid Python module, although not a particularly useful This is a perfectly valid Python module, although not a particularly useful
one. It contains an attribute called `PI`, and a function `add`. one. It contains an attribute called `PI`, and a function `add`.
<a class="anchor" id="importing-modules"></a> <a class="anchor" id="importing-modules"></a>
## Importing modules ## Importing modules
Before we can use our module, we must `import` it. Importing a module in 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 Python will make its contents available in the local scope. We can import the
contents of `mymodule` like so: contents of `numfuncs` like so:
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` ```
import numfuncs import numfuncs
``` ```
%% Cell type:markdown id: tags: %% Cell type:markdown id: tags:
This imports `numfuncs` into the local scope - everything defined in the This imports `numfuncs` into the local scope - everything defined in the
`numfuncs` module can be accessed by prefixing it with `numfuncs.`: `numfuncs` module can be accessed by prefixing it with `numfuncs.`:
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` ```
print('PI:', numfuncs.PI) print('PI:', numfuncs.PI)
print(numfuncs.add(1, 50)) print(numfuncs.add(1, 50))
``` ```
%% Cell type:markdown id: tags: %% Cell type:markdown id: tags:
There are a couple of other ways to import items from a module... There are a couple of other ways to import items from a module...
<a class="anchor" id="importing-specific-items-from-a-module"></a> <a class="anchor" id="importing-specific-items-from-a-module"></a>
### Importing specific items from a module ### Importing specific items from a module
If you only want to use one, or a few items from a module, you can import just 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 those items - a reference to just those items will be created in the local
scope: scope:
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` ```
from numfuncs import add from numfuncs import add
print(add(1, 3)) print(add(1, 3))
``` ```
%% Cell type:markdown id: tags: %% Cell type:markdown id: tags:
<a class="anchor" id="importing-everything-from-a-module"></a> <a class="anchor" id="importing-everything-from-a-module"></a>
### Importing everything from a module ### Importing everything from a module
It is possible to import _everything_ that is defined in a module like so: It is possible to import _everything_ that is defined in a module like so:
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` ```
from numfuncs import * from numfuncs import *
print('PI: ', PI) print('PI: ', PI)
print(add(1, 5)) print(add(1, 5))
``` ```
%% Cell type:markdown id: tags: %% Cell type:markdown id: tags:
__PLEASE DON'T DO THIS!__ Because every time you do, somewhere in the world, a __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, 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 because it is not possible to determine the origin of the functions and
attributes that are being used. attributes that are being used.
And naming collisions are inevitable when importing multiple modules in this 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 way, making it very difficult for somebody else to figure out what your code
is doing: is doing:
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` ```
from numfuncs import * from numfuncs import *
from strfuncs import * from strfuncs import *
print(add(1, 5)) print(add(1, 5))
``` ```
%% Cell type:markdown id: tags: %% Cell type:markdown id: tags:
Instead, it is better to give modules a name when you import them. While this 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 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 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 trace through code when the functions you use are accessed through a namespace
for each module: for each module:
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` ```
import numfuncs import numfuncs
import strfuncs import strfuncs
print('number add: ', numfuncs.add(1, 2)) print('number add: ', numfuncs.add(1, 2))
print('string add: ', strfuncs.add(1, 2)) print('string add: ', strfuncs.add(1, 2))
``` ```
%% Cell type:markdown id: tags: %% Cell type:markdown id: tags:
<a class="anchor" id="module-aliases"></a> <a class="anchor" id="module-aliases"></a>
### Module aliases ### Module aliases
And Python allows you to define an _alias_ for a module when you import it, 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 so you don't necessarily need to type out the full module name each time
you want to access something inside: you want to access something inside:
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` ```
import numfuncs as nf import numfuncs as nf
import strfuncs as sf import strfuncs as sf
print('number add: ', nf.add(1, 2)) print('number add: ', nf.add(1, 2))
print('string add: ', sf.add(1, 2)) print('string add: ', sf.add(1, 2))
``` ```
%% Cell type:markdown id: tags: %% Cell type:markdown id: tags:
You have already seen this in the earlier practicals - here are a few 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 aliases which have become a de-facto standard for commonly used Python
modules: modules:
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` ```
import os.path as op import os.path as op
import numpy as np import numpy as np
import nibabel as nib import nibabel as nib
import matplotlib as mpl import matplotlib as mpl
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
``` ```
%% Cell type:markdown id: tags: %% Cell type:markdown id: tags:
<a class="anchor" id="what-happens-when-i-import-a-module"></a> <a class="anchor" id="what-happens-when-i-import-a-module"></a>
### What happens when I import a module? ### What happens when I import a module?
When you `import` a module, the contents of the module file are literally 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 executed by the Python runtime, exactly the same as if you had typed its
contents into `ipython`. Any attributes, functions, or classes which are contents into `ipython`. Any attributes, functions, or classes which are
defined in the module will be bundled up into an object that represents the 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. module, and through which you can access the module's contents.
When we typed `import numfuncs` in the examples above, the following events When we typed `import numfuncs` in the examples above, the following events
occurred: occurred:
1. Python created a `module` object to represent the module. 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 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 inside `numfuncs.py` (i.e. the `PI` attribute and the `add` function) were
added to the `module` object. added to the `module` object.
3. A local variable called `numfuncs`, pointing to the `module` object, 3. A local variable called `numfuncs`, pointing to the `module` object,
was added to the local scope. was added to the local scope.
Because module files are literally executed on import, any statements in the 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 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 executed. As an example, take a look at the file `sideeffects.py`. Let's
import it and see what happens: import it and see what happens:
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` ```
import sideeffects import sideeffects
``` ```
%% Cell type:markdown id: tags: %% Cell type:markdown id: tags:
Ok, hopefully that wasn't too much of a surprise. Something which may be less 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 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 _first_ time that it is imported. After the first import, Python caches the
module's contents (all loaded modules are accessible through module's contents (all loaded modules are accessible through
[`sys.modules`](https://docs.python.org/3.5/library/sys.html#sys.modules)). On [`sys.modules`](https://docs.python.org/3.5/library/sys.html#sys.modules)). On
subsequent imports, the cached version of the module is returned: subsequent imports, the cached version of the module is returned:
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` ```
import sideeffects import sideeffects
``` ```
%% Cell type:markdown id: tags: %% Cell type:markdown id: tags:
<a class="anchor" id="how-can-i-make-my-own-modules-importable"></a> <a class="anchor" id="how-can-i-make-my-own-modules-importable"></a>
### How can I make my own modules importable? ### How can I make my own modules importable?
When you `import` a module, Python searches for it in the following locations, When you `import` a module, Python searches for it in the following locations,
in the following order: in the following order:
1. Built-in modules (e.g. `os`, `sys`, etc.). 1. Built-in modules (e.g. `os`, `sys`, etc.).
2. In the current directory or, if a script has been executed, in the directory 2. In the current directory or, if a script has been executed, in the directory
containing that script. 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`). 4. In installed third-party libraries (e.g. `numpy`).
If you are experimenting or developing your program, the quickest and easiest 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 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 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). below](#what-is-a-package).
<a class="anchor" id="modules-versus-scripts"></a> <a class="anchor" id="modules-versus-scripts"></a>
## Modules versus scripts ## Modules versus scripts
You now know that Python treats all files ending in `.py` as importable 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 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 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. 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: %% Cell type:code id: tags:
``` ```
with open('module_and_script.py', 'rt') as f: with open('module_and_script.py', 'rt') as f:
for line in f: for line in f:
print(line.rstrip()) print(line.rstrip())
``` ```
%% Cell type:markdown id: tags: %% Cell type:markdown id: tags:
This file contains two functions `mul` and `main`. The This file contains two functions `mul` and `main`. The
`if __name__ == '__main__':` clause at the bottom is a standard trick in Python `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 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: 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 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 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 executed, and we can access its functions just like any other module that we
import. import.
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` ```
import module_and_script as mas import module_and_script as mas
a = 1.5 a = 1.5
b = 3 b = 3
print('mul({}, {}): {}'.format(a, b, mas.mul(a, b))) print('mul({}, {}): {}'.format(a, b, mas.mul(a, b)))
print('calling main...') print('calling main...')
mas.main([str(a), str(b)]) mas.main([str(a), str(b)])
``` ```
%% Cell type:markdown id: tags: %% Cell type:markdown id: tags:
<a class="anchor" id="what-is-a-package"></a> <a class="anchor" id="what-is-a-package"></a>
## What is a package? ## What is a package?
You now know how to split your Python code up into separate files 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 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 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: A package in Python is simply a directory which:
* Contains a special file called `__init__.py` * Contains a special file called `__init__.py`
* May contain one or more module files (any other files ending in `*.py`) * May contain one or more module files (any other files ending in `*.py`)
* May contain other package directories. * May contain other package directories.
For example, the [FSLeyes](https://git.fmrib.ox.ac.uk/fsl/fsleyes/fsleyes) For example, the [FSLeyes](https://git.fmrib.ox.ac.uk/fsl/fsleyes/fsleyes)
code is organised into packages and sub-packages as follows (abridged): code is organised into packages and sub-packages as follows (abridged):
> ``` > ```
> fsleyes/ > fsleyes/
> __init__.py > __init__.py
> main.py > main.py
> frame.py > frame.py
> views/ > views/
> __init__.py > __init__.py
> orthopanel.py > orthopanel.py
> lightboxpanel.py > lightboxpanel.py
> controls/ > controls/
> __init__.py > __init__.py
> locationpanel.py > locationpanel.py
> overlaylistpanel.py > overlaylistpanel.py
> ``` > ```
Within a package structure, we will typically still import modules directly, Within a package structure, we will typically still import modules directly,
via their full path within the package: via their full path within the package:
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` ```
import fsleyes.main as fmain import fsleyes.main as fmain
fmain.fsleyes_main() fmain.fsleyes_main()
``` ```
%% Cell type:markdown id: tags: %% Cell type:markdown id: tags:
<a class="anchor" id="init-py"></a> <a class="anchor" id="init-py"></a>
### `__init__.py` ### `__init__.py`
Every Python package must have an `__init__.py` file. In many cases, this will 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 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 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. 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 As an example, take a look the `02_modules_and_packages/fsleyes/__init__.py`
in our mock FSLeyes package. We have imported the `fsleyes_main` function from file in our mock FSLeyes package. We have imported the `fsleyes_main` function
the `fsleyes.main` module, making it available at the package level. So 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 instead of importing the `fsleyes.main` module, we could instead just import
the `fsleyes` package: the `fsleyes` package:
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` ```
import fsleyes import fsleyes
fsleyes.fsleyes_main() fsleyes.fsleyes_main()
``` ```
%% Cell type:markdown id: tags: %% Cell type:markdown id: tags:
<a class="anchor" id="useful-references"></a> <a class="anchor" id="useful-references"></a>
## Useful references ## 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) * [Using `__init__.py`](http://mikegrouchy.com/blog/2012/05/be-pythonic-__init__py.html)
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
Python gives you a lot of flexibility in how you organise your code. If you 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. 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 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 But when your code starts to grow beyond what can reasonably be defined in a
...@@ -14,7 +14,7 @@ understandable manner. ...@@ -14,7 +14,7 @@ understandable manner.
For this practical we have prepared a handful of example files - you can find For this practical we have prepared a handful of example files - you can find
them alongside this notebook file, in a directory called them alongside this notebook file, in a directory called
`modules_and_packages/`. `02_modules_and_packages/`.
## Contents ## Contents
...@@ -33,7 +33,7 @@ them alongside this notebook file, in a directory called ...@@ -33,7 +33,7 @@ them alongside this notebook file, in a directory called
``` ```
import os import os
os.chdir('modules_and_packages') os.chdir('02_modules_and_packages')
``` ```
<a class="anchor" id="what-is-a-module"></a> <a class="anchor" id="what-is-a-module"></a>
...@@ -41,8 +41,8 @@ os.chdir('modules_and_packages') ...@@ -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 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 at `02_modules_and_packages/numfuncs.py` - either open it in your editor, or
this code block: run this code block:
``` ```
...@@ -61,8 +61,8 @@ one. It contains an attribute called `PI`, and a function `add`. ...@@ -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 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 Python will make its contents available in the local scope. We can import the
contents of `mymodule` like so: contents of `numfuncs` like so:
``` ```
...@@ -113,7 +113,7 @@ print(add(1, 5)) ...@@ -113,7 +113,7 @@ print(add(1, 5))
__PLEASE DON'T DO THIS!__ Because every time you do, somewhere in the world, a __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, 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 because it is not possible to determine the origin of the functions and
attributes that are being used. attributes that are being used.
...@@ -235,14 +235,14 @@ in the following order: ...@@ -235,14 +235,14 @@ in the following order:
1. Built-in modules (e.g. `os`, `sys`, etc.). 1. Built-in modules (e.g. `os`, `sys`, etc.).
2. In the current directory or, if a script has been executed, in the directory 2. In the current directory or, if a script has been executed, in the directory
containing that script. 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`). 4. In installed third-party libraries (e.g. `numpy`).
If you are experimenting or developing your program, the quickest and easiest 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 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 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). below](#what-is-a-package).
...@@ -256,7 +256,7 @@ fact, there no difference between a _module_ and a _script_ - any `.py` file ...@@ -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. 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 ...@@ -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: 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 But if we `import` this module from another file, or from an interactive
...@@ -298,10 +298,10 @@ mas.main([str(a), str(b)]) ...@@ -298,10 +298,10 @@ mas.main([str(a), str(b)])
You now know how to split your Python code up into separate files 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 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 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: 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 ...@@ -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. 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 As an example, take a look the `02_modules_and_packages/fsleyes/__init__.py`
in our mock FSLeyes package. We have imported the `fsleyes_main` function from file in our mock FSLeyes package. We have imported the `fsleyes_main` function
the `fsleyes.main` module, making it available at the package level. So 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 instead of importing the `fsleyes.main` module, we could instead just import
the `fsleyes` package: the `fsleyes` package:
...@@ -367,5 +367,5 @@ fsleyes.fsleyes_main() ...@@ -367,5 +367,5 @@ fsleyes.fsleyes_main()
<a class="anchor" id="useful-references"></a> <a class="anchor" id="useful-references"></a>
## Useful references ## 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) * [Using `__init__.py`](http://mikegrouchy.com/blog/2012/05/be-pythonic-__init__py.html)
\ No newline at end of file
...@@ -19,6 +19,7 @@ you use an object-oriented approach. ...@@ -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) * [We didn't specify the `self` argument - what gives?!?](#we-didnt-specify-the-self-argument)
* [Attributes](#attributes) * [Attributes](#attributes)
* [Methods](#methods) * [Methods](#methods)
* [Method chaining](#method-chaining)
* [Protecting attribute access](#protecting-attribute-access) * [Protecting attribute access](#protecting-attribute-access)
* [A better way - properties](#a-better-way-properties]) * [A better way - properties](#a-better-way-properties])
* [Inheritance](#inheritance) * [Inheritance](#inheritance)
...@@ -46,8 +47,8 @@ section. ...@@ -46,8 +47,8 @@ section.
If you have not done any object-oriented programming before, your first step If you have not done any object-oriented programming before, your first step
is to understand the difference between _objects_ (also known as is to understand the difference between *objects* (also known as
_instances_) and _classes_ (also known as _types_). *instances*) and *classes* (also known as *types*).
If you have some experience in C, then you can start off by thinking of a 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: ...@@ -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 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_ memory. An object can have *attributes* (pieces of information), and *methods*
(functions associated with the object). You can pass objects around your code, (functions associated with the object). You can pass objects around your code,
manipulate their attributes, and call their methods. manipulate their attributes, and call their methods.
...@@ -92,12 +93,12 @@ you create an object from that class. ...@@ -92,12 +93,12 @@ you create an object from that class.
Of course there are many more differences between C structs and classes (most Of course there are many more differences between C structs and classes (most
notably [inheritance](todo), [polymorphism](todo), and [access notably [inheritance](todo), [polymorphism](todo), and [access
protection](todo)). But if you can understand the difference between a protection](todo)). But if you can understand the difference between a
_definition_ of a C struct, and an _instantiation_ of that struct, then you *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_, are most of the way towards understanding the difference between a *class*,
and an _object_. 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! > object - even classes!
...@@ -206,7 +207,7 @@ print(fm) ...@@ -206,7 +207,7 @@ print(fm)
Refer to the [official 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 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 take a look at the appendix for some more details on [how Python objects get
created](appendix-init-versus-new). created](appendix-init-versus-new).
...@@ -352,8 +353,8 @@ append a tuple to that `operations` list. ...@@ -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 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 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 *stage* each operation, and then perform them all in one go at a later point
another method, `run`, which actually does the work: in time. So let's add another method, `run`, which actually does the work:
``` ```
...@@ -387,7 +388,6 @@ class FSLMaths(object): ...@@ -387,7 +388,6 @@ class FSLMaths(object):
if isinstance(value, nib.nifti1.Nifti1Image): if isinstance(value, nib.nifti1.Nifti1Image):
value = value.get_data() value = value.get_data()
if oper == 'add': if oper == 'add':
data = data + value data = data + value
elif oper == 'mul': elif oper == 'mul':
...@@ -430,6 +430,99 @@ print('Number of voxels >0 in masked image: {}'.format(nmaskvox)) ...@@ -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> <a class="anchor" id="protecting-attribute-access"></a>
## Protecting attribute access ## Protecting attribute access
...@@ -488,9 +581,8 @@ of an object. This is in contrast to languages like C++ and Java, where the ...@@ -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. 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 However, there are a couple of conventions in Python that are
adhered [universally adhered to](https://docs.python.org/3/tutorial/classes.html#private-variables):
to](https://docs.python.org/3.5/tutorial/classes.html#private-variables):
* Class-level attributes and methods, and module-level attributes, functions, * Class-level attributes and methods, and module-level attributes, functions,
and classes, which begin with a single underscore (`_`), should be 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): ...@@ -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 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 not accessible through its original name (it is still accessible via its
[mangled [mangled name](https://docs.python.org/3/tutorial/classes.html#private-variables)
name](https://docs.python.org/3.5/tutorial/classes.html#private-variables)
though). though).
> <sup>2</sup> With the exception that module-level fields which begin with a > <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 > 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 So with all of this in mind, we can adjust our `FSLMaths` class to discourage
...@@ -541,7 +632,7 @@ print(fm.__img) ...@@ -541,7 +632,7 @@ print(fm.__img)
Python has a feature called 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 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 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 our attributes, and "decorating" them with the `@property` decorator (we will
...@@ -676,17 +767,17 @@ class Chihuahua(Dog): ...@@ -676,17 +767,17 @@ class Chihuahua(Dog):
Hopefully this example doesn't need much in the way of explanation - this Hopefully this example doesn't need much in the way of explanation - this
collection of classes captures a hierarchical relationship which exists in the collection of classes represents a hierarchical relationship which exists in
real world (and also captures the inherently annoying nature of 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 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 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 `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 from*, or *inherits from*, the `Animal` class, and that all `Dog` instances
are also `Animal` instances (but not vice-versa). are also `Animal` instances (but not vice-versa).
What does that `noiseMade` method do? There is a `noiseMade` method defined 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`, `Dog`,
[`TalkingDog`](https://twitter.com/simpsonsqotd/status/427941665836630016?lang=en), [`TalkingDog`](https://twitter.com/simpsonsqotd/status/427941665836630016?lang=en),
`Cat`, and `Chihuahua` classes (but not on the `Labrador` class). We can call `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 ...@@ -819,7 +910,7 @@ This line invokes `Operator.__init__` - the initialisation method for the
In Python, we can use the [built-in `super` 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 of correctly calling methods that are defined in an object's base-class (or
classes, in the case of [multiple inheritance](multiple-inheritance)). 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 ...@@ -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 The `NumberOperator` class has also overridden the `preprocess` method, to
ensure that all values handled by the `Operator` are numbers. This method gets ensure that all values handled by the `Operator` are numbers. This method gets
called within the `Operator.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>. `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()` > possible to access the base-class implementation [via the `super()`
> function](https://stackoverflow.com/a/4747427) (the preferred method), or by > function](https://stackoverflow.com/a/4747427) (the preferred method), or by
> [explicitly calling the base-class > [explicitly calling the base-class
...@@ -920,8 +1011,8 @@ print(so.run('python is an ok language')) ...@@ -920,8 +1011,8 @@ print(so.run('python is an ok language'))
### Polymorphism ### Polymorphism
Inheritance also allows us to take advantage of _polymorphism_, which refers 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 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 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 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` `Operator` instance, but which will work on an instance of any `Operator`
...@@ -1018,13 +1109,17 @@ def capitaliseCalled(result): ...@@ -1018,13 +1109,17 @@ def capitaliseCalled(result):
so.register('mylistener', capitaliseCalled) so.register('mylistener', capitaliseCalled)
so = StringOperator()
so.do('capitalise') so.do('capitalise')
so.do('concat', '?') so.do('concat', '?')
print(so.run('did you notice that functions are objects too')) 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 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 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 are called (and which base class method will be called, in the case of naming
...@@ -1106,12 +1201,15 @@ class FSLMaths(object): ...@@ -1106,12 +1201,15 @@ class FSLMaths(object):
def add(self, value): def add(self, value):
self.operations.append(('add', value)) self.operations.append(('add', value))
return self
def mul(self, value): def mul(self, value):
self.operations.append(('mul', value)) self.operations.append(('mul', value))
return self
def div(self, value): def div(self, value):
self.operations.append(('div', value)) self.operations.append(('div', value))
return self
def run(self, output=None): def run(self, output=None):
...@@ -1121,12 +1219,15 @@ class FSLMaths(object): ...@@ -1121,12 +1219,15 @@ class FSLMaths(object):
# Code omitted for brevity # Code omitted for brevity
# Increment the usage counter # Increment the usage counter for this operation. We can
# for this operation. We can # access class attributes (and methods) through the class
# access class attributes (and # itself, as shown here.
# methods) through the class FSLMaths.opCounters[oper] = FSLMaths.opCounters.get(oper, 0) + 1
# itself.
FSLMaths.opCounters[oper] = self.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') ...@@ -1139,17 +1240,8 @@ fmask = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm_brain_mask.nii.gz')
inimg = nib.load(fpath) inimg = nib.load(fpath)
mask = nib.load(fmask) mask = nib.load(fmask)
fm1 = FSLMaths(inimg) FSLMaths(inimg).mul(mask).add(25).run()
fm2 = FSLMaths(inimg) FSLMaths(inimg).add(15).div(1.5).run()
fm1.mul(mask)
fm1.add(15)
fm2.add(25)
fm1.div(1.5)
fm1.run()
fm2.run()
print('FSLMaths usage statistics') print('FSLMaths usage statistics')
for oper in ('add', 'div', 'mul'): for oper in ('add', 'div', 'mul'):
...@@ -1190,12 +1282,15 @@ class FSLMaths(object): ...@@ -1190,12 +1282,15 @@ class FSLMaths(object):
def add(self, value): def add(self, value):
self.operations.append(('add', value)) self.operations.append(('add', value))
return self
def mul(self, value): def mul(self, value):
self.operations.append(('mul', value)) self.operations.append(('mul', value))
return self
def div(self, value): def div(self, value):
self.operations.append(('div', value)) self.operations.append(('div', value))
return self
def run(self, output=None): def run(self, output=None):
...@@ -1209,11 +1304,11 @@ class FSLMaths(object): ...@@ -1209,11 +1304,11 @@ class FSLMaths(object):
> There is another decorator - > There is another decorator -
> [`@staticmethod`](https://docs.python.org/3.5/library/functions.html#staticmethod) - > [`@staticmethod`](https://docs.python.org/3.5/library/functions.html#staticmethod) -
> which can be used on methods defined within a class. The difference > 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`). > 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') ...@@ -1222,14 +1317,8 @@ fmask = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm_brain_mask.nii.gz')
inimg = nib.load(fpath) inimg = nib.load(fpath)
mask = nib.load(fmask) mask = nib.load(fmask)
fm1 = FSLMaths(inimg) fm1 = FSLMaths(inimg).mul(mask).add(25)
fm2 = FSLMaths(inimg) fm2 = FSLMaths(inimg).add(15).div(1.5)
fm1.mul(mask)
fm1.add(15)
fm2.add(25)
fm1.div(1.5)
fm1.run() fm1.run()
fm2.run() fm2.run()
...@@ -1290,19 +1379,27 @@ always use the new-style format. ...@@ -1290,19 +1379,27 @@ always use the new-style format.
## Appendix: `__init__` versus `__new__` ## Appendix: `__init__` versus `__new__`
In Python, object creation is actually a two-stage process - _creation_, and In Python, object creation is actually a two-stage process - *creation*, and
then _initialisation_. The `__init__` method gets called during the then *initialisation*. The `__init__` method gets called during the
_initialisation_ stage - its job is to initialise the state of the object. But *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 note that, by the time `__init__` gets called, the object has already been
created. created.
You can also define a method called `__new__` if you need to control the 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 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/), [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 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> <a class="anchor" id="appendix-monkey-patching"></a>
...@@ -1310,24 +1407,24 @@ docs](https://docs.python.org/3.5/reference/datamodel.html#basic-customization). ...@@ -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 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 whilst it is allowed by the Python programming language, it is generally
considered quite bad practice. 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 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 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 in a clear, readable fashion, and avoid the use of devious tactics such as
monkey-patching. 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, 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 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 As another example, consider the scenario where you are dependent on a third
...@@ -1354,7 +1451,8 @@ types) is used. ...@@ -1354,7 +1451,8 @@ types) is used.
However, because a Python method can be written to accept any number or type 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 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))) ...@@ -1384,6 +1482,11 @@ print('Add three: {}'.format(a.add(1, 2, 3)))
print('Add four: {}'.format(a.add(1, 2, 3, 4))) 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> <a class="anchor" id="useful-references"></a>
## Useful references ## Useful references
...@@ -1393,5 +1496,5 @@ The official Python documentation has a wealth of information on the internal ...@@ -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: 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/tutorial/classes.html
* https://docs.python.org/3.5/reference/datamodel.html * https://docs.python.org/3/reference/datamodel.html