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 (285)
Showing
with 4825 additions and 38 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 the following:
- The `talks` directory contains some (but not all) of the _Topyc_ talks that
will be given throughout the week.
The upstream repository can be found at:
- The `getting_started` directory contains a series of practicals intended
for those of you who are new to the Python programming language, or need
a refresher.
https://git.fmrib.ox.ac.uk/fsl/pytreat-2018-practicals
- The `advanced_topics` directory contains a series of practicals on various
aspects of the Python programming language - these are intended for those
of you who are familiar with the basics of Python, and want to learn more
about the language.
To contribute to the practicals:
The practicals have been written under the assumption that FSL 6.0.3 is
installed.
## For attendees
These notebooks can be run in the `fslpython` environment using:
```
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.
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 stash save
git pull origin master
git stash pop
```
Have fun!
## For contributors
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
```
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
### 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
To run these notebooks in the `fslpython` environment, you must first install
jupyter:
### Updating your local repository
To bring in the changes that other people have contributed to the main
repository into your local repository:
```
# 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
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
git fetch --all
```
Then, do this if you are working on a branch within the main repository:
> [`notedown`](https://github.com/aaren/notedown) is a handy tool which allows
> you to convert a markdown (`.md`) file to a Jupyter notebook (`.ipynb`)
> file. So you can write your practical in your text editor of choice, and
> then convert it into a notebook, instead of writing the practical in the web
> browser interface. If you install notedown as suggested in the code block
> above, you can run it on a markdown file like so:
```
# 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:
>
> ```
> fslnotedown my_markdown_file.md > my_notebook.ipynb
> 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
> ```
Now you can start the notebook server from the repository root:
### Writing your talk as a Jupyter notebook
You may wish to install [`notedown`](https://github.com/aaren/notedown):
```
$FSLDIR/fslpython/bin/conda install -n fslpython -c conda-forge notedown
ln -s $FSLDIR/fslpython/envs/fslpython/bin/notedown $FSLDIR/bin/fslnotedown
```
`notedown` is a handy tool which allows you to convert a markdown (`.md`) file
to a Jupyter notebook (`.ipynb`) file. So you can write your practical in your
text editor of choice, and then convert it into a notebook, instead of writing
the practical in the web browser interface. If you install notedown as
suggested in the code block above, you can run it on a markdown file like so:
```
fsljupyter notebook
fslnotedown my_markdown_file.md > my_notebook.ipynb
```
%% Cell type:markdown id: tags:
# Function inputs and outputs
In Python, arguments to a function can be specified in two different ways - by
using _positional_ arguments, or by using _keyword_ arguments.
## Positional arguments
Let's say we have a function that looks like this
%% Cell type:code id: tags:
```
def myfunc(a, b, c):
print('First argument: ', a)
print('Second argument:', b)
print('Third argument: ', c)
```
%% Cell type:markdown id: tags:
If we call this function like so:
%% Cell type:code id: tags:
```
myfunc(1, 2, 3)
```
%% Cell type:markdown id: tags:
The values `1`, `2` and `3` get assigned to arguments `a`, `b`, and `c`
respectively, based on the position in which they are passed.
Python allows us to pass positional arguments into a function from a sequence,
using the star (`*`) operator. So we could store our arguments in a list or
tuple, and then pass the list straight in:
%% Cell type:code id: tags:
```
args = [3, 4, 5]
myfunc(*args)
```
%% Cell type:markdown id: tags:
You can think of the star operator as _unpacking_ the contents of the
sequence.
## Keyword arguments
Using keyword arguments allows us to pass arguments to a function in any order
we like. We could just as easily call our `myfunc` function like so, and get
the same result that we did earlier when using positional arguments:
%% Cell type:code id: tags:
```
myfunc(c=3, b=2, a=1)
```
%% Cell type:markdown id: tags:
Python has another operator - the double-star (`**`), which will unpack
keyword arguments from a `dict`. For example:
%% Cell type:code id: tags:
```
kwargs = {'a' : 4, 'b' : 5, 'c' : 6}
myfunc(**kwargs)
```
%% Cell type:markdown id: tags:
## Combining positional and keyword arguments
In fact, we can use both of these techniques at once, like so:
%% Cell type:code id: tags:
```
args = (100, 200)
kwargs = {'c' : 300}
myfunc(*args, **kwargs)
```
%% Cell type:markdown id: tags:
## Default argument values
Function arguments can be given default values, like so:
%% Cell type:code id: tags:
```
def myfunc(a=1, b=2, c=3):
print('First argument: ', a)
print('Second argument:', b)
print('Third argument: ', c)
```
%% Cell type:markdown id: tags:
Now we can call `myfunc`, only passing the arguments that we need to. The
arguments which are unspecified in the function call will be assigned their
default value:
%% Cell type:code id: tags:
```
myfunc()
myfunc(10)
myfunc(10, b=30)
myfunc(c=300)
```
%% Cell type:markdown id: tags:
__WARNING:__ _Never_ define a function with a mutable default value, such as a
`list`, `dict` or other non-primitive type. Let's see what happens when we do:
%% Cell type:code id: tags:
```
def badfunc(a=[]):
a.append('end of sequence')
output = ', '.join([str(elem) for elem in a])
print(output)
```
%% Cell type:markdown id: tags:
With this function, all is well and good if we pass in our own value for `a`:
%% Cell type:code id: tags:
```
badfunc([1, 2, 3, 4])
badfunc([2, 4, 6])
```
%% Cell type:markdown id: tags:
But what happens when we let `badfunc` use the default value for `a`?
%% Cell type:code id: tags:
```
badfunc()
badfunc()
badfunc()
```
%% Cell type:markdown id: tags:
This happens because default argument values are created when the function is
defined, and will persist for the duration of your program. So in this
example, the default value for `a`, a Python `list`, gets created when
`badfunc` is defined, and hangs around for the lifetime of the `badfunc`
function!
## Variable numbers of arguments - `args` and `kwargs`
The `*` and `**` operators can also be used in function definitions - this
indicates that a function may accept a variable number of arguments.
Let's redefine `myfunc` to accept any number of positional arguments - here,
all positional arguments will be passed into `myfunc` as a tuple called
`args`:
%% Cell type:code id: tags:
```
def myfunc(*args):
print('myfunc({})'.format(args))
print(' Number of arguments: {}'.format(len(args)))
for i, arg in enumerate(args):
print(' Argument {:2d}: {}'.format(i, arg))
myfunc()
myfunc(1)
myfunc(1, 2, 3)
myfunc(1, 'a', [3, 4])
```
%% Cell type:markdown id: tags:
Similarly, we can define a function to accept any number of keyword
arguments. In this case, the keyword arguments will be packed into a `dict`
called `kwargs`:
%% Cell type:code id: tags:
```
def myfunc(**kwargs):
print('myfunc({})'.format(kwargs))
for k, v in kwargs.items():
print(' Argument {} = {}'.format(k, v))
myfunc()
myfunc(a=1, b=2)
myfunc(a='abc', foo=123)
```
%% Cell type:markdown id: tags:
This is a useful technique in many circumstances. For example, if you are
writing a function which calls another function that takes many arguments, you
can use ``**kwargs`` to pass-through arguments to the second function. As an
example, let's say we have functions `flirt` and `fnirt`, which respectively
perform linear and non-linear registration:
%% Cell type:code id: tags:
```
def flirt(infile,
ref,
outfile=None,
init=None,
omat=None,
dof=12):
# TODO get MJ to fill this bit in
pass
def fnirt(infile,
ref,
outfile=None,
aff=None,
interp='nn',
refmask=None,
minmet='lg',
subsamp=4):
# TODO get Jesper to fill this bit in
pass
```
%% Cell type:markdown id: tags:
We want to write our own registration function which uses the `flirt` and
`fnirt` functions, while also allowing the `fnirt` parameters to be
customised. We can use `**kwargs` to do this:
%% Cell type:code id: tags:
```
def do_nonlinear_reg(infile, ref, outfile, **kwargs):
"""Aligns infile to ref using non-linear registration. All keyword
arguments are passed through to the fnirt function.
"""
affmat = '/tmp/aff.mat'
# calculate a rough initial linear alignemnt
flirt(infile, ref, omat=affmat)
fnirt(infile, ref, outfile, aff=affmat, **kwargs)
```
# Function inputs and outputs
In Python, arguments to a function can be specified in two different ways - by
using _positional_ arguments, or by using _keyword_ arguments.
## Positional arguments
Let's say we have a function that looks like this
```
def myfunc(a, b, c):
print('First argument: ', a)
print('Second argument:', b)
print('Third argument: ', c)
```
If we call this function like so:
```
myfunc(1, 2, 3)
```
The values `1`, `2` and `3` get assigned to arguments `a`, `b`, and `c`
respectively, based on the position in which they are passed.
Python allows us to pass positional arguments into a function from a sequence,
using the star (`*`) operator. So we could store our arguments in a list or
tuple, and then pass the list straight in:
```
args = [3, 4, 5]
myfunc(*args)
```
You can think of the star operator as _unpacking_ the contents of the
sequence.
## Keyword arguments
Using keyword arguments allows us to pass arguments to a function in any order
we like. We could just as easily call our `myfunc` function like so, and get
the same result that we did earlier when using positional arguments:
```
myfunc(c=3, b=2, a=1)
```
Python has another operator - the double-star (`**`), which will unpack
keyword arguments from a `dict`. For example:
```
kwargs = {'a' : 4, 'b' : 5, 'c' : 6}
myfunc(**kwargs)
```
## Combining positional and keyword arguments
In fact, we can use both of these techniques at once, like so:
```
args = (100, 200)
kwargs = {'c' : 300}
myfunc(*args, **kwargs)
```
## Default argument values
Function arguments can be given default values, like so:
```
def myfunc(a=1, b=2, c=3):
print('First argument: ', a)
print('Second argument:', b)
print('Third argument: ', c)
```
Now we can call `myfunc`, only passing the arguments that we need to. The
arguments which are unspecified in the function call will be assigned their
default value:
```
myfunc()
myfunc(10)
myfunc(10, b=30)
myfunc(c=300)
```
__WARNING:__ _Never_ define a function with a mutable default value, such as a
`list`, `dict` or other non-primitive type. Let's see what happens when we do:
```
def badfunc(a=[]):
a.append('end of sequence')
output = ', '.join([str(elem) for elem in a])
print(output)
```
With this function, all is well and good if we pass in our own value for `a`:
```
badfunc([1, 2, 3, 4])
badfunc([2, 4, 6])
```
But what happens when we let `badfunc` use the default value for `a`?
```
badfunc()
badfunc()
badfunc()
```
This happens because default argument values are created when the function is
defined, and will persist for the duration of your program. So in this
example, the default value for `a`, a Python `list`, gets created when
`badfunc` is defined, and hangs around for the lifetime of the `badfunc`
function!
## Variable numbers of arguments - `args` and `kwargs`
The `*` and `**` operators can also be used in function definitions - this
indicates that a function may accept a variable number of arguments.
Let's redefine `myfunc` to accept any number of positional arguments - here,
all positional arguments will be passed into `myfunc` as a tuple called
`args`:
```
def myfunc(*args):
print('myfunc({})'.format(args))
print(' Number of arguments: {}'.format(len(args)))
for i, arg in enumerate(args):
print(' Argument {:2d}: {}'.format(i, arg))
myfunc()
myfunc(1)
myfunc(1, 2, 3)
myfunc(1, 'a', [3, 4])
```
Similarly, we can define a function to accept any number of keyword
arguments. In this case, the keyword arguments will be packed into a `dict`
called `kwargs`:
```
def myfunc(**kwargs):
print('myfunc({})'.format(kwargs))
for k, v in kwargs.items():
print(' Argument {} = {}'.format(k, v))
myfunc()
myfunc(a=1, b=2)
myfunc(a='abc', foo=123)
```
This is a useful technique in many circumstances. For example, if you are
writing a function which calls another function that takes many arguments, you
can use ``**kwargs`` to pass-through arguments to the second function. As an
example, let's say we have functions `flirt` and `fnirt`, which respectively
perform linear and non-linear registration:
```
def flirt(infile,
ref,
outfile=None,
init=None,
omat=None,
dof=12):
# TODO get MJ to fill this bit in
pass
def fnirt(infile,
ref,
outfile=None,
aff=None,
interp='nn',
refmask=None,
minmet='lg',
subsamp=4):
# TODO get Jesper to fill this bit in
pass
```
We want to write our own registration function which uses the `flirt` and
`fnirt` functions, while also allowing the `fnirt` parameters to be
customised. We can use `**kwargs` to do this:
```
def do_nonlinear_reg(infile, ref, outfile, **kwargs):
"""Aligns infile to ref using non-linear registration. All keyword
arguments are passed through to the fnirt function.
"""
affmat = '/tmp/aff.mat'
# calculate a rough initial linear alignemnt
flirt(infile, ref, omat=affmat)
fnirt(infile, ref, outfile, aff=affmat, **kwargs)
```
%% 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 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
`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('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 `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 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 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.
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
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 `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 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
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*.
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 `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/tutorial/modules.html)
* [Using `__init__.py`](http://mikegrouchy.com/blog/2012/05/be-pythonic-__init__py.html)
# 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 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
`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)
```
import os
os.chdir('02_modules_and_packages')
```
<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 `02_modules_and_packages/numfuncs.py` - either open it in your editor, or
run this code block:
```
with open('numfuncs.py', 'rt') as f:
for line in f:
print(line.rstrip())
```
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 in the local scope. We can import the
contents of `numfuncs` like so:
```
import numfuncs
```
This imports `numfuncs` into the local scope - everything defined in the
`numfuncs` module can be accessed by prefixing it with `numfuncs.`:
```
print('PI:', numfuncs.PI)
print(numfuncs.add(1, 50))
```
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:
```
from numfuncs import add
print(add(1, 3))
```
<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:
```
from numfuncs import *
print('PI: ', PI)
print(add(1, 5))
```
__PLEASE DON'T DO THIS!__ Because every time you do, somewhere in the world, a
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:
```
from numfuncs import *
from strfuncs import *
print(add(1, 5))
```
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:
```
import numfuncs
import strfuncs
print('number add: ', numfuncs.add(1, 2))
print('string add: ', strfuncs.add(1, 2))
```
<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:
```
import numfuncs as nf
import strfuncs as sf
print('number add: ', nf.add(1, 2))
print('string add: ', sf.add(1, 2))
```
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:
```
import os.path as op
import numpy as np
import nibabel as nib
import matplotlib as mpl
import matplotlib.pyplot as plt
```
<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:
```
import sideeffects
```
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:
```
import sideeffects
```
<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.
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
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 `02_modules_and_packages/module_and_script.py`:
```
with open('module_and_script.py', 'rt') as f:
for line in f:
print(line.rstrip())
```
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 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.
```
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)])
```
<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
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*.
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:
```
import fsleyes.main as fmain
fmain.fsleyes_main()
```
<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 `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:
```
import fsleyes
fsleyes.fsleyes_main()
```
<a class="anchor" id="useful-references"></a>
## Useful references
* [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)
#!/usr/bin/env python
from fsleyes.main import fsleyes_main
#!/usr/bin/env python
def fsleyes_main():
print('Woo, you\'ve started a mock version of FSLeyes!')
#!/usr/bin/env python
import sys
def mul(a, b):
"""Multiply two numbers together. """
return a * b
def main(args=None):
"""Read in command line arguments,
and call the mul function.
"""
if args is None:
args = sys.argv[1:]
if len(args) != 2:
print('Usage: module_and_scripy.py a b')
sys.exit(1)
a = float(args[0])
b = float(args[1])
print('{} * {}: {}'.format(a, b, mul(a, b)))
# If this module is executed as a
# script, call the main function
if __name__ == '__main__':
main()
#!/usr/bin/env python
# See: https://fsl.fmrib.ox.ac.uk/fslcourse/lectures/scripting/_0200.fpd/cheat3
PI = 3.1417
def add(a, b):
return float(a) + float(b)
#!/usr/bin/env python
print('Hah, you\'ve imported the sideeffects module! '
'You\'ll never see this message again!')
#!/usr/bin/env python
def add(a, b):
return str(a) + str(b)
This diff is collapsed.
This diff is collapsed.