diff --git a/README.md b/README.md index 003a8506907312d2adf2eaf8dced61cf8119f45a..c3ca8ace3c12aaf0c3598b2f4c18cf3d3999be3b 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,62 @@ This repository contains Jupyter notebooks and data for the 2018 WIN PyTreat. +It contains two sets of practicals: + +- 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. + +- 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. + + +These practicals have been written under the assumption that FSL 5.0.10 is +installed. + + +## For attendees + + +To run these notebooks in the `fslpython` environment, you must first install +jupyter: + + +``` +# If your FSL installation requires administrative privileges to modify, then +# you MUST run these commands as root - don't just prefix each individual +# command with sudo, or you will probably install jupyter into the wrong +# location! +# +# One further complication - once you have become root, $FSLDIR may not be set, +# so either set it as we have ione below, or make sure that it is set, before +# proceeding. +sudo su +export FSLDIR=/usr/local/fsl +source $FSLDIR/fslpython/bin/activate fslpython +conda install jupyter +source deactivate +ln -s $FSLDIR/fslpython/envs/fslpython/bin/jupyter $FSLDIR/bin/fsljupyter +``` + + +Then, clone this repository on your local machine, and run +`fsljupyter notebook`: + + +``` +git clone git@git.fmrib.ox.ac.uk:fsl/pytreat-2018-practicals.git +cd pytreat-2018-practicals +fsljupyter notebook +``` + + +Have fun! + + +## For contributors The upstream repository can be found at: @@ -39,44 +95,27 @@ To contribute to the practicals: repository. -To run these notebooks in the `fslpython` environment, you must first install -jupyter: - +When you install `jupyter` above, you may also wish to install +[`notedown`](https://github.com/aaren/notedown): ``` -# 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 +# . +# 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/jupyter $FSLDIR/bin/fsljupyter 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: -> [`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: -> -> ``` -> fslnotedown my_markdown_file.md > my_notebook.ipynb -> ``` - - -Now you can start the notebook server from the repository root: ``` -fsljupyter notebook +fslnotedown my_markdown_file.md > my_notebook.ipynb ``` diff --git a/advanced_topics/README.md b/advanced_topics/README.md new file mode 100644 index 0000000000000000000000000000000000000000..86d80881f592e895e31dfc360220b1df3eea6925 --- /dev/null +++ b/advanced_topics/README.md @@ -0,0 +1,16 @@ +Advanced Python +=============== + +This directory contains a collection of practicals, each of which covers a +particular feature of the Python Programming language. They are intended for +people who are familiar with the basics of Python, and want to learn about +some of the more advanced features of the language. + +Practicals on the following topics are available: + +* Function inputs and outputs +* Modules and packages +* Object-oriented programming +* Operator overloading +* Decorators +* Context managers diff --git a/advanced_topics/function_inputs_and_outputs.ipynb b/advanced_topics/function_inputs_and_outputs.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..d18fc09a26b3d57104362c75f46938c0ac4e4a65 --- /dev/null +++ b/advanced_topics/function_inputs_and_outputs.ipynb @@ -0,0 +1,370 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Function inputs and outputs\n", + "\n", + "\n", + "In Python, arguments to a function can be specified in two different ways - by\n", + "using _positional_ arguments, or by using _keyword_ arguments.\n", + "\n", + "\n", + "## Positional arguments\n", + "\n", + "\n", + "Let's say we have a function that looks like this" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def myfunc(a, b, c):\n", + " print('First argument: ', a)\n", + " print('Second argument:', b)\n", + " print('Third argument: ', c)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If we call this function like so:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "myfunc(1, 2, 3)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The values `1`, `2` and `3` get assigned to arguments `a`, `b`, and `c`\n", + "respectively, based on the position in which they are passed.\n", + "\n", + "\n", + "Python allows us to pass positional arguments into a function from a sequence,\n", + "using the star (`*`) operator. So we could store our arguments in a list or\n", + "tuple, and then pass the list straight in:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "args = [3, 4, 5]\n", + "myfunc(*args)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can think of the star operator as _unpacking_ the contents of the\n", + "sequence.\n", + "\n", + "\n", + "## Keyword arguments\n", + "\n", + "\n", + "Using keyword arguments allows us to pass arguments to a function in any order\n", + "we like. We could just as easily call our `myfunc` function like so, and get\n", + "the same result that we did earlier when using positional arguments:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "myfunc(c=3, b=2, a=1)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Python has another operator - the double-star (`**`), which will unpack\n", + "keyword arguments from a `dict`. For example:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "kwargs = {'a' : 4, 'b' : 5, 'c' : 6}\n", + "myfunc(**kwargs)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Combining positional and keyword arguments\n", + "\n", + "\n", + "In fact, we can use both of these techniques at once, like so:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "args = (100, 200)\n", + "kwargs = {'c' : 300}\n", + "\n", + "myfunc(*args, **kwargs)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Default argument values\n", + "\n", + "\n", + "Function arguments can be given default values, like so:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def myfunc(a=1, b=2, c=3):\n", + " print('First argument: ', a)\n", + " print('Second argument:', b)\n", + " print('Third argument: ', c)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we can call `myfunc`, only passing the arguments that we need to. The\n", + "arguments which are unspecified in the function call will be assigned their\n", + "default value:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "myfunc()\n", + "myfunc(10)\n", + "myfunc(10, b=30)\n", + "myfunc(c=300)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "__WARNING:__ _Never_ define a function with a mutable default value, such as a\n", + "`list`, `dict` or other non-primitive type. Let's see what happens when we do:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def badfunc(a=[]):\n", + " a.append('end of sequence')\n", + " output = ', '.join([str(elem) for elem in a])\n", + " print(output)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "With this function, all is well and good if we pass in our own value for `a`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "badfunc([1, 2, 3, 4])\n", + "badfunc([2, 4, 6])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "But what happens when we let `badfunc` use the default value for `a`?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "badfunc()\n", + "badfunc()\n", + "badfunc()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This happens because default argument values are created when the function is\n", + "defined, and will persist for the duration of your program. So in this\n", + "example, the default value for `a`, a Python `list`, gets created when\n", + "`badfunc` is defined, and hangs around for the lifetime of the `badfunc`\n", + "function!\n", + "\n", + "\n", + "## Variable numbers of arguments - `args` and `kwargs`\n", + "\n", + "\n", + "The `*` and `**` operators can also be used in function definitions - this\n", + "indicates that a function may accept a variable number of arguments.\n", + "\n", + "\n", + "Let's redefine `myfunc` to accept any number of positional arguments - here,\n", + "all positional arguments will be passed into `myfunc` as a tuple called\n", + "`args`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def myfunc(*args):\n", + " print('myfunc({})'.format(args))\n", + " print(' Number of arguments: {}'.format(len(args)))\n", + " for i, arg in enumerate(args):\n", + " print(' Argument {:2d}: {}'.format(i, arg))\n", + "\n", + "myfunc()\n", + "myfunc(1)\n", + "myfunc(1, 2, 3)\n", + "myfunc(1, 'a', [3, 4])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Similarly, we can define a function to accept any number of keyword\n", + "arguments. In this case, the keyword arguments will be packed into a `dict`\n", + "called `kwargs`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def myfunc(**kwargs):\n", + " print('myfunc({})'.format(kwargs))\n", + " for k, v in kwargs.items():\n", + " print(' Argument {} = {}'.format(k, v))\n", + "\n", + "myfunc()\n", + "myfunc(a=1, b=2)\n", + "myfunc(a='abc', foo=123)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This is a useful technique in many circumstances. For example, if you are\n", + "writing a function which calls another function that takes many arguments, you\n", + "can use ``**kwargs`` to pass-through arguments to the second function. As an\n", + "example, let's say we have functions `flirt` and `fnirt`, which respectively\n", + "perform linear and non-linear registration:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def flirt(infile,\n", + " ref,\n", + " outfile=None,\n", + " init=None,\n", + " omat=None,\n", + " dof=12):\n", + " # TODO get MJ to fill this bit in\n", + " pass\n", + "\n", + "def fnirt(infile,\n", + " ref,\n", + " outfile=None,\n", + " aff=None,\n", + " interp='nn',\n", + " refmask=None,\n", + " minmet='lg',\n", + " subsamp=4):\n", + " # TODO get Jesper to fill this bit in\n", + " pass" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We want to write our own registration function which uses the `flirt` and\n", + "`fnirt` functions, while also allowing the `fnirt` parameters to be\n", + "customised. We can use `**kwargs` to do this:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def do_nonlinear_reg(infile, ref, outfile, **kwargs):\n", + " \"\"\"Aligns infile to ref using non-linear registration. All keyword\n", + " arguments are passed through to the fnirt function.\n", + " \"\"\"\n", + "\n", + " affmat = '/tmp/aff.mat'\n", + "\n", + " # calculate a rough initial linear alignemnt\n", + " flirt(infile, ref, omat=affmat)\n", + "\n", + " fnirt(infile, ref, outfile, aff=affmat, **kwargs)" + ] + } + ], + "metadata": {}, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/advanced_topics/function_inputs_and_outputs.md b/advanced_topics/function_inputs_and_outputs.md new file mode 100644 index 0000000000000000000000000000000000000000..234cea469dfa5ee0ac20e6f4d2c416c4c81cedd9 --- /dev/null +++ b/advanced_topics/function_inputs_and_outputs.md @@ -0,0 +1,237 @@ +# 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) +``` diff --git a/advanced_topics/modules_and_packages.ipynb b/advanced_topics/modules_and_packages.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..cb1f719189b60dd94728baebb94c4bf7d1dde077 --- /dev/null +++ b/advanced_topics/modules_and_packages.ipynb @@ -0,0 +1,518 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Modules and packages\n", + "\n", + "\n", + "Python gives you a lot of flexibility in how you organise your code. If you\n", + "want, you can write a Python program just as you would write a Bash script.\n", + "You don't _have_ to use functions, classes, modules or packages if you don't\n", + "want to, or if the script's task does not require them.\n", + "\n", + "\n", + "But when your code starts to grow beyond what can reasonably be defined in a\n", + "single file, you will (hopefully) want to start arranging it in a more\n", + "understandable manner.\n", + "\n", + "\n", + "For this practical we have prepared a handful of example files - you can find\n", + "them alongside this notebook file, in a directory called\n", + "`modules_and_packages/`.\n", + "\n", + "\n", + "## Contents\n", + "\n", + "* [What is a module?](#what-is-a-module)\n", + "* [Importing modules](#importing-modules)\n", + " * [Importing specific items from a module](#importing-specific-items-from-a-module)\n", + " * [Importing everything from a module](#importing-everything-from-a-module)\n", + " * [Module aliases](#module-aliases)\n", + " * [What happens when I import a module?](#what-happens-when-i-import-a-module)\n", + " * [How can I make my own modules importable?](#how-can-i-make-my-own-modules-importable)\n", + "* [Modules versus scripts](#modules-versus-scripts)\n", + "* [What is a package?](#what-is-a-package)\n", + " * [`__init__.py`](#init-py)\n", + "* [Useful references](#useful-references)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "os.chdir('modules_and_packages')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "<a class=\"anchor\" id=\"what-is-a-module\"></a>\n", + "## What is a module?\n", + "\n", + "\n", + "Any file ending with `.py` is considered to be a module in Python. Take a look\n", + "at `modules_and_packages/numfuncs.py` - either open it in your editor, or run\n", + "this code block:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "with open('numfuncs.py', 'rt') as f:\n", + " for line in f:\n", + " print(line.rstrip())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This is a perfectly valid Python module, although not a particularly useful\n", + "one. It contains an attribute called `PI`, and a function `add`.\n", + "\n", + "\n", + "<a class=\"anchor\" id=\"importing-modules\"></a>\n", + "## Importing modules\n", + "\n", + "\n", + "Before we can use our module, we must `import` it. Importing a module in\n", + "Python will make its contents available to the local scope. We can import the\n", + "contents of `mymodule` like so:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numfuncs" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This imports `numfuncs` into the local scope - everything defined in the\n", + "`numfuncs` module can be accessed by prefixing it with `numfuncs.`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print('PI:', numfuncs.PI)\n", + "print(numfuncs.add(1, 50))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There are a couple of other ways to import items from a module...\n", + "\n", + "\n", + "<a class=\"anchor\" id=\"importing-specific-items-from-a-module\"></a>\n", + "### Importing specific items from a module\n", + "\n", + "\n", + "If you only want to use one, or a few items from a module, you can import just\n", + "those items - a reference to just those items will be created in the local\n", + "scope:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from numfuncs import add\n", + "print(add(1, 3))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "<a class=\"anchor\" id=\"importing-everything-from-a-module\"></a>\n", + "### Importing everything from a module\n", + "\n", + "\n", + "It is possible to import _everything_ that is defined in a module like so:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from numfuncs import *\n", + "print('PI: ', PI)\n", + "print(add(1, 5))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "__PLEASE DON'T DO THIS!__ Because every time you do, somewhere in the world, a\n", + "software developer will will spontaneously stub his/her toe, and start crying.\n", + "Using this approach can make more complicated programs very difficult to read,\n", + "because it is not possible to determine the origin of the functions and\n", + "attributes that are being used.\n", + "\n", + "\n", + "And naming collisions are inevitable when importing multiple modules in this\n", + "way, making it very difficult for somebody else to figure out what your code\n", + "is doing:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from numfuncs import *\n", + "from strfuncs import *\n", + "\n", + "print(add(1, 5))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Instead, it is better to give modules a name when you import them. While this\n", + "requires you to type more code, the benefits of doing this far outweigh the\n", + "hassle of typing a few extra characters - it becomes much easier to read and\n", + "trace through code when the functions you use are accessed through a namespace\n", + "for each module:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numfuncs\n", + "import strfuncs\n", + "print('number add: ', numfuncs.add(1, 2))\n", + "print('string add: ', strfuncs.add(1, 2))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "<a class=\"anchor\" id=\"module-aliases\"></a>\n", + "### Module aliases\n", + "\n", + "\n", + "And Python allows you to define an _alias_ for a module when you import it,\n", + "so you don't necessarily need to type out the full module name each time\n", + "you want to access something inside:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numfuncs as nf\n", + "import strfuncs as sf\n", + "print('number add: ', nf.add(1, 2))\n", + "print('string add: ', sf.add(1, 2))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You have already seen this in the earlier practicals - here are a few\n", + "aliases which have become a de-facto standard for commonly used Python\n", + "modules:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os.path as op\n", + "import numpy as np\n", + "import nibabel as nib\n", + "import matplotlib as mpl\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "<a class=\"anchor\" id=\"what-happens-when-i-import-a-module\"></a>\n", + "### What happens when I import a module?\n", + "\n", + "\n", + "When you `import` a module, the contents of the module file are literally\n", + "executed by the Python runtime, exactly the same as if you had typed its\n", + "contents into `ipython`. Any attributes, functions, or classes which are\n", + "defined in the module will be bundled up into an object that represents the\n", + "module, and through which you can access the module's contents.\n", + "\n", + "\n", + "When we typed `import numfuncs` in the examples above, the following events\n", + "occurred:\n", + "\n", + "\n", + "1. Python created a `module` object to represent the module.\n", + "\n", + "2. The `numfuncs.py` file was read and executed, and all of the items defined\n", + " inside `numfuncs.py` (i.e. the `PI` attribute and the `add` function) were\n", + " added to the `module` object.\n", + "\n", + "3. A local variable called `numfuncs`, pointing to the `module` object,\n", + " was added to the local scope.\n", + "\n", + "\n", + "Because module files are literally executed on import, any statements in the\n", + "module file which are not encapsulated inside a class or function will be\n", + "executed. As an example, take a look at the file `sideeffects.py`. Let's\n", + "import it and see what happens:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import sideeffects" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Ok, hopefully that wasn't too much of a surprise. Something which may be less\n", + "intuitive, however, is that a module's contents will only be executed on the\n", + "_first_ time that it is imported. After the first import, Python caches the\n", + "module's contents (all loaded modules are accessible through\n", + "[`sys.modules`](https://docs.python.org/3.5/library/sys.html#sys.modules)). On\n", + "subsequent imports, the cached version of the module is returned:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import sideeffects" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "<a class=\"anchor\" id=\"how-can-i-make-my-own-modules-importable\"></a>\n", + "### How can I make my own modules importable?\n", + "\n", + "\n", + "When you `import` a module, Python searches for it in the following locations,\n", + "in the following order:\n", + "\n", + "\n", + "1. Built-in modules (e.g. `os`, `sys`, etc.).\n", + "2. In the current directory or, if a script has been executed, in the directory\n", + " containing that script.\n", + "3. In directories listed in the PYTHONPATH environment variable.\n", + "4. In installed third-party libraries (e.g. `numpy`).\n", + "\n", + "\n", + "If you are experimenting or developing your program, the quickest and easiest\n", + "way to make your module(s) importable is to add their containing directory to\n", + "the `PYTHONPATH`. But if you are developing a larger piece of software, you\n", + "should probably organise your modules into _packages_, which are [described\n", + "below](#what-is-a-package).\n", + "\n", + "\n", + "<a class=\"anchor\" id=\"modules-versus-scripts\"></a>\n", + "## Modules versus scripts\n", + "\n", + "\n", + "You now know that Python treats all files ending in `.py` as importable\n", + "modules. But all files ending in `.py` can also be treated as scripts. In\n", + "fact, there no difference between a _module_ and a _script_ - any `.py` file\n", + "can be executed as a script, or imported as a module, or both.\n", + "\n", + "\n", + "Have a look at the file `modules_and_packages/module_and_script.py`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "with open('module_and_script.py', 'rt') as f:\n", + " for line in f:\n", + " print(line.rstrip())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This file contains two functions `mul` and `main`. The\n", + "`if __name__ == '__main__':` clause at the bottom is a standard trick in Python\n", + "that allows you to add code to a file that is _only executed when the module is\n", + "called as a script_. Try it in a terminal now:\n", + "\n", + "\n", + "> `python modules_and_packages/module_and_script.py`\n", + "\n", + "\n", + "But if we `import` this module from another file, or from an interactive\n", + "session, the code within the `if __name__ == '__main__':` clause will not be\n", + "executed, and we can access its functions just like any other module that we\n", + "import." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import module_and_script as mas\n", + "\n", + "a = 1.5\n", + "b = 3\n", + "\n", + "print('mul({}, {}): {}'.format(a, b, mas.mul(a, b)))\n", + "print('calling main...')\n", + "mas.main([str(a), str(b)])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "<a class=\"anchor\" id=\"what-is-a-package\"></a>\n", + "## What is a package?\n", + "\n", + "\n", + "You now know how to split your Python code up into separate files\n", + "(a.k.a. _modules_). When your code grows beyond a handful of files, you may\n", + "wish for more fine-grained control over the namespaces in which your modules\n", + "live. Python has another feature which allows you to organise your modules\n", + "into _packages_.\n", + "\n", + "\n", + "A package in Python is simply a directory which:\n", + "\n", + "\n", + "* Contains a special file called `__init__.py`\n", + "* May contain one or more module files (any other files ending in `*.py`)\n", + "* May contain other package directories.\n", + "\n", + "\n", + "For example, the [FSLeyes](https://git.fmrib.ox.ac.uk/fsl/fsleyes/fsleyes)\n", + "code is organised into packages and sub-packages as follows (abridged):\n", + "\n", + "\n", + "> ```\n", + "> fsleyes/\n", + "> __init__.py\n", + "> main.py\n", + "> frame.py\n", + "> views/\n", + "> __init__.py\n", + "> orthopanel.py\n", + "> lightboxpanel.py\n", + "> controls/\n", + "> __init__.py\n", + "> locationpanel.py\n", + "> overlaylistpanel.py\n", + "> ```\n", + "\n", + "\n", + "Within a package structure, we will typically still import modules directly,\n", + "via their full path within the package:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import fsleyes.main as fmain\n", + "fmain.fsleyes_main()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "<a class=\"anchor\" id=\"init-py\"></a>\n", + "### `__init__.py`\n", + "\n", + "\n", + "Every Python package must have an `__init__.py` file. In many cases, this will\n", + "actually be an empty file, and you don't need to worry about it any more, apart\n", + "from knowing that it is needed. But you can use `__init__.py` to perform some\n", + "package-specific initialisation, and/or to customise the package's namespace.\n", + "\n", + "\n", + "As an example, take a look the `modules_and_packages/fsleyes/__init__.py` file\n", + "in our mock FSLeyes package. We have imported the `fsleyes_main` function from\n", + "the `fsleyes.main` module, making it available at the package level. So\n", + "instead of importing the `fsleyes.main` module, we could instead just import\n", + "the `fsleyes` package:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import fsleyes\n", + "fsleyes.fsleyes_main()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "<a class=\"anchor\" id=\"useful-references\"></a>\n", + "## Useful references\n", + "\n", + "* [Modules and packages in Python](https://docs.python.org/3.5/tutorial/modules.html)\n", + "* [Using `__init__.py`](http://mikegrouchy.com/blog/2012/05/be-pythonic-__init__py.html)" + ] + } + ], + "metadata": {}, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/advanced_topics/modules_and_packages.md b/advanced_topics/modules_and_packages.md new file mode 100644 index 0000000000000000000000000000000000000000..f459a1ca08b47967a20e53ae2854342da8aa6049 --- /dev/null +++ b/advanced_topics/modules_and_packages.md @@ -0,0 +1,371 @@ +# Modules and packages + + +Python gives you a lot of flexibility in how you organise your code. If you +want, you can write a Python program just as you would write a Bash script. +You don't _have_ to use functions, classes, modules or packages if you don't +want to, or if the script's task does not require them. + + +But when your code starts to grow beyond what can reasonably be defined in a +single file, you will (hopefully) want to start arranging it in a more +understandable manner. + + +For this practical we have prepared a handful of example files - you can find +them alongside this notebook file, in a directory called +`modules_and_packages/`. + + +## 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('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 `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 to the local scope. We can import the +contents of `mymodule` 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 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 `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 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 `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.5/tutorial/modules.html) +* [Using `__init__.py`](http://mikegrouchy.com/blog/2012/05/be-pythonic-__init__py.html) \ No newline at end of file diff --git a/advanced_topics/modules_and_packages/fsleyes/__init__.py b/advanced_topics/modules_and_packages/fsleyes/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a1a8c37d86718ffad50b0252e40ed23b06fba17c --- /dev/null +++ b/advanced_topics/modules_and_packages/fsleyes/__init__.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python + +from fsleyes.main import fsleyes_main diff --git a/advanced_topics/modules_and_packages/fsleyes/controls/__init__.py b/advanced_topics/modules_and_packages/fsleyes/controls/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/advanced_topics/modules_and_packages/fsleyes/controls/locationpanel.py b/advanced_topics/modules_and_packages/fsleyes/controls/locationpanel.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/advanced_topics/modules_and_packages/fsleyes/controls/overlaylistpanel.py b/advanced_topics/modules_and_packages/fsleyes/controls/overlaylistpanel.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/advanced_topics/modules_and_packages/fsleyes/frame.py b/advanced_topics/modules_and_packages/fsleyes/frame.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/advanced_topics/modules_and_packages/fsleyes/main.py b/advanced_topics/modules_and_packages/fsleyes/main.py new file mode 100644 index 0000000000000000000000000000000000000000..9d52a171dd15863488e0d927da6004de7a9dae6a --- /dev/null +++ b/advanced_topics/modules_and_packages/fsleyes/main.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python + +def fsleyes_main(): + print('Woo, you\'ve started a mock version of FSLeyes!') diff --git a/advanced_topics/modules_and_packages/fsleyes/views/__init__.py b/advanced_topics/modules_and_packages/fsleyes/views/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/advanced_topics/modules_and_packages/fsleyes/views/lightboxpanel.py b/advanced_topics/modules_and_packages/fsleyes/views/lightboxpanel.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/advanced_topics/modules_and_packages/fsleyes/views/orthopanel.py b/advanced_topics/modules_and_packages/fsleyes/views/orthopanel.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/advanced_topics/modules_and_packages/module_and_script.py b/advanced_topics/modules_and_packages/module_and_script.py new file mode 100644 index 0000000000000000000000000000000000000000..42420b59a01dc1816bff1028b312d52b03d39cf0 --- /dev/null +++ b/advanced_topics/modules_and_packages/module_and_script.py @@ -0,0 +1,32 @@ +#!/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() diff --git a/advanced_topics/modules_and_packages/numfuncs.py b/advanced_topics/modules_and_packages/numfuncs.py new file mode 100644 index 0000000000000000000000000000000000000000..c250d8be3b5b2b328c94eab9322f2d991800ed82 --- /dev/null +++ b/advanced_topics/modules_and_packages/numfuncs.py @@ -0,0 +1,7 @@ +#!/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) diff --git a/advanced_topics/modules_and_packages/sideeffects.py b/advanced_topics/modules_and_packages/sideeffects.py new file mode 100644 index 0000000000000000000000000000000000000000..1ccc43430706bcf0aaf0354d50f8a87a006acac9 --- /dev/null +++ b/advanced_topics/modules_and_packages/sideeffects.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python + +print('Hah, you\'ve imported the sideeffects module! ' + 'You\'ll never see this message again!') diff --git a/advanced_topics/modules_and_packages/strfuncs.py b/advanced_topics/modules_and_packages/strfuncs.py new file mode 100644 index 0000000000000000000000000000000000000000..b9f7dd7f745f44b9a2c85bcfc41d81e91cad6905 --- /dev/null +++ b/advanced_topics/modules_and_packages/strfuncs.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python + +def add(a, b): + return str(a) + str(b) diff --git a/advanced_topics/object_oriented_programming.ipynb b/advanced_topics/object_oriented_programming.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..deca29f7e0d2aef7a45dcc7d3fa68c183ea42711 --- /dev/null +++ b/advanced_topics/object_oriented_programming.ipynb @@ -0,0 +1,1709 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Object-oriented programming in Python\n", + "\n", + "\n", + "By now you might have realised that __everything__ in Python is an\n", + "object. Strings are objects, numbers are objects, functions are objects,\n", + "modules are objects - __everything__ is an object!\n", + "\n", + "\n", + "But this does not mean that you have to use Python in an object-oriented\n", + "fashion. You can stick with functions and statements, and get quite a lot\n", + "done. But some problems are just easier to solve, and to reason about, when\n", + "you use an object-oriented approach.\n", + "\n", + "\n", + "* [Objects versus classes](#objects-versus-classes)\n", + "* [Defining a class](#defining-a-class)\n", + "* [Object creation - the `__init__` method](#object-creation-the-init-method)\n", + " * [Our method is called `__init__`, but we didn't actually call the `__init__` method!](#our-method-is-called-init)\n", + " * [We didn't specify the `self` argument - what gives?!?](#we-didnt-specify-the-self-argument)\n", + "* [Attributes](#attributes)\n", + "* [Methods](#methods)\n", + "* [Protecting attribute access](#protecting-attribute-access)\n", + " * [A better way - properties](#a-better-way-properties])\n", + "* [Inheritance](#inheritance)\n", + " * [The basics](#the-basics)\n", + " * [Code re-use and problem decomposition](#code-re-use-and-problem-decomposition)\n", + " * [Polymorphism](#polymorphism)\n", + " * [Multiple inheritance](#multiple-inheritance)\n", + "* [Class attributes and methods](#class-attributes-and-methods)\n", + " * [Class attributes](#class-attributes)\n", + " * [Class methods](#class-methods)\n", + "* [Appendix: The `object` base-class](#appendix-the-object-base-class)\n", + "* [Appendix: `__init__` versus `__new__`](#appendix-init-versus-new)\n", + "* [Appendix: Monkey-patching](#appendix-monkey-patching)\n", + "* [Appendix: Method overloading](#appendix-method-overloading)\n", + "* [Useful references](#useful-references)\n", + "\n", + "\n", + "<a class=\"anchor\" id=\"objects-versus-classes\"></a>\n", + "## Objects versus classes\n", + "\n", + "\n", + "If you are versed in C++, Java, C#, or some other object-oriented language,\n", + "then this should all hopefully sound familiar, and you can skip to the next\n", + "section.\n", + "\n", + "\n", + "If you have not done any object-oriented programming before, your first step\n", + "is to understand the difference between _objects_ (also known as\n", + "_instances_) and _classes_ (also known as _types_).\n", + "\n", + "\n", + "If you have some experience in C, then you can start off by thinking of a\n", + "class as like a `struct` definition - a `struct` is a specification for the\n", + "layout of a chunk of memory. For example, here is a typical struct definition:\n", + "\n", + "> ```\n", + "> /**\n", + "> * Struct representing a stack.\n", + "> */\n", + "> typedef struct __stack {\n", + "> uint8_t capacity; /**< the maximum capacity of this stack */\n", + "> uint8_t size; /**< the current size of this stack */\n", + "> void **top; /**< pointer to the top of this stack */\n", + "> } stack_t;\n", + "> ```\n", + "\n", + "\n", + "Now, an _object_ is not a definition, but rather a thing which resides in\n", + "memory. An object can have _attributes_ (pieces of information), and _methods_\n", + "(functions associated with the object). You can pass objects around your code,\n", + "manipulate their attributes, and call their methods.\n", + "\n", + "\n", + "Returning to our C metaphor, you can think of an object as like an\n", + "instantiation of a struct:\n", + "\n", + "\n", + "> ```\n", + "> stack_t stack;\n", + "> stack.capacity = 10;\n", + "> ```\n", + "\n", + "\n", + "One of the major differences between a `struct` in C, and a `class` in Python\n", + "and other object oriented languages, is that you can't (easily) add functions\n", + "to a `struct` - it is just a chunk of memory. Whereas in Python, you can add\n", + "functions to your class definition, which will then be added as methods when\n", + "you create an object from that class.\n", + "\n", + "\n", + "Of course there are many more differences between C structs and classes (most\n", + "notably [inheritance](todo), [polymorphism](todo), and [access\n", + "protection](todo)). But if you can understand the difference between a\n", + "_definition_ of a C struct, and an _instantiation_ of that struct, then you\n", + "are most of the way towards understanding the difference between a _class_,\n", + "and an _object_.\n", + "\n", + "\n", + "> But just to confuse you, remember that in Python, __everything__ is an\n", + "> object - even classes!\n", + "\n", + "\n", + "<a class=\"anchor\" id=\"defining-a-class\"></a>\n", + "## Defining a class\n", + "\n", + "\n", + "Defining a class in Python is simple. Let's take on a small project, by\n", + "developing a class which can be used in place of the `fslmaths` shell command." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class FSLMaths(object):\n", + " pass" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this statement, we defined a new class called `FSLMaths`, which inherits\n", + "from the built-in `object` base-class (see [below](inheritance) for more\n", + "details on inheritance).\n", + "\n", + "\n", + "Now that we have defined our class, we can create objects - instances of that\n", + "class - by calling the class itself, as if it were a function:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fm1 = FSLMaths()\n", + "fm2 = FSLMaths()\n", + "print(fm1)\n", + "print(fm2)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Although these objects are not of much use at this stage. Let's do some more\n", + "work.\n", + "\n", + "\n", + "<a class=\"anchor\" id=\"object-creation-the-init-method\"></a>\n", + "## Object creation - the `__init__` method\n", + "\n", + "\n", + "The first thing that our `fslmaths` replacement will need is an input image.\n", + "It makes sense to pass this in when we create an `FSLMaths` object:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class FSLMaths(object):\n", + " def __init__(self, inimg):\n", + " self.img = inimg" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here we have added a _method_ called `__init__` to our class (remember that a\n", + "_method_ is just a function which is defined in a class, and which can be\n", + "called on instances of that class). This method expects two arguments -\n", + "`self`, and `inimg`. So now, when we create an instance of the `FSLMaths`\n", + "class, we will need to provide an input image:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import nibabel as nib\n", + "import os.path as op\n", + "\n", + "fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')\n", + "inimg = nib.load(fpath)\n", + "fm = FSLMaths(inimg)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There are a couple of things to note here...\n", + "\n", + "\n", + "<a class=\"anchor\" id=\"our-method-is-called-init\"></a>\n", + "### Our method is called `__init__`, but we didn't actually call the `__init__` method!\n", + "\n", + "\n", + "`__init__` is a special method in Python - it is called when an instance of a\n", + "class is created. And recall that we can create an instance of a class by\n", + "calling the class in the same way that we call a function.\n", + "\n", + "\n", + "There are a number of \"special\" methods that you can add to a class in Python\n", + "to customise various aspects of how instances of the class behave. One of the\n", + "first ones you may come across is the `__str__` method, which defines how an\n", + "object should be printed (more specifically, how an object gets converted into\n", + "a string). For example, we could add a `__str__` method to our `FSLMaths`\n", + "class like so:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class FSLMaths(object):\n", + "\n", + " def __init__(self, inimg):\n", + " self.img = inimg\n", + "\n", + " def __str__(self):\n", + " return 'FSLMaths({})'.format(self.img.get_filename())\n", + "\n", + "fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')\n", + "inimg = nib.load(fpath)\n", + "fm = FSLMaths(inimg)\n", + "\n", + "print(fm)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Refer to the [official\n", + "docs](https://docs.python.org/3.5/reference/datamodel.html#special-method-names)\n", + "for details on all of the special methods that can be defined in a class. And\n", + "take a look at the appendix for some more details on [how Python objects get\n", + "created](appendix-init-versus-new).\n", + "\n", + "\n", + "<a class=\"anchor\" id=\"we-didnt-specify-the-self-argument\"></a>\n", + "### We didn't specify the `self` argument - what gives?!?\n", + "\n", + "\n", + "The `self` argument is a special argument for methods in Python. If you are\n", + "coming from C++, Java, C# or similar, `self` in Python is equivalent to `this`\n", + "in those languages.\n", + "\n", + "\n", + "In a method, the `self` argument is a reference to the object that the method\n", + "was called on. So in this line of code:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fm = FSLMaths(inimg)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "the `self` argument in `__init__` will be a reference to the `FSLMaths` object\n", + "that has been created (and is then assigned to the `fm` variable, after the\n", + "`__init__` method has finished).\n", + "\n", + "\n", + "But note that you __do not__ need to explicitly provide the `self` argument\n", + "when you call a method on an object, or when you create a new object. The\n", + "Python runtime will take care of passing the instance to its method, as the\n", + "first argument to the method.\n", + "\n", + "\n", + "But when you are writing a class, you __do__ need to explicitly list `self` as\n", + "the first argument to all of the methods of the class.\n", + "\n", + "\n", + "<a class=\"anchor\" id=\"attributes\"></a>\n", + "## Attributes\n", + "\n", + "\n", + "In Python, the term __attribute__ is used to refer to a piece of information\n", + "that is associated with an object. An attribute is generally a reference to\n", + "another object (which might be a string, a number, or a list, or some other\n", + "more complicated object).\n", + "\n", + "\n", + "Remember that we modified our `FSLMaths` class so that it is passed an input\n", + "image on creation:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class FSLMaths(object):\n", + " def __init__(self, inimg):\n", + " self.img = inimg\n", + "\n", + "fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')\n", + "fm = FSLMaths(nib.load(fpath))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Take a look at what is going on in the `__init__` method - we take the `inimg`\n", + "argument, and create a reference to it called `self.img`. We have added an\n", + "_attribute_ to the `FSLMaths` instance, called `img`, and we can access that\n", + "attribute like so:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print('Input for our FSLMaths instance: {}'.format(fm.img.get_filename()))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And that concludes the section on adding attributes to Python objects.\n", + "\n", + "\n", + "Just kidding. But it really is that simple. This is one aspect of Python which\n", + "might be quite jarring to you if you are coming from a language with more\n", + "rigid semantics, such as C++ or Java. In those languages, you must pre-specify\n", + "all of the attributes and methods that are a part of a class. But Python is\n", + "much more flexible - you can simply add attributes to an object after it has\n", + "been created. In fact, you can even do this outside of the class\n", + "definition<sup>1</sup>:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fm = FSLMaths(inimg)\n", + "fm.another_attribute = 'Haha'\n", + "print(fm.another_attribute)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "__But ...__ while attributes can be added to a Python object at any time, it is\n", + "good practice (and makes for more readable and maintainable code) to add all\n", + "of an object's attributes within the `__init__` method.\n", + "\n", + "\n", + "> <sup>1</sup>This not possible with many of the built-in types, such as\n", + "> `list` and `dict` objects, nor with types that are defined in Python\n", + "> extensions (Python modules that are written in C).\n", + "\n", + "\n", + "<a class=\"anchor\" id=\"methods\"></a>\n", + "## Methods\n", + "\n", + "\n", + "We've been dilly-dallying on this little `FSLMaths` project for a while now,\n", + "but our class still can't actually do anything. Let's start adding some\n", + "functionality:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class FSLMaths(object):\n", + "\n", + " def __init__(self, inimg):\n", + " self.img = inimg\n", + " self.operations = []\n", + "\n", + " def add(self, value):\n", + " self.operations.append(('add', value))\n", + "\n", + " def mul(self, value):\n", + " self.operations.append(('mul', value))\n", + "\n", + " def div(self, value):\n", + " self.operations.append(('div', value))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Woah woah, [slow down egg-head!](https://www.youtube.com/watch?v=yz-TemWooa4)\n", + "We've modified `__init__` so that a second attribute called `operations` is\n", + "added to our object - this `operations` attribute is simply a list.\n", + "\n", + "\n", + "Then, we added a handful of methods - `add`, `mul`, and `div` - which each\n", + "append a tuple to that `operations` list.\n", + "\n", + "\n", + "> Note that, just like in the `__init__` method, the first argument that will\n", + "> be passed to these methods is `self` - a reference to the object that the\n", + "> method has been called on.\n", + "\n", + "\n", + "The idea behind this design is that our `FSLMaths` class will not actually do\n", + "anything when we call the `add`, `mul` or `div` methods. Instead, it will\n", + "\"stage\" each operation, and then perform them all in one go. So let's add\n", + "another method, `run`, which actually does the work:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import nibabel as nib\n", + "\n", + "class FSLMaths(object):\n", + "\n", + " def __init__(self, inimg):\n", + " self.img = inimg\n", + " self.operations = []\n", + "\n", + " def add(self, value):\n", + " self.operations.append(('add', value))\n", + "\n", + " def mul(self, value):\n", + " self.operations.append(('mul', value))\n", + "\n", + " def div(self, value):\n", + " self.operations.append(('div', value))\n", + "\n", + " def run(self, output=None):\n", + "\n", + " data = np.array(self.img.get_data())\n", + "\n", + " for oper, value in self.operations:\n", + "\n", + " # Value could be an image.\n", + " # If not, we assume that\n", + " # it is a scalar/numpy array.\n", + " if isinstance(value, nib.nifti1.Nifti1Image):\n", + " value = value.get_data()\n", + "\n", + "\n", + " if oper == 'add':\n", + " data = data + value\n", + " elif oper == 'mul':\n", + " data = data * value\n", + " elif oper == 'div':\n", + " data = data / value\n", + "\n", + " # turn final output into a nifti,\n", + " # and save it to disk if an\n", + " # 'output' has been specified.\n", + " outimg = nib.nifti1.Nifti1Image(data, inimg.affine)\n", + "\n", + " if output is not None:\n", + " nib.save(outimg, output)\n", + "\n", + " return outimg" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We now have a useable (but not very useful) `FSLMaths` class!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')\n", + "fmask = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm_brain_mask.nii.gz')\n", + "inimg = nib.load(fpath)\n", + "mask = nib.load(fmask)\n", + "fm = FSLMaths(inimg)\n", + "\n", + "fm.mul(mask)\n", + "fm.add(-10)\n", + "\n", + "outimg = fm.run()\n", + "\n", + "norigvox = (inimg .get_data() > 0).sum()\n", + "nmaskvox = (outimg.get_data() > 0).sum()\n", + "\n", + "print('Number of voxels >0 in original image: {}'.format(norigvox))\n", + "print('Number of voxels >0 in masked image: {}'.format(nmaskvox))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "<a class=\"anchor\" id=\"protecting-attribute-access\"></a>\n", + "## Protecting attribute access\n", + "\n", + "\n", + "In our `FSLMaths` class, the input image was added as an attribute called\n", + "`img` to `FSLMaths` objects. We saw that it is easy to read the attributes\n", + "of an object - if we have a `FSLMaths` instance called `fm`, we can read its\n", + "input image via `fm.img`.\n", + "\n", + "\n", + "But it is just as easy to write the attributes of an object. What's to stop\n", + "some sloppy research assistant from overwriting our `img` attribute?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "inimg = nib.load(op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz'))\n", + "fm = FSLMaths(inimg)\n", + "fm.img = None\n", + "fm.run()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Well, the scary answer is ... there is __nothing__ stopping you from doing\n", + "whatever you want to a Python object. You can add, remove, and modify\n", + "attributes at will. You can even replace the methods of an existing object if\n", + "you like:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fm = FSLMaths(inimg)\n", + "\n", + "def myadd(value):\n", + " print('Oh no, I\\'m not going to add {} to '\n", + " 'your image. Go away!'.format(value))\n", + "\n", + "fm.add = myadd\n", + "fm.add(123)\n", + "\n", + "fm.mul = None\n", + "fm.mul(123)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "But you really shouldn't get into the habit of doing devious things like\n", + "this. Think of the poor souls who inherit your code years after you have left\n", + "the lab - if you go around overwriting all of the methods and attributes of\n", + "your objects, they are not going to have a hope in hell of understanding what\n", + "your code is actually doing, and they are not going to like you very\n", + "much. Take a look at the appendix for a [brief discussion on this\n", + "topic](appendix-monkey-patching).\n", + "\n", + "\n", + "Python tends to assume that programmers are \"responsible adults\", and hence\n", + "doesn't do much in the way of restricting access to the attributes or methods\n", + "of an object. This is in contrast to languages like C++ and Java, where the\n", + "notion of a private attribute or method is strictly enforced by the language.\n", + "\n", + "\n", + "However, there are a couple of conventions in Python that are [universally\n", + "adhered\n", + "to](https://docs.python.org/3.5/tutorial/classes.html#private-variables):\n", + "\n", + "* Class-level attributes and methods, and module-level attributes, functions,\n", + " and classes, which begin with a single underscore (`_`), should be\n", + " considered __protected__ - they are intended for internal use only, and\n", + " should not be considered part of the public API of a class or module. This\n", + " is not enforced by the language in any way<sup>2</sup> - remember, we are\n", + " all responsible adults here!\n", + "\n", + "* Class-level attributes and methods which begin with a double-underscore\n", + " (`__`) should be considered __private__. Python provides a weak form of\n", + " enforcement for this rule - any attribute or method with such a name will\n", + " actually be _renamed_ (in a standardised manner) at runtime, so that it is\n", + " not accessible through its original name (it is still accessible via its\n", + " [mangled\n", + " name](https://docs.python.org/3.5/tutorial/classes.html#private-variables)\n", + " though).\n", + "\n", + "\n", + "> <sup>2</sup> With the exception that module-level fields which begin with a\n", + "> single underscore will not be imported into the local scope via the\n", + "> `from [module] import *` techinque.\n", + "\n", + "\n", + "So with all of this in mind, we can adjust our `FSLMaths` class to discourage\n", + "our sloppy research assistant from overwriting the `img` attribute:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# remainder of definition omitted for brevity\n", + "class FSLMaths(object):\n", + " def __init__(self, inimg):\n", + " self.__img = inimg\n", + " self.__operations = []" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "But now we have lost the ability to read our `__img` attribute:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "inimg = nib.load(op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz'))\n", + "fm = FSLMaths(inimg)\n", + "print(fm.__img)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "<a class=\"anchor\" id=\"a-better-way-properties\"></a>\n", + "### A better way - properties\n", + "\n", + "\n", + "Python has a feature called\n", + "[`properties`](https://docs.python.org/3.5/library/functions.html#property),\n", + "which is a nice way of controlling access to the attributes of an object. We\n", + "can use properties by defining a \"getter\" method which can be used to access\n", + "our attributes, and \"decorating\" them with the `@property` decorator (we will\n", + "cover decorators in a later practical)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class FSLMaths(object):\n", + " def __init__(self, inimg):\n", + " self.__img = inimg\n", + " self.__operations = []\n", + "\n", + " @property\n", + " def img(self):\n", + " return self.__img" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "So we are still storing our input image as a private attribute, but now we\n", + "have made it available in a read-only manner via the public `img` property:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')\n", + "inimg = nib.load(fpath)\n", + "fm = FSLMaths(inimg)\n", + "\n", + "print(fm.img.get_filename())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that, even though we have defined `img` as a method, we can access it\n", + "like an attribute - this is due to the magic behind the `@property` decorator.\n", + "\n", + "\n", + "We can also define \"setter\" methods for a property. For example, we might wish\n", + "to add the ability for a user of our `FSLMaths` class to change the input\n", + "image after creation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class FSLMaths(object):\n", + " def __init__(self, inimg):\n", + " self.__img = None\n", + " self.__operations = []\n", + " self.img = inimg\n", + "\n", + " @property\n", + " def img(self):\n", + " return self.__img\n", + "\n", + " @img.setter\n", + " def img(self, value):\n", + " if not isinstance(value, nib.nifti1.Nifti1Image):\n", + " raise ValueError('value must be a NIFTI image!')\n", + " self.__img = value" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> Note that we used the `img` setter method within `__init__` to validate the\n", + "> initial `inimg` that was passed in during creation.\n", + "\n", + "\n", + "Property setters are a nice way to add validation logic for when an attribute\n", + "is assigned a value. In this example, an error will be raised if the new input\n", + "is not a NIFTI image." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')\n", + "inimg = nib.load(fpath)\n", + "fm = FSLMaths(inimg)\n", + "\n", + "print('Input: ', fm.img.get_filename())\n", + "\n", + "# let's change the input\n", + "# to a different image\n", + "fpath2 = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm_brain.nii.gz')\n", + "inimg2 = nib.load(fpath2)\n", + "fm.img = inimg2\n", + "\n", + "print('New input: ', fm.img.get_filename())\n", + "\n", + "print('This is going to explode')\n", + "fm.img = 'abcde'" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "<a class=\"anchor\" id=\"inheritance\"></a>\n", + "## Inheritance\n", + "\n", + "\n", + "One of the major advantages of an object-oriented programming approach is\n", + "_inheritance_ - the ability to define hierarchical relationships between\n", + "classes and instances.\n", + "\n", + "\n", + "<a class=\"anchor\" id=\"the-basics\"></a>\n", + "### The basics\n", + "\n", + "\n", + "My local veterinary surgery runs some Python code which looks like the\n", + "following, to assist the nurses in identifying an animal when it arrives at\n", + "the surgery:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class Animal(object):\n", + " def noiseMade(self):\n", + " raise NotImplementedError('This method must be '\n", + " 'implemented by sub-classes')\n", + "\n", + "class Dog(Animal):\n", + " def noiseMade(self):\n", + " return 'Woof'\n", + "\n", + "class TalkingDog(Dog):\n", + " def noiseMade(self):\n", + " return 'Hi Homer, find your soulmate!'\n", + "\n", + "class Cat(Animal):\n", + " def noiseMade(self):\n", + " return 'Meow'\n", + "\n", + "class Labrador(Dog):\n", + " pass\n", + "\n", + "class Chihuahua(Dog):\n", + " def noiseMade(self):\n", + " return 'Yap yap yap'" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Hopefully this example doesn't need much in the way of explanation - this\n", + "collection of classes captures a hierarchical relationship which exists in the\n", + "real world (and also captures the inherently annoying nature of\n", + "chihuahuas). For example, in the real world, all dogs are animals, but not all\n", + "animals are dogs. Therefore in our model, the `Dog` class has specified\n", + "`Animal` as its base class. We say that the `Dog` class _extends_, _derives\n", + "from_, or _inherits from_, the `Animal` class, and that all `Dog` instances\n", + "are also `Animal` instances (but not vice-versa).\n", + "\n", + "\n", + "What does that `noiseMade` method do? There is a `noiseMade` method defined\n", + "on the `Animal` class, but it has been re-implemented, or _overridden_ in the\n", + "`Dog`,\n", + "[`TalkingDog`](https://twitter.com/simpsonsqotd/status/427941665836630016?lang=en),\n", + "`Cat`, and `Chihuahua` classes (but not on the `Labrador` class). We can call\n", + "the `noiseMade` method on any `Animal` instance, but the specific behaviour\n", + "that we get is dependent on the specific type of animal." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "d = Dog()\n", + "l = Labrador()\n", + "c = Cat()\n", + "ch = Chihuahua()\n", + "\n", + "print('Noise made by dogs: {}'.format(d .noiseMade()))\n", + "print('Noise made by labradors: {}'.format(l .noiseMade()))\n", + "print('Noise made by cats: {}'.format(c .noiseMade()))\n", + "print('Noise made by chihuahuas: {}'.format(ch.noiseMade()))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that calling the `noiseMade` method on a `Labrador` instance resulted in\n", + "the `Dog.noiseMade` implementation being called.\n", + "\n", + "\n", + "<a class=\"anchor\" id=\"code-re-use-and-problem-decomposition\"></a>\n", + "### Code re-use and problem decomposition\n", + "\n", + "\n", + "Inheritance allows us to split a problem into smaller problems, and to re-use\n", + "code. Let's demonstrate this with a more involved (and even more contrived)\n", + "example. Imagine that a former colleague had written a class called\n", + "`Operator`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class Operator(object):\n", + "\n", + " def __init__(self):\n", + " super().__init__() # this line will be explained later\n", + " self.__operations = []\n", + " self.__opFuncs = {}\n", + "\n", + " @property\n", + " def operations(self):\n", + " return list(self.__operations)\n", + "\n", + " @property\n", + " def functions(self):\n", + " return dict(self.__opFuncs)\n", + "\n", + " def addFunction(self, name, func):\n", + " self.__opFuncs[name] = func\n", + "\n", + " def do(self, name, *values):\n", + " self.__operations.append((name, values))\n", + "\n", + " def preprocess(self, value):\n", + " return value\n", + "\n", + " def run(self, input):\n", + " data = self.preprocess(input)\n", + " for oper, vals in self.__operations:\n", + " func = self.__opFuncs[oper]\n", + " vals = [self.preprocess(v) for v in vals]\n", + " data = func(data, *vals)\n", + " return data" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This `Operator` class provides an interface and logic to execute a chain of\n", + "operations - an operation is some function which accepts one or more inputs,\n", + "and produce one output.\n", + "\n", + "\n", + "But it stops short of defining any operations. Instead, we can create another\n", + "class - a sub-class - which derives from the `Operator` class. This sub-class\n", + "will define the operations that will ultimately be executed by the `Operator`\n", + "class. All that the `Operator` class does is:\n", + "\n", + "- Allow functions to be registered with the `addFunction` method - all\n", + " registered functions can be used via the `do` method.\n", + "\n", + "- Stage an operation (using a registered function) via the `do` method. Note\n", + " that `do` allows any number of values to be passed to it, as we used the `*`\n", + " operator when specifying the `values` argument.\n", + "\n", + "- Run all staged operations via the `run` method - it passes an input through\n", + " all of the operations that have been staged, and then returns the final\n", + " result.\n", + "\n", + "\n", + "Let's define a sub-class:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class NumberOperator(Operator):\n", + "\n", + " def __init__(self):\n", + " super().__init__()\n", + " self.addFunction('add', self.add)\n", + " self.addFunction('mul', self.mul)\n", + " self.addFunction('negate', self.negate)\n", + "\n", + " def preprocess(self, value):\n", + " return float(value)\n", + "\n", + " def add(self, a, b):\n", + " return a + b\n", + "\n", + " def mul(self, a, b):\n", + " return a * b\n", + "\n", + " def negate(self, a):\n", + " return -a" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `NumberOperator` is a sub-class of `Operator`, which we can use for basic\n", + "numerical calculations. It provides a handful of simple numerical methods, but\n", + "the most interesting stuff is inside `__init__`.\n", + "\n", + "\n", + "> ```\n", + "> super().__init__()\n", + "> ```\n", + "\n", + "\n", + "This line invokes `Operator.__init__` - the initialisation method for the\n", + "`Operator` base-class.\n", + "\n", + "\n", + "In Python, we can use the [built-in `super`\n", + "method](https://docs.python.org/3.5/library/functions.html#super) to take care\n", + "of correctly calling methods that are defined in an object's base-class (or\n", + "classes, in the case of [multiple inheritance](multiple-inheritance)).\n", + "\n", + "\n", + "> The `super` function is one thing which changed between Python 2 and 3 -\n", + "> in Python 2, it was necessary to pass both the type and the instance\n", + "> to `super`. So it is common to see code that looks like this:\n", + ">\n", + "> ```\n", + "> def __init__(self):\n", + "> super(NumberOperator, self).__init__()\n", + "> ```\n", + ">\n", + "> Fortunately things are a lot cleaner in Python 3.\n", + "\n", + "\n", + "Let's move on to the next few lines in `__init__`:\n", + "\n", + "\n", + "> ```\n", + "> self.addFunction('add', self.add)\n", + "> self.addFunction('mul', self.mul)\n", + "> self.addFunction('negate', self.negate)\n", + "> ```\n", + "\n", + "\n", + "Here we are registering all of the functionality that is provided by the\n", + "`NumberOperator` class, via the `Operator.addFunction` method.\n", + "\n", + "\n", + "The `NumberOperator` class has also overridden the `preprocess` method, to\n", + "ensure that all values handled by the `Operator` are numbers. This method gets\n", + "called within the `Operator.run` method - for a `NumberOperator` instance, the\n", + "`NumberOperator.preprocess` method will get called<sup>1</sup>.\n", + "\n", + "\n", + "> <sup>1</sup> When a sub-class overrides a base-class method, it is still\n", + "> possible to access the base-class implementation [via the `super()`\n", + "> function](https://stackoverflow.com/a/4747427) (the preferred method), or by\n", + "> [explicitly calling the base-class\n", + "> implementation](https://stackoverflow.com/a/2421325).\n", + "\n", + "\n", + "Now let's see what our `NumberOperator` class does:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "no = NumberOperator()\n", + "no.do('add', 10)\n", + "no.do('mul', 2)\n", + "no.do('negate')\n", + "\n", + "print('Operations on {}: {}'.format(10, no.run(10)))\n", + "print('Operations on {}: {}'.format(2.5, no.run(5)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It works! While this is a contrived example, hopefully you can see how\n", + "inheritance can be used to break a problem down into sub-problems:\n", + "\n", + "- The `Operator` class provides all of the logic needed to manage and execute\n", + " operations, without caring about what those operations are actually doing.\n", + "\n", + "- This leaves the `NumberOperator` class free to concentrate on implementing\n", + " the functions that are specific to its task, and not having to worry about\n", + " how they are executed.\n", + "\n", + "\n", + "We could also easily implement other `Operator` sub-classes to work on\n", + "different data types, such as arrays, images, or even non-numeric data such as\n", + "strings:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class StringOperator(Operator):\n", + " def __init__(self):\n", + " super().__init__()\n", + " self.addFunction('capitalise', self.capitalise)\n", + " self.addFunction('concat', self.concat)\n", + "\n", + " def preprocess(self, value):\n", + " return str(value)\n", + "\n", + " def capitalise(self, s):\n", + " return ' '.join([w[0].upper() + w[1:] for w in s.split()])\n", + "\n", + " def concat(self, s1, s2):\n", + " return s1 + s2\n", + "\n", + "so = StringOperator()\n", + "so.do('capitalise')\n", + "so.do('concat', '!')\n", + "\n", + "print(so.run('python is an ok language'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "<a class=\"anchor\" id=\"polymorphism\"></a>\n", + "### Polymorphism\n", + "\n", + "\n", + "Inheritance also allows us to take advantage of _polymorphism_, which refers\n", + "to idea that, in an object-oriented language, we should be able to use an\n", + "object without having complete knowledge about the class, or type, of that\n", + "object. For example, we should be able to write a function which expects an\n", + "`Operator` instance, but which will work on an instance of any `Operator`\n", + "sub-classs. As an example, let's write a function which prints a summary of an\n", + "`Operator` instance:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def operatorSummary(o):\n", + " print(type(o).__name__)\n", + " print(' All functions: ')\n", + " for fname in o.functions.keys():\n", + " print(' {}'.format(fname))\n", + " print(' Staged operations: ')\n", + " for i, (fname, vals) in enumerate(o.operations):\n", + " vals = ', '.join([str(v) for v in vals])\n", + " print(' {}: {}({})'.format(i + 1, fname, vals))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Because the `operatorSummary` function only uses methods that are defined\n", + "in the `Operator` base-class, we can use it on _any_ `Operator` instance,\n", + "regardless of its specific type:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "operatorSummary(no)\n", + "operatorSummary(so)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "<a class=\"anchor\" id=\"multiple-inheritance\"></a>\n", + "### Multiple inheritance\n", + "\n", + "\n", + "Python allows you to define a class which has multiple base classes - this is\n", + "known as _multiple inheritance_. For example, we might want to build a\n", + "notification mechanisim into our `StringOperator` class, so that listeners can\n", + "be notified whenever the `capitalise` method gets called. It so happens that\n", + "our old colleague of `Operator` class fame also wrote a `Notifier` class which\n", + "allows listeners to register to be notified when an event occurs:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class Notifier(object):\n", + "\n", + " def __init__(self):\n", + " super().__init__()\n", + " self.__listeners = {}\n", + "\n", + " def register(self, name, func):\n", + " self.__listeners[name] = func\n", + "\n", + " def notify(self, *args, **kwargs):\n", + " for func in self.__listeners.values():\n", + " func(*args, **kwargs)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's modify the `StringOperator` class to use the functionality of the\n", + "`Notifier ` class:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class StringOperator(Operator, Notifier):\n", + "\n", + " def __init__(self):\n", + " super().__init__()\n", + " self.addFunction('capitalise', self.capitalise)\n", + " self.addFunction('concat', self.concat)\n", + "\n", + " def preprocess(self, value):\n", + " return str(value)\n", + "\n", + " def capitalise(self, s):\n", + " result = ' '.join([w[0].upper() + w[1:] for w in s.split()])\n", + " self.notify(result)\n", + " return result\n", + "\n", + " def concat(self, s1, s2):\n", + " return s1 + s2" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, anything which is interested in uses of the `capitalise` method can\n", + "register as a listener on a `StringOperator` instance:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "so = StringOperator()\n", + "\n", + "def capitaliseCalled(result):\n", + " print('Capitalise operation called: {}'.format(result))\n", + "\n", + "so.register('mylistener', capitaliseCalled)\n", + "\n", + "so = StringOperator()\n", + "so.do('capitalise')\n", + "so.do('concat', '?')\n", + "\n", + "print(so.run('did you notice that functions are objects too'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If you wish to use multiple inheritance in your design, it is important to be\n", + "aware of the mechanism that Python uses to determine how base class methods\n", + "are called (and which base class method will be called, in the case of naming\n", + "conflicts). This is referred to as the Method Resolution Order (MRO) - further\n", + "details on the topic can be found\n", + "[here](https://www.python.org/download/releases/2.3/mro/), and a more concise\n", + "summary\n", + "[here](http://python-history.blogspot.co.uk/2010/06/method-resolution-order.html).\n", + "\n", + "\n", + "Note also that for base class `__init__` methods to be correctly called in a\n", + "design which uses multiple inheritance, _all_ classes in the hierarchy must\n", + "invoke `super().__init__()`. This can become complicated when some base\n", + "classes expect to be passed arguments to their `__init__` method. In scenarios\n", + "like this it may be prefereable to manually invoke the base class `__init__`\n", + "methods instead of using `super()`. For example:\n", + "\n", + "\n", + "> ```\n", + "> class StringOperator(Operator, Notifier):\n", + "> def __init__(self):\n", + "> Operator.__init__(self)\n", + "> Notifier.__init__(self)\n", + "> ```\n", + "\n", + "\n", + "This approach has the disadvantage that if the base classes change, you will\n", + "have to change these invocations. But the advantage is that you know exactly\n", + "how the class hierarchy will be initialised. In general though, doing\n", + "everything with `super()` will result in more maintainable code.\n", + "\n", + "\n", + "<a class=\"anchor\" id=\"class-attributes-and-methods\"></a>\n", + "## Class attributes and methods\n", + "\n", + "\n", + "Up to this point we have been covering how to add attributes and methods to an\n", + "_object_. But it is also possible to add methods and attributes to a _class_\n", + "(`static` methods and fields, for those of you familiar with C++ or Java).\n", + "\n", + "\n", + "Class attributes and methods can be accessed without having to create an\n", + "instance of the class - they are not associated with individual objects, but\n", + "rather with the class itself.\n", + "\n", + "\n", + "Class methods and attributes can be useful in several scenarios - as a\n", + "hypothetical, not very useful example, let's say that we want to gain usage\n", + "statistics for how many times each type of operation is used on instances of\n", + "our `FSLMaths` class. We might, for example, use this information in a grant\n", + "application to show evidence that more research is needed to optimise the\n", + "performance of the `add` operation.\n", + "\n", + "\n", + "<a class=\"anchor\" id=\"class-attributes\"></a>\n", + "### Class attributes\n", + "\n", + "\n", + "Let's add a `dict` called `opCounters` as a class attribute to the `FSLMaths`\n", + "class - whenever an operation is called on a `FSLMaths` instance, the counter\n", + "for that operation will be incremented:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import nibabel as nib\n", + "\n", + "class FSLMaths(object):\n", + "\n", + " # It's this easy to add a class-level\n", + " # attribute. This dict is associated\n", + " # with the FSLMaths *class*, not with\n", + " # any individual FSLMaths instance.\n", + " opCounters = {}\n", + "\n", + " def __init__(self, inimg):\n", + " self.img = inimg\n", + " self.operations = []\n", + "\n", + " def add(self, value):\n", + " self.operations.append(('add', value))\n", + "\n", + " def mul(self, value):\n", + " self.operations.append(('mul', value))\n", + "\n", + " def div(self, value):\n", + " self.operations.append(('div', value))\n", + "\n", + " def run(self, output=None):\n", + "\n", + " data = np.array(self.img.get_data())\n", + "\n", + " for oper, value in self.operations:\n", + "\n", + " # Code omitted for brevity\n", + "\n", + " # Increment the usage counter\n", + " # for this operation. We can\n", + " # access class attributes (and\n", + " # methods) through the class\n", + " # itself.\n", + " FSLMaths.opCounters[oper] = self.opCounters.get(oper, 0) + 1" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "So let's see it in action:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')\n", + "fmask = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm_brain_mask.nii.gz')\n", + "inimg = nib.load(fpath)\n", + "mask = nib.load(fmask)\n", + "\n", + "fm1 = FSLMaths(inimg)\n", + "fm2 = FSLMaths(inimg)\n", + "\n", + "fm1.mul(mask)\n", + "fm1.add(15)\n", + "\n", + "fm2.add(25)\n", + "fm1.div(1.5)\n", + "\n", + "fm1.run()\n", + "fm2.run()\n", + "\n", + "print('FSLMaths usage statistics')\n", + "for oper in ('add', 'div', 'mul'):\n", + " print(' {} : {}'.format(oper, FSLMaths.opCounters.get(oper, 0)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "<a class=\"anchor\" id=\"class-methods\"></a>\n", + "### Class methods\n", + "\n", + "\n", + "It is just as easy to add a method to a class - let's take our reporting code\n", + "from above, and add it as a method to the `FSLMaths` class.\n", + "\n", + "\n", + "A class method is denoted by the\n", + "[`@classmethod`](https://docs.python.org/3.5/library/functions.html#classmethod)\n", + "decorator. Note that, where a regular method which is called on an instance\n", + "will be passed the instance as its first argument (`self`), a class method\n", + "will be passed the class itself as the first argument - the standard\n", + "convention is to call this argument `cls`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class FSLMaths(object):\n", + "\n", + " opCounters = {}\n", + "\n", + " @classmethod\n", + " def usage(cls):\n", + " print('FSLMaths usage statistics')\n", + " for oper in ('add', 'div', 'mul'):\n", + " print(' {} : {}'.format(oper, FSLMaths.opCounters.get(oper, 0)))\n", + "\n", + " def __init__(self, inimg):\n", + " self.img = inimg\n", + " self.operations = []\n", + "\n", + " def add(self, value):\n", + " self.operations.append(('add', value))\n", + "\n", + " def mul(self, value):\n", + " self.operations.append(('mul', value))\n", + "\n", + " def div(self, value):\n", + " self.operations.append(('div', value))\n", + "\n", + " def run(self, output=None):\n", + "\n", + " data = np.array(self.img.get_data())\n", + "\n", + " for oper, value in self.operations:\n", + " FSLMaths.opCounters[oper] = self.opCounters.get(oper, 0) + 1" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> There is another decorator -\n", + "> [`@staticmethod`](https://docs.python.org/3.5/library/functions.html#staticmethod) -\n", + "> which can be used on methods defined within a class. The difference\n", + "> between a `@classmethod` and a `@staticmethod` is that the latter will _not_\n", + "> be passed the class (`cls`).\n", + "\n", + "\n", + "calling a class method is the same as accessing a class attribute:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')\n", + "fmask = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm_brain_mask.nii.gz')\n", + "inimg = nib.load(fpath)\n", + "mask = nib.load(fmask)\n", + "\n", + "fm1 = FSLMaths(inimg)\n", + "fm2 = FSLMaths(inimg)\n", + "\n", + "fm1.mul(mask)\n", + "fm1.add(15)\n", + "\n", + "fm2.add(25)\n", + "fm1.div(1.5)\n", + "\n", + "fm1.run()\n", + "fm2.run()\n", + "\n", + "FSLMaths.usage()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that it is also possible to access class attributes and methods through\n", + "instances:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(fm1.opCounters)\n", + "fm1.usage()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "<a class=\"anchor\" id=\"appendix-the-object-base-class\"></a>\n", + "## Appendix: The `object` base-class\n", + "\n", + "\n", + "When you are defining a class, you need to specify the base-class from which\n", + "your class inherits. If your class does not inherit from a particular class,\n", + "then it should inherit from the built-in `object` class:\n", + "\n", + "\n", + "> ```\n", + "> class MyClass(object):\n", + "> ...\n", + "> ```\n", + "\n", + "\n", + "However, in older code bases, you might see class definitions that look like\n", + "this, without explicitly inheriting from the `object` base class:\n", + "\n", + "\n", + "> ```\n", + "> class MyClass:\n", + "> ...\n", + "> ```\n", + "\n", + "\n", + "This syntax is a [throwback to older versions of\n", + "Python](https://docs.python.org/2/reference/datamodel.html#new-style-and-classic-classes).\n", + "In Python 3 there is actually no difference in defining classes in the\n", + "\"new-style\" way we have used throughout this tutorial, or the \"old-style\" way\n", + "mentioned in this appendix.\n", + "\n", + "\n", + "But if you are writing code which needs to run on both Python 2 and 3, you\n", + "__must__ define your classes in the new-style convention, i.e. by explicitly\n", + "inheriting from the `object` base class. Therefore, the safest approach is to\n", + "always use the new-style format.\n", + "\n", + "\n", + "<a class=\"anchor\" id=\"appendix-init-versus-new\"></a>\n", + "## Appendix: `__init__` versus `__new__`\n", + "\n", + "\n", + "In Python, object creation is actually a two-stage process - _creation_, and\n", + "then _initialisation_. The `__init__` method gets called during the\n", + "_initialisation_ stage - its job is to initialise the state of the object. But\n", + "note that, by the time `__init__` gets called, the object has already been\n", + "created.\n", + "\n", + "\n", + "You can also define a method called `__new__` if you need to control the\n", + "creation stage, although this is very rarely needed. A brief explanation on\n", + "the difference between `__new__` and `__init__` can be found\n", + "[here](https://www.reddit.com/r/learnpython/comments/2s3pms/what_is_the_difference_between_init_and_new/cnm186z/),\n", + "and you may also wish to take a look at the [official Python\n", + "docs](https://docs.python.org/3.5/reference/datamodel.html#basic-customization).\n", + "\n", + "\n", + "<a class=\"anchor\" id=\"appendix-monkey-patching\"></a>\n", + "## Appendix: Monkey-patching\n", + "\n", + "\n", + "The act of run-time modification of objects or class definitions is referred\n", + "to as [_monkey-patching_](https://en.wikipedia.org/wiki/Monkey_patch) and,\n", + "whilst it is allowed by the Python programming language, it is generally\n", + "considered quite bad practice.\n", + "\n", + "\n", + "Just because you _can_ do something doesn't mean that you _should_. Python\n", + "gives you the flexibility to write your software in whatever manner you deem\n", + "suitable. __But__ if you want to write software that will be used, adopted,\n", + "maintained, and enjoyed by other people, you should be polite, write your code\n", + "in a clear, readable fashion, and avoid the use of devious tactics such as\n", + "monkey-patching.\n", + "\n", + "\n", + "__However__, while monkey-patching may seem like a horrific programming\n", + "practice to those of you coming from the realms of C++, Java, and the like,\n", + "(and it is horrific in many cases), it can be _extremely_ useful in certain\n", + "circumstances. For instance, monkey-patching makes [unit testing a\n", + "breeze in Python](https://docs.python.org/3.5/library/unittest.mock.html).\n", + "\n", + "\n", + "As another example, consider the scenario where you are dependent on a third\n", + "party library which has bugs in it. No problem - while you are waiting for the\n", + "library author to release a new version of the library, you can write your own\n", + "working implementation and [monkey-patch it\n", + "in!](https://git.fmrib.ox.ac.uk/fsl/fsleyes/fsleyes/blob/0.21.0/fsleyes/views/viewpanel.py#L726)\n", + "\n", + "\n", + "<a class=\"anchor\" id=\"appendix-method-overloading\"></a>\n", + "## Appendix: Method overloading\n", + "\n", + "\n", + "Method overloading (defining multiple methods with the same name in a class,\n", + "but each accepting different arguments) is one of the only object-oriented\n", + "features that is not present in Python. Becuase Python does not perform any\n", + "runtime checks on the types of arguments that are passed to a method, or the\n", + "compatibility of the method to accept the arguments, it would not be possible\n", + "to determine which implementation of a method is to be called. In other words,\n", + "in Python only the name of a method is used to identify that method, unlike in\n", + "C++ and Java, where the full method signature (name, input types and return\n", + "types) is used.\n", + "\n", + "\n", + "However, because a Python method can be written to accept any number or type\n", + "of arguments, it is very easy to to build your own overloading logic by\n", + "writing a \"dispatch\" method. Here is YACE (Yet Another Contrived Example):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class Adder(object):\n", + "\n", + " def add(self, *args):\n", + " if len(args) == 2: return self.__add2(*args)\n", + " elif len(args) == 3: return self.__add3(*args)\n", + " elif len(args) == 4: return self.__add4(*args)\n", + " else:\n", + " raise AttributeError('No method available to accept {} '\n", + " 'arguments'.format(len(args)))\n", + "\n", + " def __add2(self, a, b):\n", + " return a + b\n", + "\n", + " def __add3(self, a, b, c):\n", + " return a + b + c\n", + "\n", + " def __add4(self, a, b, c, d):\n", + " return a + b + c + d\n", + "\n", + "a = Adder()\n", + "\n", + "print('Add two: {}'.format(a.add(1, 2)))\n", + "print('Add three: {}'.format(a.add(1, 2, 3)))\n", + "print('Add four: {}'.format(a.add(1, 2, 3, 4)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "<a class=\"anchor\" id=\"useful-references\"></a>\n", + "## Useful references\n", + "\n", + "\n", + "The official Python documentation has a wealth of information on the internal\n", + "workings of classes and objects, so these pages are worth a read:\n", + "\n", + "\n", + "* https://docs.python.org/3.5/tutorial/classes.html\n", + "* https://docs.python.org/3.5/reference/datamodel.html" + ] + } + ], + "metadata": {}, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/advanced_topics/object_oriented_programming.md b/advanced_topics/object_oriented_programming.md new file mode 100644 index 0000000000000000000000000000000000000000..59aa3e99bad9496e81cd44695011871969352f0a --- /dev/null +++ b/advanced_topics/object_oriented_programming.md @@ -0,0 +1,1397 @@ +# Object-oriented programming in Python + + +By now you might have realised that __everything__ in Python is an +object. Strings are objects, numbers are objects, functions are objects, +modules are objects - __everything__ is an object! + + +But this does not mean that you have to use Python in an object-oriented +fashion. You can stick with functions and statements, and get quite a lot +done. But some problems are just easier to solve, and to reason about, when +you use an object-oriented approach. + + +* [Objects versus classes](#objects-versus-classes) +* [Defining a class](#defining-a-class) +* [Object creation - the `__init__` method](#object-creation-the-init-method) + * [Our method is called `__init__`, but we didn't actually call the `__init__` method!](#our-method-is-called-init) + * [We didn't specify the `self` argument - what gives?!?](#we-didnt-specify-the-self-argument) +* [Attributes](#attributes) +* [Methods](#methods) +* [Protecting attribute access](#protecting-attribute-access) + * [A better way - properties](#a-better-way-properties]) +* [Inheritance](#inheritance) + * [The basics](#the-basics) + * [Code re-use and problem decomposition](#code-re-use-and-problem-decomposition) + * [Polymorphism](#polymorphism) + * [Multiple inheritance](#multiple-inheritance) +* [Class attributes and methods](#class-attributes-and-methods) + * [Class attributes](#class-attributes) + * [Class methods](#class-methods) +* [Appendix: The `object` base-class](#appendix-the-object-base-class) +* [Appendix: `__init__` versus `__new__`](#appendix-init-versus-new) +* [Appendix: Monkey-patching](#appendix-monkey-patching) +* [Appendix: Method overloading](#appendix-method-overloading) +* [Useful references](#useful-references) + + +<a class="anchor" id="objects-versus-classes"></a> +## Objects versus classes + + +If you are versed in C++, Java, C#, or some other object-oriented language, +then this should all hopefully sound familiar, and you can skip to the next +section. + + +If you have not done any object-oriented programming before, your first step +is to understand the difference between _objects_ (also known as +_instances_) and _classes_ (also known as _types_). + + +If you have some experience in C, then you can start off by thinking of a +class as like a `struct` definition - a `struct` is a specification for the +layout of a chunk of memory. For example, here is a typical struct definition: + +> ``` +> /** +> * Struct representing a stack. +> */ +> typedef struct __stack { +> uint8_t capacity; /**< the maximum capacity of this stack */ +> uint8_t size; /**< the current size of this stack */ +> void **top; /**< pointer to the top of this stack */ +> } stack_t; +> ``` + + +Now, an _object_ is not a definition, but rather a thing which resides in +memory. An object can have _attributes_ (pieces of information), and _methods_ +(functions associated with the object). You can pass objects around your code, +manipulate their attributes, and call their methods. + + +Returning to our C metaphor, you can think of an object as like an +instantiation of a struct: + + +> ``` +> stack_t stack; +> stack.capacity = 10; +> ``` + + +One of the major differences between a `struct` in C, and a `class` in Python +and other object oriented languages, is that you can't (easily) add functions +to a `struct` - it is just a chunk of memory. Whereas in Python, you can add +functions to your class definition, which will then be added as methods when +you create an object from that class. + + +Of course there are many more differences between C structs and classes (most +notably [inheritance](todo), [polymorphism](todo), and [access +protection](todo)). But if you can understand the difference between a +_definition_ of a C struct, and an _instantiation_ of that struct, then you +are most of the way towards understanding the difference between a _class_, +and an _object_. + + +> But just to confuse you, remember that in Python, __everything__ is an +> object - even classes! + + +<a class="anchor" id="defining-a-class"></a> +## Defining a class + + +Defining a class in Python is simple. Let's take on a small project, by +developing a class which can be used in place of the `fslmaths` shell command. + + +``` +class FSLMaths(object): + pass +``` + + +In this statement, we defined a new class called `FSLMaths`, which inherits +from the built-in `object` base-class (see [below](inheritance) for more +details on inheritance). + + +Now that we have defined our class, we can create objects - instances of that +class - by calling the class itself, as if it were a function: + + +``` +fm1 = FSLMaths() +fm2 = FSLMaths() +print(fm1) +print(fm2) +``` + + +Although these objects are not of much use at this stage. Let's do some more +work. + + +<a class="anchor" id="object-creation-the-init-method"></a> +## Object creation - the `__init__` method + + +The first thing that our `fslmaths` replacement will need is an input image. +It makes sense to pass this in when we create an `FSLMaths` object: + + +``` +class FSLMaths(object): + def __init__(self, inimg): + self.img = inimg +``` + + +Here we have added a _method_ called `__init__` to our class (remember that a +_method_ is just a function which is defined in a class, and which can be +called on instances of that class). This method expects two arguments - +`self`, and `inimg`. So now, when we create an instance of the `FSLMaths` +class, we will need to provide an input image: + + +``` +import nibabel as nib +import os.path as op + +fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz') +inimg = nib.load(fpath) +fm = FSLMaths(inimg) +``` + + +There are a couple of things to note here... + + +<a class="anchor" id="our-method-is-called-init"></a> +### Our method is called `__init__`, but we didn't actually call the `__init__` method! + + +`__init__` is a special method in Python - it is called when an instance of a +class is created. And recall that we can create an instance of a class by +calling the class in the same way that we call a function. + + +There are a number of "special" methods that you can add to a class in Python +to customise various aspects of how instances of the class behave. One of the +first ones you may come across is the `__str__` method, which defines how an +object should be printed (more specifically, how an object gets converted into +a string). For example, we could add a `__str__` method to our `FSLMaths` +class like so: + + +``` +class FSLMaths(object): + + def __init__(self, inimg): + self.img = inimg + + def __str__(self): + return 'FSLMaths({})'.format(self.img.get_filename()) + +fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz') +inimg = nib.load(fpath) +fm = FSLMaths(inimg) + +print(fm) +``` + + +Refer to the [official +docs](https://docs.python.org/3.5/reference/datamodel.html#special-method-names) +for details on all of the special methods that can be defined in a class. And +take a look at the appendix for some more details on [how Python objects get +created](appendix-init-versus-new). + + +<a class="anchor" id="we-didnt-specify-the-self-argument"></a> +### We didn't specify the `self` argument - what gives?!? + + +The `self` argument is a special argument for methods in Python. If you are +coming from C++, Java, C# or similar, `self` in Python is equivalent to `this` +in those languages. + + +In a method, the `self` argument is a reference to the object that the method +was called on. So in this line of code: + + +``` +fm = FSLMaths(inimg) +``` + + +the `self` argument in `__init__` will be a reference to the `FSLMaths` object +that has been created (and is then assigned to the `fm` variable, after the +`__init__` method has finished). + + +But note that you __do not__ need to explicitly provide the `self` argument +when you call a method on an object, or when you create a new object. The +Python runtime will take care of passing the instance to its method, as the +first argument to the method. + + +But when you are writing a class, you __do__ need to explicitly list `self` as +the first argument to all of the methods of the class. + + +<a class="anchor" id="attributes"></a> +## Attributes + + +In Python, the term __attribute__ is used to refer to a piece of information +that is associated with an object. An attribute is generally a reference to +another object (which might be a string, a number, or a list, or some other +more complicated object). + + +Remember that we modified our `FSLMaths` class so that it is passed an input +image on creation: + + +``` +class FSLMaths(object): + def __init__(self, inimg): + self.img = inimg + +fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz') +fm = FSLMaths(nib.load(fpath)) +``` + + +Take a look at what is going on in the `__init__` method - we take the `inimg` +argument, and create a reference to it called `self.img`. We have added an +_attribute_ to the `FSLMaths` instance, called `img`, and we can access that +attribute like so: + + +``` +print('Input for our FSLMaths instance: {}'.format(fm.img.get_filename())) +``` + + +And that concludes the section on adding attributes to Python objects. + + +Just kidding. But it really is that simple. This is one aspect of Python which +might be quite jarring to you if you are coming from a language with more +rigid semantics, such as C++ or Java. In those languages, you must pre-specify +all of the attributes and methods that are a part of a class. But Python is +much more flexible - you can simply add attributes to an object after it has +been created. In fact, you can even do this outside of the class +definition<sup>1</sup>: + + +``` +fm = FSLMaths(inimg) +fm.another_attribute = 'Haha' +print(fm.another_attribute) +``` + + +__But ...__ while attributes can be added to a Python object at any time, it is +good practice (and makes for more readable and maintainable code) to add all +of an object's attributes within the `__init__` method. + + +> <sup>1</sup>This not possible with many of the built-in types, such as +> `list` and `dict` objects, nor with types that are defined in Python +> extensions (Python modules that are written in C). + + +<a class="anchor" id="methods"></a> +## Methods + + +We've been dilly-dallying on this little `FSLMaths` project for a while now, +but our class still can't actually do anything. Let's start adding some +functionality: + + +``` +class FSLMaths(object): + + def __init__(self, inimg): + self.img = inimg + self.operations = [] + + def add(self, value): + self.operations.append(('add', value)) + + def mul(self, value): + self.operations.append(('mul', value)) + + def div(self, value): + self.operations.append(('div', value)) +``` + + +Woah woah, [slow down egg-head!](https://www.youtube.com/watch?v=yz-TemWooa4) +We've modified `__init__` so that a second attribute called `operations` is +added to our object - this `operations` attribute is simply a list. + + +Then, we added a handful of methods - `add`, `mul`, and `div` - which each +append a tuple to that `operations` list. + + +> Note that, just like in the `__init__` method, the first argument that will +> be passed to these methods is `self` - a reference to the object that the +> method has been called on. + + +The idea behind this design is that our `FSLMaths` class will not actually do +anything when we call the `add`, `mul` or `div` methods. Instead, it will +"stage" each operation, and then perform them all in one go. So let's add +another method, `run`, which actually does the work: + + +``` +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)) + + def mul(self, value): + self.operations.append(('mul', value)) + + def div(self, value): + self.operations.append(('div', value)) + + 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 +``` + + +We now have a useable (but not very useful) `FSLMaths` class! + + +``` +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) +fm = FSLMaths(inimg) + +fm.mul(mask) +fm.add(-10) + +outimg = fm.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)) +``` + + +<a class="anchor" id="protecting-attribute-access"></a> +## Protecting attribute access + + +In our `FSLMaths` class, the input image was added as an attribute called +`img` to `FSLMaths` objects. We saw that it is easy to read the attributes +of an object - if we have a `FSLMaths` instance called `fm`, we can read its +input image via `fm.img`. + + +But it is just as easy to write the attributes of an object. What's to stop +some sloppy research assistant from overwriting our `img` attribute? + + +``` +inimg = nib.load(op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')) +fm = FSLMaths(inimg) +fm.img = None +fm.run() +``` + + +Well, the scary answer is ... there is __nothing__ stopping you from doing +whatever you want to a Python object. You can add, remove, and modify +attributes at will. You can even replace the methods of an existing object if +you like: + + +``` +fm = FSLMaths(inimg) + +def myadd(value): + print('Oh no, I\'m not going to add {} to ' + 'your image. Go away!'.format(value)) + +fm.add = myadd +fm.add(123) + +fm.mul = None +fm.mul(123) +``` + + +But you really shouldn't get into the habit of doing devious things like +this. Think of the poor souls who inherit your code years after you have left +the lab - if you go around overwriting all of the methods and attributes of +your objects, they are not going to have a hope in hell of understanding what +your code is actually doing, and they are not going to like you very +much. Take a look at the appendix for a [brief discussion on this +topic](appendix-monkey-patching). + + +Python tends to assume that programmers are "responsible adults", and hence +doesn't do much in the way of restricting access to the attributes or methods +of an object. This is in contrast to languages like C++ and Java, where the +notion of a private attribute or method is strictly enforced by the language. + + +However, there are a couple of conventions in Python that are [universally +adhered +to](https://docs.python.org/3.5/tutorial/classes.html#private-variables): + +* Class-level attributes and methods, and module-level attributes, functions, + and classes, which begin with a single underscore (`_`), should be + considered __protected__ - they are intended for internal use only, and + should not be considered part of the public API of a class or module. This + is not enforced by the language in any way<sup>2</sup> - remember, we are + all responsible adults here! + +* Class-level attributes and methods which begin with a double-underscore + (`__`) should be considered __private__. Python provides a weak form of + enforcement for this rule - any attribute or method with such a name will + actually be _renamed_ (in a standardised manner) at runtime, so that it is + not accessible through its original name (it is still accessible via its + [mangled + name](https://docs.python.org/3.5/tutorial/classes.html#private-variables) + though). + + +> <sup>2</sup> With the exception that module-level fields which begin with a +> single underscore will not be imported into the local scope via the +> `from [module] import *` techinque. + + +So with all of this in mind, we can adjust our `FSLMaths` class to discourage +our sloppy research assistant from overwriting the `img` attribute: + + +``` +# remainder of definition omitted for brevity +class FSLMaths(object): + def __init__(self, inimg): + self.__img = inimg + self.__operations = [] +``` + +But now we have lost the ability to read our `__img` attribute: + + +``` +inimg = nib.load(op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')) +fm = FSLMaths(inimg) +print(fm.__img) +``` + + +<a class="anchor" id="a-better-way-properties"></a> +### A better way - properties + + +Python has a feature called +[`properties`](https://docs.python.org/3.5/library/functions.html#property), +which is a nice way of controlling access to the attributes of an object. We +can use properties by defining a "getter" method which can be used to access +our attributes, and "decorating" them with the `@property` decorator (we will +cover decorators in a later practical). + + +``` +class FSLMaths(object): + def __init__(self, inimg): + self.__img = inimg + self.__operations = [] + + @property + def img(self): + return self.__img +``` + + +So we are still storing our input image as a private attribute, but now we +have made it available in a read-only manner via the public `img` property: + + +``` +fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz') +inimg = nib.load(fpath) +fm = FSLMaths(inimg) + +print(fm.img.get_filename()) +``` + + +Note that, even though we have defined `img` as a method, we can access it +like an attribute - this is due to the magic behind the `@property` decorator. + + +We can also define "setter" methods for a property. For example, we might wish +to add the ability for a user of our `FSLMaths` class to change the input +image after creation. + + +``` +class FSLMaths(object): + def __init__(self, inimg): + self.__img = None + self.__operations = [] + self.img = inimg + + @property + def img(self): + return self.__img + + @img.setter + def img(self, value): + if not isinstance(value, nib.nifti1.Nifti1Image): + raise ValueError('value must be a NIFTI image!') + self.__img = value +``` + + +> Note that we used the `img` setter method within `__init__` to validate the +> initial `inimg` that was passed in during creation. + + +Property setters are a nice way to add validation logic for when an attribute +is assigned a value. In this example, an error will be raised if the new input +is not a NIFTI image. + + +``` +fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz') +inimg = nib.load(fpath) +fm = FSLMaths(inimg) + +print('Input: ', fm.img.get_filename()) + +# let's change the input +# to a different image +fpath2 = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm_brain.nii.gz') +inimg2 = nib.load(fpath2) +fm.img = inimg2 + +print('New input: ', fm.img.get_filename()) + +print('This is going to explode') +fm.img = 'abcde' +``` + + +<a class="anchor" id="inheritance"></a> +## Inheritance + + +One of the major advantages of an object-oriented programming approach is +_inheritance_ - the ability to define hierarchical relationships between +classes and instances. + + +<a class="anchor" id="the-basics"></a> +### The basics + + +My local veterinary surgery runs some Python code which looks like the +following, to assist the nurses in identifying an animal when it arrives at +the surgery: + + +``` +class Animal(object): + def noiseMade(self): + raise NotImplementedError('This method must be ' + 'implemented by sub-classes') + +class Dog(Animal): + def noiseMade(self): + return 'Woof' + +class TalkingDog(Dog): + def noiseMade(self): + return 'Hi Homer, find your soulmate!' + +class Cat(Animal): + def noiseMade(self): + return 'Meow' + +class Labrador(Dog): + pass + +class Chihuahua(Dog): + def noiseMade(self): + return 'Yap yap yap' +``` + + +Hopefully this example doesn't need much in the way of explanation - this +collection of classes captures a hierarchical relationship which exists in the +real world (and also captures the inherently annoying nature of +chihuahuas). For example, in the real world, all dogs are animals, but not all +animals are dogs. Therefore in our model, the `Dog` class has specified +`Animal` as its base class. We say that the `Dog` class _extends_, _derives +from_, or _inherits from_, the `Animal` class, and that all `Dog` instances +are also `Animal` instances (but not vice-versa). + + +What does that `noiseMade` method do? There is a `noiseMade` method defined +on the `Animal` class, but it has been re-implemented, or _overridden_ in the +`Dog`, +[`TalkingDog`](https://twitter.com/simpsonsqotd/status/427941665836630016?lang=en), +`Cat`, and `Chihuahua` classes (but not on the `Labrador` class). We can call +the `noiseMade` method on any `Animal` instance, but the specific behaviour +that we get is dependent on the specific type of animal. + + +``` +d = Dog() +l = Labrador() +c = Cat() +ch = Chihuahua() + +print('Noise made by dogs: {}'.format(d .noiseMade())) +print('Noise made by labradors: {}'.format(l .noiseMade())) +print('Noise made by cats: {}'.format(c .noiseMade())) +print('Noise made by chihuahuas: {}'.format(ch.noiseMade())) +``` + + +Note that calling the `noiseMade` method on a `Labrador` instance resulted in +the `Dog.noiseMade` implementation being called. + + +<a class="anchor" id="code-re-use-and-problem-decomposition"></a> +### Code re-use and problem decomposition + + +Inheritance allows us to split a problem into smaller problems, and to re-use +code. Let's demonstrate this with a more involved (and even more contrived) +example. Imagine that a former colleague had written a class called +`Operator`: + + +``` +class Operator(object): + + def __init__(self): + super().__init__() # this line will be explained later + self.__operations = [] + self.__opFuncs = {} + + @property + def operations(self): + return list(self.__operations) + + @property + def functions(self): + return dict(self.__opFuncs) + + def addFunction(self, name, func): + self.__opFuncs[name] = func + + def do(self, name, *values): + self.__operations.append((name, values)) + + def preprocess(self, value): + return value + + def run(self, input): + data = self.preprocess(input) + for oper, vals in self.__operations: + func = self.__opFuncs[oper] + vals = [self.preprocess(v) for v in vals] + data = func(data, *vals) + return data +``` + + +This `Operator` class provides an interface and logic to execute a chain of +operations - an operation is some function which accepts one or more inputs, +and produce one output. + + +But it stops short of defining any operations. Instead, we can create another +class - a sub-class - which derives from the `Operator` class. This sub-class +will define the operations that will ultimately be executed by the `Operator` +class. All that the `Operator` class does is: + +- Allow functions to be registered with the `addFunction` method - all + registered functions can be used via the `do` method. + +- Stage an operation (using a registered function) via the `do` method. Note + that `do` allows any number of values to be passed to it, as we used the `*` + operator when specifying the `values` argument. + +- Run all staged operations via the `run` method - it passes an input through + all of the operations that have been staged, and then returns the final + result. + + +Let's define a sub-class: + + +``` +class NumberOperator(Operator): + + def __init__(self): + super().__init__() + self.addFunction('add', self.add) + self.addFunction('mul', self.mul) + self.addFunction('negate', self.negate) + + def preprocess(self, value): + return float(value) + + def add(self, a, b): + return a + b + + def mul(self, a, b): + return a * b + + def negate(self, a): + return -a +``` + + +The `NumberOperator` is a sub-class of `Operator`, which we can use for basic +numerical calculations. It provides a handful of simple numerical methods, but +the most interesting stuff is inside `__init__`. + + +> ``` +> super().__init__() +> ``` + + +This line invokes `Operator.__init__` - the initialisation method for the +`Operator` base-class. + + +In Python, we can use the [built-in `super` +method](https://docs.python.org/3.5/library/functions.html#super) to take care +of correctly calling methods that are defined in an object's base-class (or +classes, in the case of [multiple inheritance](multiple-inheritance)). + + +> The `super` function is one thing which changed between Python 2 and 3 - +> in Python 2, it was necessary to pass both the type and the instance +> to `super`. So it is common to see code that looks like this: +> +> ``` +> def __init__(self): +> super(NumberOperator, self).__init__() +> ``` +> +> Fortunately things are a lot cleaner in Python 3. + + +Let's move on to the next few lines in `__init__`: + + +> ``` +> self.addFunction('add', self.add) +> self.addFunction('mul', self.mul) +> self.addFunction('negate', self.negate) +> ``` + + +Here we are registering all of the functionality that is provided by the +`NumberOperator` class, via the `Operator.addFunction` method. + + +The `NumberOperator` class has also overridden the `preprocess` method, to +ensure that all values handled by the `Operator` are numbers. This method gets +called within the `Operator.run` method - for a `NumberOperator` instance, the +`NumberOperator.preprocess` method will get called<sup>1</sup>. + + +> <sup>1</sup> When a sub-class overrides a base-class method, it is still +> possible to access the base-class implementation [via the `super()` +> function](https://stackoverflow.com/a/4747427) (the preferred method), or by +> [explicitly calling the base-class +> implementation](https://stackoverflow.com/a/2421325). + + +Now let's see what our `NumberOperator` class does: + + +``` +no = NumberOperator() +no.do('add', 10) +no.do('mul', 2) +no.do('negate') + +print('Operations on {}: {}'.format(10, no.run(10))) +print('Operations on {}: {}'.format(2.5, no.run(5))) +``` + + +It works! While this is a contrived example, hopefully you can see how +inheritance can be used to break a problem down into sub-problems: + +- The `Operator` class provides all of the logic needed to manage and execute + operations, without caring about what those operations are actually doing. + +- This leaves the `NumberOperator` class free to concentrate on implementing + the functions that are specific to its task, and not having to worry about + how they are executed. + + +We could also easily implement other `Operator` sub-classes to work on +different data types, such as arrays, images, or even non-numeric data such as +strings: + + +``` +class StringOperator(Operator): + def __init__(self): + super().__init__() + self.addFunction('capitalise', self.capitalise) + self.addFunction('concat', self.concat) + + def preprocess(self, value): + return str(value) + + def capitalise(self, s): + return ' '.join([w[0].upper() + w[1:] for w in s.split()]) + + def concat(self, s1, s2): + return s1 + s2 + +so = StringOperator() +so.do('capitalise') +so.do('concat', '!') + +print(so.run('python is an ok language')) +``` + +<a class="anchor" id="polymorphism"></a> +### Polymorphism + + +Inheritance also allows us to take advantage of _polymorphism_, which refers +to idea that, in an object-oriented language, we should be able to use an +object without having complete knowledge about the class, or type, of that +object. For example, we should be able to write a function which expects an +`Operator` instance, but which will work on an instance of any `Operator` +sub-classs. As an example, let's write a function which prints a summary of an +`Operator` instance: + + +``` +def operatorSummary(o): + print(type(o).__name__) + print(' All functions: ') + for fname in o.functions.keys(): + print(' {}'.format(fname)) + print(' Staged operations: ') + for i, (fname, vals) in enumerate(o.operations): + vals = ', '.join([str(v) for v in vals]) + print(' {}: {}({})'.format(i + 1, fname, vals)) +``` + + +Because the `operatorSummary` function only uses methods that are defined +in the `Operator` base-class, we can use it on _any_ `Operator` instance, +regardless of its specific type: + + +``` +operatorSummary(no) +operatorSummary(so) +``` + + +<a class="anchor" id="multiple-inheritance"></a> +### Multiple inheritance + + +Python allows you to define a class which has multiple base classes - this is +known as _multiple inheritance_. For example, we might want to build a +notification mechanisim into our `StringOperator` class, so that listeners can +be notified whenever the `capitalise` method gets called. It so happens that +our old colleague of `Operator` class fame also wrote a `Notifier` class which +allows listeners to register to be notified when an event occurs: + + +``` +class Notifier(object): + + def __init__(self): + super().__init__() + self.__listeners = {} + + def register(self, name, func): + self.__listeners[name] = func + + def notify(self, *args, **kwargs): + for func in self.__listeners.values(): + func(*args, **kwargs) +``` + + +Let's modify the `StringOperator` class to use the functionality of the +`Notifier ` class: + + +``` +class StringOperator(Operator, Notifier): + + def __init__(self): + super().__init__() + self.addFunction('capitalise', self.capitalise) + self.addFunction('concat', self.concat) + + def preprocess(self, value): + return str(value) + + def capitalise(self, s): + result = ' '.join([w[0].upper() + w[1:] for w in s.split()]) + self.notify(result) + return result + + def concat(self, s1, s2): + return s1 + s2 +``` + + +Now, anything which is interested in uses of the `capitalise` method can +register as a listener on a `StringOperator` instance: + + +``` +so = StringOperator() + +def capitaliseCalled(result): + print('Capitalise operation called: {}'.format(result)) + +so.register('mylistener', capitaliseCalled) + +so = StringOperator() +so.do('capitalise') +so.do('concat', '?') + +print(so.run('did you notice that functions are objects too')) +``` + +If you wish to use multiple inheritance in your design, it is important to be +aware of the mechanism that Python uses to determine how base class methods +are called (and which base class method will be called, in the case of naming +conflicts). This is referred to as the Method Resolution Order (MRO) - further +details on the topic can be found +[here](https://www.python.org/download/releases/2.3/mro/), and a more concise +summary +[here](http://python-history.blogspot.co.uk/2010/06/method-resolution-order.html). + + +Note also that for base class `__init__` methods to be correctly called in a +design which uses multiple inheritance, _all_ classes in the hierarchy must +invoke `super().__init__()`. This can become complicated when some base +classes expect to be passed arguments to their `__init__` method. In scenarios +like this it may be prefereable to manually invoke the base class `__init__` +methods instead of using `super()`. For example: + + +> ``` +> class StringOperator(Operator, Notifier): +> def __init__(self): +> Operator.__init__(self) +> Notifier.__init__(self) +> ``` + + +This approach has the disadvantage that if the base classes change, you will +have to change these invocations. But the advantage is that you know exactly +how the class hierarchy will be initialised. In general though, doing +everything with `super()` will result in more maintainable code. + + +<a class="anchor" id="class-attributes-and-methods"></a> +## Class attributes and methods + + +Up to this point we have been covering how to add attributes and methods to an +_object_. But it is also possible to add methods and attributes to a _class_ +(`static` methods and fields, for those of you familiar with C++ or Java). + + +Class attributes and methods can be accessed without having to create an +instance of the class - they are not associated with individual objects, but +rather with the class itself. + + +Class methods and attributes can be useful in several scenarios - as a +hypothetical, not very useful example, let's say that we want to gain usage +statistics for how many times each type of operation is used on instances of +our `FSLMaths` class. We might, for example, use this information in a grant +application to show evidence that more research is needed to optimise the +performance of the `add` operation. + + +<a class="anchor" id="class-attributes"></a> +### Class attributes + + +Let's add a `dict` called `opCounters` as a class attribute to the `FSLMaths` +class - whenever an operation is called on a `FSLMaths` instance, the counter +for that operation will be incremented: + + +``` +import numpy as np +import nibabel as nib + +class FSLMaths(object): + + # It's this easy to add a class-level + # attribute. This dict is associated + # with the FSLMaths *class*, not with + # any individual FSLMaths instance. + opCounters = {} + + def __init__(self, inimg): + self.img = inimg + self.operations = [] + + def add(self, value): + self.operations.append(('add', value)) + + def mul(self, value): + self.operations.append(('mul', value)) + + def div(self, value): + self.operations.append(('div', value)) + + def run(self, output=None): + + data = np.array(self.img.get_data()) + + for oper, value in self.operations: + + # Code omitted for brevity + + # Increment the usage counter + # for this operation. We can + # access class attributes (and + # methods) through the class + # itself. + FSLMaths.opCounters[oper] = self.opCounters.get(oper, 0) + 1 +``` + + +So let's see it in action: + + +``` +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) + +fm1 = FSLMaths(inimg) +fm2 = FSLMaths(inimg) + +fm1.mul(mask) +fm1.add(15) + +fm2.add(25) +fm1.div(1.5) + +fm1.run() +fm2.run() + +print('FSLMaths usage statistics') +for oper in ('add', 'div', 'mul'): + print(' {} : {}'.format(oper, FSLMaths.opCounters.get(oper, 0))) +``` + + +<a class="anchor" id="class-methods"></a> +### Class methods + + +It is just as easy to add a method to a class - let's take our reporting code +from above, and add it as a method to the `FSLMaths` class. + + +A class method is denoted by the +[`@classmethod`](https://docs.python.org/3.5/library/functions.html#classmethod) +decorator. Note that, where a regular method which is called on an instance +will be passed the instance as its first argument (`self`), a class method +will be passed the class itself as the first argument - the standard +convention is to call this argument `cls`: + + +``` +class FSLMaths(object): + + opCounters = {} + + @classmethod + def usage(cls): + print('FSLMaths usage statistics') + for oper in ('add', 'div', 'mul'): + print(' {} : {}'.format(oper, FSLMaths.opCounters.get(oper, 0))) + + def __init__(self, inimg): + self.img = inimg + self.operations = [] + + def add(self, value): + self.operations.append(('add', value)) + + def mul(self, value): + self.operations.append(('mul', value)) + + def div(self, value): + self.operations.append(('div', value)) + + def run(self, output=None): + + data = np.array(self.img.get_data()) + + for oper, value in self.operations: + FSLMaths.opCounters[oper] = self.opCounters.get(oper, 0) + 1 +``` + + +> There is another decorator - +> [`@staticmethod`](https://docs.python.org/3.5/library/functions.html#staticmethod) - +> which can be used on methods defined within a class. The difference +> between a `@classmethod` and a `@staticmethod` is that the latter will _not_ +> be passed the class (`cls`). + + +calling a class method is the same as accessing a class attribute: + + +``` +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) + +fm1 = FSLMaths(inimg) +fm2 = FSLMaths(inimg) + +fm1.mul(mask) +fm1.add(15) + +fm2.add(25) +fm1.div(1.5) + +fm1.run() +fm2.run() + +FSLMaths.usage() +``` + + +Note that it is also possible to access class attributes and methods through +instances: + + +``` +print(fm1.opCounters) +fm1.usage() +``` + + +<a class="anchor" id="appendix-the-object-base-class"></a> +## Appendix: The `object` base-class + + +When you are defining a class, you need to specify the base-class from which +your class inherits. If your class does not inherit from a particular class, +then it should inherit from the built-in `object` class: + + +> ``` +> class MyClass(object): +> ... +> ``` + + +However, in older code bases, you might see class definitions that look like +this, without explicitly inheriting from the `object` base class: + + +> ``` +> class MyClass: +> ... +> ``` + + +This syntax is a [throwback to older versions of +Python](https://docs.python.org/2/reference/datamodel.html#new-style-and-classic-classes). +In Python 3 there is actually no difference in defining classes in the +"new-style" way we have used throughout this tutorial, or the "old-style" way +mentioned in this appendix. + + +But if you are writing code which needs to run on both Python 2 and 3, you +__must__ define your classes in the new-style convention, i.e. by explicitly +inheriting from the `object` base class. Therefore, the safest approach is to +always use the new-style format. + + +<a class="anchor" id="appendix-init-versus-new"></a> +## Appendix: `__init__` versus `__new__` + + +In Python, object creation is actually a two-stage process - _creation_, and +then _initialisation_. The `__init__` method gets called during the +_initialisation_ stage - its job is to initialise the state of the object. But +note that, by the time `__init__` gets called, the object has already been +created. + + +You can also define a method called `__new__` if you need to control the +creation stage, although this is very rarely needed. A brief explanation on +the difference between `__new__` and `__init__` can be found +[here](https://www.reddit.com/r/learnpython/comments/2s3pms/what_is_the_difference_between_init_and_new/cnm186z/), +and you may also wish to take a look at the [official Python +docs](https://docs.python.org/3.5/reference/datamodel.html#basic-customization). + + +<a class="anchor" id="appendix-monkey-patching"></a> +## Appendix: Monkey-patching + + +The act of run-time modification of objects or class definitions is referred +to as [_monkey-patching_](https://en.wikipedia.org/wiki/Monkey_patch) and, +whilst it is allowed by the Python programming language, it is generally +considered quite bad practice. + + +Just because you _can_ do something doesn't mean that you _should_. Python +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, +maintained, and enjoyed by other people, you should be polite, write your code +in a clear, readable fashion, and avoid the use of devious tactics such as +monkey-patching. + + +__However__, while monkey-patching may seem like a horrific programming +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 +circumstances. For instance, monkey-patching makes [unit testing a +breeze in Python](https://docs.python.org/3.5/library/unittest.mock.html). + + +As another example, consider the scenario where you are dependent on a third +party library which has bugs in it. No problem - while you are waiting for the +library author to release a new version of the library, you can write your own +working implementation and [monkey-patch it +in!](https://git.fmrib.ox.ac.uk/fsl/fsleyes/fsleyes/blob/0.21.0/fsleyes/views/viewpanel.py#L726) + + +<a class="anchor" id="appendix-method-overloading"></a> +## Appendix: Method overloading + + +Method overloading (defining multiple methods with the same name in a class, +but each accepting different arguments) is one of the only object-oriented +features that is not present in Python. Becuase Python does not perform any +runtime checks on the types of arguments that are passed to a method, or the +compatibility of the method to accept the arguments, it would not be possible +to determine which implementation of a method is to be called. In other words, +in Python only the name of a method is used to identify that method, unlike in +C++ and Java, where the full method signature (name, input types and return +types) is used. + + +However, because a Python method can be written to accept any number or type +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): + + +``` +class Adder(object): + + def add(self, *args): + if len(args) == 2: return self.__add2(*args) + elif len(args) == 3: return self.__add3(*args) + elif len(args) == 4: return self.__add4(*args) + else: + raise AttributeError('No method available to accept {} ' + 'arguments'.format(len(args))) + + def __add2(self, a, b): + return a + b + + def __add3(self, a, b, c): + return a + b + c + + def __add4(self, a, b, c, d): + return a + b + c + d + +a = Adder() + +print('Add two: {}'.format(a.add(1, 2))) +print('Add three: {}'.format(a.add(1, 2, 3))) +print('Add four: {}'.format(a.add(1, 2, 3, 4))) +``` + + +<a class="anchor" id="useful-references"></a> +## Useful references + + +The official Python documentation has a wealth of information on the internal +workings of classes and objects, so these pages are worth a read: + + +* https://docs.python.org/3.5/tutorial/classes.html +* https://docs.python.org/3.5/reference/datamodel.html diff --git a/getting_started/basics.ipynb b/getting_started/01_basics.ipynb similarity index 56% rename from getting_started/basics.ipynb rename to getting_started/01_basics.ipynb index 4cd6d7951303564e791df3d80c95220afd2ce159..a3210a7e20a0249eb372f786bdbd83688074a0d2 100644 --- a/getting_started/basics.ipynb +++ b/getting_started/01_basics.ipynb @@ -30,17 +30,9 @@ }, { "cell_type": "code", - "execution_count": 63, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "4\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "a = 4\n", "b = 3.6\n", @@ -59,19 +51,9 @@ }, { "cell_type": "code", - "execution_count": 64, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[10, 20, 30]\n", - "{'b': 20, 'a': 10}\n", - "4 3.6 abc\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "print(d)\n", "print(e)\n", @@ -100,17 +82,9 @@ }, { "cell_type": "code", - "execution_count": 65, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "test string :: another test string\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "s1 = \"test string\"\n", "s2 = 'another test string'\n", @@ -126,20 +100,9 @@ }, { "cell_type": "code", - "execution_count": 66, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "This is\n", - "a string over\n", - "multiple lines\n", - "\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "s3 = '''This is\n", "a string over\n", @@ -159,18 +122,9 @@ }, { "cell_type": "code", - "execution_count": 67, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The numerical value is 1 and a name is PyTreat\n", - "A name is PyTreat and a number is 1\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "x = 1\n", "y = 'PyTreat'\n", @@ -192,18 +146,9 @@ }, { "cell_type": "code", - "execution_count": 68, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "THIS IS A TEST STRING\n", - "this is a test string\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "s = 'This is a Test String'\n", "print(s.upper())\n", @@ -219,17 +164,9 @@ }, { "cell_type": "code", - "execution_count": 69, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "This is a Better String\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "s = 'This is a Test String'\n", "s2 = s.replace('Test', 'Better')\n", @@ -245,17 +182,9 @@ }, { "cell_type": "code", - "execution_count": 70, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "This is an example of an example String\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "import re\n", "s = 'This is a test of a Test String'\n", @@ -277,17 +206,9 @@ }, { "cell_type": "code", - "execution_count": 71, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "['This', 'is', 'a', 'test', 'of', 'a', 'Test', 'String']\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "print(s.split())" ] @@ -311,18 +232,9 @@ }, { "cell_type": "code", - "execution_count": 72, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "(3, 7.6, 'str')\n", - "[1, 'mj', -5.4]\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "xtuple = (3, 7.6, 'str')\n", "xlist = [1, 'mj', -5.4]\n", @@ -339,18 +251,9 @@ }, { "cell_type": "code", - "execution_count": 73, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "x2 is: ((3, 7.6, 'str'), [1, 'mj', -5.4])\n", - "x3 is: [(3, 7.6, 'str'), [1, 'mj', -5.4]]\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "x2 = (xtuple, xlist)\n", "x3 = [xtuple, xlist]\n", @@ -369,17 +272,9 @@ }, { "cell_type": "code", - "execution_count": 74, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[10, 20, 30, 70, 80]\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "a = [10, 20, 30]\n", "a = a + [70]\n", @@ -398,17 +293,9 @@ }, { "cell_type": "code", - "execution_count": 75, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "20\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "d = [10, 20, 30]\n", "print(d[1])" @@ -424,18 +311,9 @@ }, { "cell_type": "code", - "execution_count": 76, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "10\n", - "30\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "a = [10, 20, 30, 40, 50, 60]\n", "print(a[0])\n", @@ -451,18 +329,9 @@ }, { "cell_type": "code", - "execution_count": 77, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "60\n", - "10\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "print(a[-1])\n", "print(a[-6])" @@ -477,42 +346,18 @@ }, { "cell_type": "code", - "execution_count": 78, - "metadata": {}, - "outputs": [ - { - "ename": "IndexError", - "evalue": "list index out of range", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mIndexError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m<ipython-input-78-f4cf4536701c>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m()\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ma\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m7\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", - "\u001b[0;31mIndexError\u001b[0m: list index out of range" - ], - "output_type": "error" - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "print(a[-7])" ] }, { "cell_type": "code", - "execution_count": 79, - "metadata": {}, - "outputs": [ - { - "ename": "IndexError", - "evalue": "list index out of range", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mIndexError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m<ipython-input-79-52d95fbe5286>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m()\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ma\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m6\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", - "\u001b[0;31mIndexError\u001b[0m: list index out of range" - ], - "output_type": "error" - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "print(a[6])" ] @@ -526,17 +371,9 @@ }, { "cell_type": "code", - "execution_count": 80, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "6\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "print(len(a))" ] @@ -550,18 +387,9 @@ }, { "cell_type": "code", - "execution_count": 81, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "20\n", - "40\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "b = [[10, 20, 30], [40, 50, 60]]\n", "print(b[0][1])\n", @@ -584,17 +412,9 @@ }, { "cell_type": "code", - "execution_count": 82, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[10, 20, 30]\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "print(a[0:3])" ] @@ -611,18 +431,9 @@ }, { "cell_type": "code", - "execution_count": 83, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[10, 20, 30]\n", - "[20, 30]\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "a = [10, 20, 30, 40, 50, 60]\n", "print(a[0:3]) # same as a(1:3) in MATLAB\n", @@ -641,21 +452,9 @@ }, { "cell_type": "code", - "execution_count": 84, - "metadata": {}, - "outputs": [ - { - "ename": "TypeError", - "evalue": "list indices must be integers or slices, not list", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m<ipython-input-84-aad7915ae3d8>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m()\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0mb\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;36m3\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m4\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ma\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mb\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", - "\u001b[0;31mTypeError\u001b[0m: list indices must be integers or slices, not list" - ], - "output_type": "error" - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "b = [3, 4]\n", "print(a[b])" @@ -672,17 +471,9 @@ }, { "cell_type": "code", - "execution_count": 85, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[10, 20, 30, 10, 20, 30, 10, 20, 30, 10, 20, 30]\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "d = [10, 20, 30]\n", "print(d * 4)" @@ -697,19 +488,9 @@ }, { "cell_type": "code", - "execution_count": 86, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[10, 20, 30, 40]\n", - "[10, 30, 40]\n", - "[30, 40]\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "d.append(40)\n", "print(d)\n", @@ -728,19 +509,9 @@ }, { "cell_type": "code", - "execution_count": 87, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "10\n", - "20\n", - "30\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "d = [10, 20, 30]\n", "for x in d:\n", @@ -760,134 +531,9 @@ }, { "cell_type": "code", - "execution_count": 88, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Help on list object:\n", - "\n", - "class list(object)\n", - " | list() -> new empty list\n", - " | list(iterable) -> new list initialized from iterable's items\n", - " | \n", - " | Methods defined here:\n", - " | \n", - " | __add__(self, value, /)\n", - " | Return self+value.\n", - " | \n", - " | __contains__(self, key, /)\n", - " | Return key in self.\n", - " | \n", - " | __delitem__(self, key, /)\n", - " | Delete self[key].\n", - " | \n", - " | __eq__(self, value, /)\n", - " | Return self==value.\n", - " | \n", - " | __ge__(self, value, /)\n", - " | Return self>=value.\n", - " | \n", - " | __getattribute__(self, name, /)\n", - " | Return getattr(self, name).\n", - " | \n", - " | __getitem__(...)\n", - " | x.__getitem__(y) <==> x[y]\n", - " | \n", - " | __gt__(self, value, /)\n", - " | Return self>value.\n", - " | \n", - " | __iadd__(self, value, /)\n", - " | Implement self+=value.\n", - " | \n", - " | __imul__(self, value, /)\n", - " | Implement self*=value.\n", - " | \n", - " | __init__(self, /, *args, **kwargs)\n", - " | Initialize self. See help(type(self)) for accurate signature.\n", - " | \n", - " | __iter__(self, /)\n", - " | Implement iter(self).\n", - " | \n", - " | __le__(self, value, /)\n", - " | Return self<=value.\n", - " | \n", - " | __len__(self, /)\n", - " | Return len(self).\n", - " | \n", - " | __lt__(self, value, /)\n", - " | Return self<value.\n", - " | \n", - " | __mul__(self, value, /)\n", - " | Return self*value.n\n", - " | \n", - " | __ne__(self, value, /)\n", - " | Return self!=value.\n", - " | \n", - " | __new__(*args, **kwargs) from builtins.type\n", - " | Create and return a new object. See help(type) for accurate signature.\n", - " | \n", - " | __repr__(self, /)\n", - " | Return repr(self).\n", - " | \n", - " | __reversed__(...)\n", - " | L.__reversed__() -- return a reverse iterator over the list\n", - " | \n", - " | __rmul__(self, value, /)\n", - " | Return self*value.\n", - " | \n", - " | __setitem__(self, key, value, /)\n", - " | Set self[key] to value.\n", - " | \n", - " | __sizeof__(...)\n", - " | L.__sizeof__() -- size of L in memory, in bytes\n", - " | \n", - " | append(...)\n", - " | L.append(object) -> None -- append object to end\n", - " | \n", - " | clear(...)\n", - " | L.clear() -> None -- remove all items from L\n", - " | \n", - " | copy(...)\n", - " | L.copy() -> list -- a shallow copy of L\n", - " | \n", - " | count(...)\n", - " | L.count(value) -> integer -- return number of occurrences of value\n", - " | \n", - " | extend(...)\n", - " | L.extend(iterable) -> None -- extend list by appending elements from the iterable\n", - " | \n", - " | index(...)\n", - " | L.index(value, [start, [stop]]) -> integer -- return first index of value.\n", - " | Raises ValueError if the value is not present.\n", - " | \n", - " | insert(...)\n", - " | L.insert(index, object) -- insert object before index\n", - " | \n", - " | pop(...)\n", - " | L.pop([index]) -> item -- remove and return item at index (default last).\n", - " | Raises IndexError if list is empty or index is out of range.\n", - " | \n", - " | remove(...)\n", - " | L.remove(value) -> None -- remove first occurrence of value.\n", - " | Raises ValueError if the value is not present.\n", - " | \n", - " | reverse(...)\n", - " | L.reverse() -- reverse *IN PLACE*\n", - " | \n", - " | sort(...)\n", - " | L.sort(key=None, reverse=False) -> None -- stable sort *IN PLACE*\n", - " | \n", - " | ----------------------------------------------------------------------\n", - " | Data and other attributes defined here:\n", - " | \n", - " | __hash__ = None\n", - "\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "help(d)" ] @@ -901,64 +547,9 @@ }, { "cell_type": "code", - "execution_count": 89, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "['__add__',\n", - " '__class__',\n", - " '__contains__',\n", - " '__delattr__',\n", - " '__delitem__',\n", - " '__dir__',\n", - " '__doc__',\n", - " '__eq__',\n", - " '__format__',\n", - " '__ge__',\n", - " '__getattribute__',\n", - " '__getitem__',\n", - " '__gt__',\n", - " '__hash__',\n", - " '__iadd__',\n", - " '__imul__',\n", - " '__init__',\n", - " '__iter__',\n", - " '__le__',\n", - " '__len__',\n", - " '__lt__',\n", - " '__mul__',\n", - " '__ne__',\n", - " '__new__',\n", - " '__reduce__',\n", - " '__reduce_ex__',\n", - " '__repr__',\n", - " '__reversed__',\n", - " '__rmul__',\n", - " '__setattr__',\n", - " '__setitem__',\n", - " '__sizeof__',\n", - " '__str__',\n", - " '__subclasshook__',\n", - " 'append',\n", - " 'clear',\n", - " 'copy',\n", - " 'count',\n", - " 'extend',\n", - " 'index',\n", - " 'insert',\n", - " 'pop',\n", - " 'remove',\n", - " 'reverse',\n", - " 'sort']" - ] - }, - "execution_count": 89, - "metadata": {}, - "output_type": "execute_result" - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "dir(d)" ] @@ -978,20 +569,9 @@ }, { "cell_type": "code", - "execution_count": 90, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2\n", - "dict_keys(['b', 'a'])\n", - "dict_values([20, 10])\n", - "10\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "e = {'a' : 10, 'b': 20}\n", "print(len(e))\n", @@ -1015,17 +595,9 @@ }, { "cell_type": "code", - "execution_count": 91, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'c': 555, 'b': 20, 'a': 10}\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "e['c'] = 555 # just like in Biobank! ;)\n", "print(e)" @@ -1042,18 +614,9 @@ }, { "cell_type": "code", - "execution_count": 92, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'c': 555, 'a': 10}\n", - "{'a': 10}\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "e.pop('b')\n", "print(e)\n", @@ -1072,19 +635,9 @@ }, { "cell_type": "code", - "execution_count": 93, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "('c', 555)\n", - "('b', 20)\n", - "('a', 10)\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "e = {'a' : 10, 'b': 20, 'c':555}\n", "for k, v in e.items():\n", @@ -1102,19 +655,9 @@ }, { "cell_type": "code", - "execution_count": 94, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "('c', 555)\n", - "('b', 20)\n", - "('a', 10)\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "for k in e:\n", " print((k, e[k]))" @@ -1137,17 +680,9 @@ }, { "cell_type": "code", - "execution_count": 95, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "7\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "a = 7\n", "b = a\n", @@ -1164,17 +699,9 @@ }, { "cell_type": "code", - "execution_count": 96, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[8888]\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "a = [7]\n", "b = a\n", @@ -1191,17 +718,9 @@ }, { "cell_type": "code", - "execution_count": 97, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[7, 7]\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "a = [7]\n", "b = a * 2\n", @@ -1218,17 +737,9 @@ }, { "cell_type": "code", - "execution_count": 98, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[7]\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "a = [7]\n", "b = list(a)\n", @@ -1245,18 +756,9 @@ }, { "cell_type": "code", - "execution_count": 99, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "(2, 5, 7)\n", - "[2, 5, 7]\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "xt = (2, 5, 7)\n", "xl = list(xt)\n", @@ -1275,20 +777,9 @@ }, { "cell_type": "code", - "execution_count": 100, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[5]\n", - "[5, 10]\n", - "[5, 10]\n", - "[5, 10]\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "def foo1(x):\n", " x.append(10)\n", @@ -1326,21 +817,9 @@ }, { "cell_type": "code", - "execution_count": 101, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Not a is: False\n", - "Not 1 is: False\n", - "Not 0 is: True\n", - "Not {} is: True\n", - "{}==0 is: False\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "a = True\n", "print('Not a is:', not a)\n", @@ -1359,19 +838,9 @@ }, { "cell_type": "code", - "execution_count": 102, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "False\n", - "True\n", - "True\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "print('the' in 'a number of words')\n", "print('of' in 'a number of words')\n", @@ -1389,18 +858,9 @@ }, { "cell_type": "code", - "execution_count": 103, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "0.9933407850276534\n", - "Positive\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "import random\n", "a = random.uniform(-1, 1)\n", @@ -1422,17 +882,9 @@ }, { "cell_type": "code", - "execution_count": 104, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Variable is true, or at least not empty\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "a = [] # just one of many examples\n", "if not a:\n", @@ -1454,21 +906,9 @@ }, { "cell_type": "code", - "execution_count": 105, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2\n", - "is\n", - "more\n", - "than\n", - "1\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "for x in [2, 'is', 'more', 'than', 1]:\n", " print(x)" @@ -1485,23 +925,9 @@ }, { "cell_type": "code", - "execution_count": 106, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2\n", - "3\n", - "4\n", - "5\n", - "6\n", - "7\n", - "8\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "for x in range(2, 9):\n", " print(x)" @@ -1518,18 +944,9 @@ }, { "cell_type": "code", - "execution_count": 107, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "4\n", - "7\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "x, y = [4, 7]\n", "print(x)\n", @@ -1545,21 +962,9 @@ }, { "cell_type": "code", - "execution_count": 108, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[('Some', 0), ('set', 1), ('of', 2), ('items', 3)]\n", - "0 Some\n", - "1 set\n", - "2 of\n", - "3 items\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "alist = ['Some', 'set', 'of', 'items']\n", "blist = list(range(len(alist)))\n", @@ -1581,17 +986,9 @@ }, { "cell_type": "code", - "execution_count": 109, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "35.041627991396396\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "import random\n", "n = 0\n", @@ -1625,17 +1022,9 @@ }, { "cell_type": "code", - "execution_count": 110, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "0.6576892259376057 0.11717666603919556\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "import random\n", "x = random.uniform(0, 1)\n", @@ -1654,18 +1043,9 @@ }, { "cell_type": "code", - "execution_count": 111, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]\n", - "[0, 1, 4, 9, 16, 25, 36, 64, 81]\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "v1 = [ x**2 for x in range(10) ]\n", "print(v1)\n", @@ -1679,34 +1059,9 @@ "source": [ "You'll find that python programmers use this kind of construction _*a lot*_." ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.5.2" - } - }, + "metadata": {}, "nbformat": 4, "nbformat_minor": 2 } diff --git a/getting_started/basics.md b/getting_started/01_basics.md similarity index 100% rename from getting_started/basics.md rename to getting_started/01_basics.md diff --git a/getting_started/04_numpy.ipynb b/getting_started/04_numpy.ipynb index e877863b0d6105f54087da9a1759aab2a4cc8b31..23b4131ecdd19d26f411a08610cdbc0c851138f5 100644 --- a/getting_started/04_numpy.ipynb +++ b/getting_started/04_numpy.ipynb @@ -51,6 +51,7 @@ "* [Appendix A: Generating random numbers](#appendix-generating-random-numbers)\n", "* [Appendix B: Importing Numpy](#appendix-importing-numpy)\n", "* [Appendix C: Vectors in Numpy](#appendix-vectors-in-numpy)\n", + "* [Appendix D: The Numpy `matrix`](#appendix-the-numpy-matrix)\n", "\n", "* [Useful references](#useful-references)\n", "\n", @@ -120,7 +121,7 @@ "unwieldy when you have more than a couple of dimensions.\n", "\n", "\n", - "___Numy array == Matlab matrix:___ These are in contrast to the Numpy array\n", + "___Numpy array == Matlab matrix:___ These are in contrast to the Numpy array\n", "and Matlab matrix, which are both thin wrappers around a contiguous chunk of\n", "memory, and which provide blazing-fast performance (because behind the scenes\n", "in both Numpy and Matlab, it's C, C++ and FORTRAN all the way down).\n", @@ -186,7 +187,8 @@ "array, thus completely bypassing the use of Python lists and the costly\n", "list-to-array conversion. I'm emphasising this to help you understand the\n", "difference between Python lists and Numpy arrays. Apologies if you've already\n", - "got it, forgiveness please.\n", + "got it, [forgiveness\n", + "please](https://www.youtube.com/watch?v=ZeHflFNR4kQ&feature=youtu.be&t=128).\n", "\n", "\n", "<a class=\"anchor\" id=\"numpy-basics\"></a>\n", @@ -314,7 +316,7 @@ "> for more information.\n", "\n", "\n", - " Of course you can also save data out to a text file just as easily, with\n", + "Of course you can also save data out to a text file just as easily, with\n", "[`numpy.savetxt`](https://docs.scipy.org/doc/numpy/reference/generated/numpy.savetxt.html):" ] }, @@ -336,6 +338,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ + "> The `fmt` argument to the `numpy.savetxt` function uses a specification\n", + "> language similar to that used in the C `printf` function - in the example\n", + "> above, `'%i`' indicates that the values of the array should be output as\n", + "> signed integers. See the [`numpy.savetxt`\n", + "> documentation](https://docs.scipy.org/doc/numpy/reference/generated/numpy.savetxt.html)\n", + "> for more details on specifying the output format.\n", + "\n", + "\n", "<a class=\"anchor\" id=\"array-properties\"></a>\n", "### Array properties\n", "\n", @@ -368,7 +378,9 @@ "source": [ "> As depicted above, passing a Numpy array to the built-in `len` function will\n", "> only give you the length of the first dimension, so you will typically want\n", - "> to avoid using it - use the `size` attribute instead.\n", + "> to avoid using it - instead, use the `size` attribute if you want to know\n", + "> how many elements are in an array, or the `shape` attribute if you want to\n", + "> know the array shape.\n", "\n", "\n", "<a class=\"anchor\" id=\"descriptive-statistics\"></a>\n", @@ -402,6 +414,35 @@ "cell_type": "markdown", "metadata": {}, "source": [ + "These methods can also be applied to arrays with multiple dimensions:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "a = np.random.randint(1, 10, (3, 3))\n", + "print('a:')\n", + "print(a)\n", + "print('min: ', a.min())\n", + "print('row mins: ', a.min(axis=1))\n", + "print('col mins: ', a.min(axis=0))\n", + "print('Min index : ', a.argmin())\n", + "print('Row min indices: ', a.argmin(axis=1))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that, for a multi-dimensional array, the `argmin` and `argmax` methods\n", + "will return the (0-based) index of the minimum/maximum values into a\n", + "[flattened](https://docs.scipy.org/doc/numpy-1.14.0/reference/generated/numpy.ndarray.flatten.html)\n", + "view of the array.\n", + "\n", + "\n", "> <sup>2</sup> Python, being an object-oriented language, distinguishes\n", "> between _functions_ and _methods_. Hopefully we all know what a function is\n", "> - a _method_ is simply the term used to refer to a function that is\n", @@ -685,8 +726,8 @@ "froth coming out of your mouth. I guess you're angry that `a * b` didn't give\n", "you the matrix product, like it would have in Matlab. Well all I can say is\n", "that Numpy is not Matlab. Matlab operations are typically consistent with\n", - "linear algebra notation. This is not the case in Numpy. Get over it. Take a\n", - "calmative.\n", + "linear algebra notation. This is not the case in Numpy. Get over it.\n", + "[Get yourself a calmative](https://youtu.be/M_w_n-8w3IQ?t=32).\n", "\n", "\n", "<a class=\"anchor\" id=\"matrix-multiplication\"></a>\n", @@ -732,6 +773,48 @@ "> backwards-compatibility, go ahead and use it!\n", "\n", "\n", + "One potential source of confusion for those of you who are used to Matlab's\n", + "linear algebra-based take on things is that Numpy treats row and column\n", + "vectors differently - you should take a break now and skim over the [appendix\n", + "on vectors in Numpy](#appendix-vectors-in-numpy).\n", + "\n", + "\n", + "For matrix-by-vector multiplications, a 1-dimensional Numpy array may be\n", + "treated as _either_ a row vector _or_ a column vector, depending on where\n", + "it is in the expression:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "a = np.arange(1, 5).reshape((2, 2))\n", + "b = np.random.randint(1, 10, 2)\n", + "\n", + "print('a:')\n", + "print(a)\n", + "print('b:', b)\n", + "\n", + "print('a @ b - b is a column vector:')\n", + "print(a @ b)\n", + "print('b @ a - b is a row vector:')\n", + "print(b @ a)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If you really can't stand using `@` to denote matrix multiplication, and just\n", + "want things to be like they were back in Matlab-land, you do have the option\n", + "of using a different Numpy data type - the `matrix` - which behaves a bit more\n", + "like what you might expect from Matlab. You can find a brief overview of the\n", + "`matrix` data type in [the appendix](appendix-the-numpy-matrix).\n", + "\n", + "\n", + "\n", "<a class=\"anchor\" id=\"broadcasting\"></a>\n", "### Broadcasting\n", "\n", @@ -775,8 +858,7 @@ "source": [ "> Here we used a handy feature of the `reshape` method - if you pass `-1` for\n", "> the size of one dimension, it will automatically determine the size to use\n", - "> for that dimension. Take a look at [the\n", - "> appendix](#appendix-vectors-in-numpy) for a discussion on vectors in Numpy.\n", + "> for that dimension.\n", "\n", "\n", "Here is a more useful example, where we use broadcasting to de-mean the rows\n", @@ -1119,7 +1201,7 @@ "metadata": {}, "source": [ "The `numpy.where` function can be combined with boolean arrays to easily\n", - "generate of coordinate arrays for values which meet some condition:" + "generate coordinate arrays for values which meet some condition:" ] }, { @@ -1408,6 +1490,47 @@ "cell_type": "markdown", "metadata": {}, "source": [ + "<a class=\"anchor\" id=\"appendix-the-numpy-matrix\"></a>\n", + "## Appendix D: The Numpy `matrix`\n", + "\n", + "\n", + "By now you should be aware that a Numpy `array` does not behave in quite the\n", + "same way as a Matlab matrix. The primary difference between Numpy and Matlab\n", + "is that in Numpy, the `*` operator denotes element-wise multiplication,\n", + "gwhereas in Matlab, `*` denotes matrix multiplication.\n", + "\n", + "\n", + "Numpy does support the `@` operator for matrix multiplication, but if this is\n", + "a complete show-stopper for you - if you just can't bring yourself to write `A\n", + "@ B` to denote the matrix product of `A` and `B` - if you _must_ have your\n", + "code looking as Matlab-like as possible, then you should look into the Numpy\n", + "[`matrix`](https://docs.scipy.org/doc/numpy/reference/generated/numpy.matrix.html)\n", + "data type.\n", + "\n", + "\n", + "The `matrix` is an alternative to the `array` which essentially behaves more\n", + "like a Matlab matrix:\n", + "\n", + "* `matrix` objects always have exactly two dimensions.\n", + "* `a * b` denotes matrix multiplication, rather than elementwise\n", + " multiplication.\n", + "* `matrix` objects have `.H` and `.I` attributes, which are convenient ways to\n", + " access the conjugate transpose and inverse of the matrix respectively.\n", + "\n", + "\n", + "Note however that use of the `matrix` type is _not_ widespread, and if you use\n", + "it you will risk confusing others who are familiar with the much more commonly\n", + "used `array`, and who need to work with your code. In fact, the official Numpy\n", + "documentation [recommends against using the `matrix`\n", + "type](https://docs.scipy.org/doc/numpy-dev/user/numpy-for-matlab-users.html#array-or-matrix-which-should-i-use).\n", + "\n", + "\n", + "But if you are writing some very maths-heavy code, and you want your code to\n", + "be as clear and concise, and maths/Matlab-like as possible, then the `matrix`\n", + "type is there for you. Just make sure you document your code well to make it\n", + "clear to others what is going on!\n", + "\n", + "\n", "<a class=\"anchor\" id=\"useful-references\"></a>\n", "## Useful references\n", "\n", @@ -1417,7 +1540,8 @@ "* [Broadcasting in Numpy](https://docs.scipy.org/doc/numpy/user/basics.broadcasting.html)\n", "* [Indexing in Numpy](https://docs.scipy.org/doc/numpy/reference/arrays.indexing.html)\n", "* [Random sampling in `numpy.random`](https://docs.scipy.org/doc/numpy/reference/routines.random.html)\n", - "* [Python slicing](https://www.pythoncentral.io/how-to-slice-listsarrays-and-tuples-in-python/)" + "* [Python slicing](https://www.pythoncentral.io/how-to-slice-listsarrays-and-tuples-in-python/)\n", + "* [Numpy for Matlab users](https://docs.scipy.org/doc/numpy-dev/user/numpy-for-matlab-users.html)" ] } ], diff --git a/getting_started/04_numpy.md b/getting_started/04_numpy.md index dce3532bb6e37ba55f749cd3f7d25cf97878c7da..8ea10a8521108c14c0c4a37a39dca132e961da4d 100644 --- a/getting_started/04_numpy.md +++ b/getting_started/04_numpy.md @@ -45,6 +45,7 @@ out of date, but we will update it for the next release of FSL. * [Appendix A: Generating random numbers](#appendix-generating-random-numbers) * [Appendix B: Importing Numpy](#appendix-importing-numpy) * [Appendix C: Vectors in Numpy](#appendix-vectors-in-numpy) +* [Appendix D: The Numpy `matrix`](#appendix-the-numpy-matrix) * [Useful references](#useful-references) @@ -98,7 +99,7 @@ array in Matlab - they can store anything, but are extremely inefficient, and unwieldy when you have more than a couple of dimensions. -___Numy array == Matlab matrix:___ These are in contrast to the Numpy array +___Numpy array == Matlab matrix:___ These are in contrast to the Numpy array and Matlab matrix, which are both thin wrappers around a contiguous chunk of memory, and which provide blazing-fast performance (because behind the scenes in both Numpy and Matlab, it's C, C++ and FORTRAN all the way down). @@ -148,7 +149,8 @@ will be loading our data from text or binary files directly into a Numpy array, thus completely bypassing the use of Python lists and the costly list-to-array conversion. I'm emphasising this to help you understand the difference between Python lists and Numpy arrays. Apologies if you've already -got it, forgiveness please. +got it, [forgiveness +please](https://www.youtube.com/watch?v=ZeHflFNR4kQ&feature=youtu.be&t=128). <a class="anchor" id="numpy-basics"></a> @@ -236,7 +238,7 @@ print(data) > for more information. - Of course you can also save data out to a text file just as easily, with +Of course you can also save data out to a text file just as easily, with [`numpy.savetxt`](https://docs.scipy.org/doc/numpy/reference/generated/numpy.savetxt.html): @@ -250,6 +252,13 @@ with open('mydata.txt', 'rt') as f: ``` +> The `fmt` argument to the `numpy.savetxt` function uses a specification +> language similar to that used in the C `printf` function - in the example +> above, `'%i`' indicates that the values of the array should be output as +> signed integers. See the [`numpy.savetxt` +> documentation](https://docs.scipy.org/doc/numpy/reference/generated/numpy.savetxt.html) +> for more details on specifying the output format. + <a class="anchor" id="array-properties"></a> ### Array properties @@ -275,7 +284,9 @@ print('Length of first dimension: ', len(z)) > As depicted above, passing a Numpy array to the built-in `len` function will > only give you the length of the first dimension, so you will typically want -> to avoid using it - use the `size` attribute instead. +> to avoid using it - instead, use the `size` attribute if you want to know +> how many elements are in an array, or the `shape` attribute if you want to +> know the array shape. <a class="anchor" id="descriptive-statistics"></a> @@ -301,6 +312,27 @@ print('prod: ', a.prod()) ``` +These methods can also be applied to arrays with multiple dimensions: + + +``` +a = np.random.randint(1, 10, (3, 3)) +print('a:') +print(a) +print('min: ', a.min()) +print('row mins: ', a.min(axis=1)) +print('col mins: ', a.min(axis=0)) +print('Min index : ', a.argmin()) +print('Row min indices: ', a.argmin(axis=1)) +``` + + +Note that, for a multi-dimensional array, the `argmin` and `argmax` methods +will return the (0-based) index of the minimum/maximum values into a +[flattened](https://docs.scipy.org/doc/numpy-1.14.0/reference/generated/numpy.ndarray.flatten.html) +view of the array. + + > <sup>2</sup> Python, being an object-oriented language, distinguishes > between _functions_ and _methods_. Hopefully we all know what a function is > - a _method_ is simply the term used to refer to a function that is @@ -502,8 +534,8 @@ Wait ... what's that you say? Oh, I couldn't understand because of all the froth coming out of your mouth. I guess you're angry that `a * b` didn't give you the matrix product, like it would have in Matlab. Well all I can say is that Numpy is not Matlab. Matlab operations are typically consistent with -linear algebra notation. This is not the case in Numpy. Get over it. Take a -calmative. +linear algebra notation. This is not the case in Numpy. Get over it. +[Get yourself a calmative](https://youtu.be/M_w_n-8w3IQ?t=32). <a class="anchor" id="matrix-multiplication"></a> @@ -541,6 +573,40 @@ print(b.dot(a)) > backwards-compatibility, go ahead and use it! +One potential source of confusion for those of you who are used to Matlab's +linear algebra-based take on things is that Numpy treats row and column +vectors differently - you should take a break now and skim over the [appendix +on vectors in Numpy](#appendix-vectors-in-numpy). + + +For matrix-by-vector multiplications, a 1-dimensional Numpy array may be +treated as _either_ a row vector _or_ a column vector, depending on where +it is in the expression: + + +``` +a = np.arange(1, 5).reshape((2, 2)) +b = np.random.randint(1, 10, 2) + +print('a:') +print(a) +print('b:', b) + +print('a @ b - b is a column vector:') +print(a @ b) +print('b @ a - b is a row vector:') +print(b @ a) +``` + + +If you really can't stand using `@` to denote matrix multiplication, and just +want things to be like they were back in Matlab-land, you do have the option +of using a different Numpy data type - the `matrix` - which behaves a bit more +like what you might expect from Matlab. You can find a brief overview of the +`matrix` data type in [the appendix](appendix-the-numpy-matrix). + + + <a class="anchor" id="broadcasting"></a> ### Broadcasting @@ -576,8 +642,7 @@ print(a * b.reshape(-1, 1)) > Here we used a handy feature of the `reshape` method - if you pass `-1` for > the size of one dimension, it will automatically determine the size to use -> for that dimension. Take a look at [the -> appendix](#appendix-vectors-in-numpy) for a discussion on vectors in Numpy. +> for that dimension. Here is a more useful example, where we use broadcasting to de-mean the rows @@ -834,7 +899,7 @@ for r, c, v in zip(rows, cols, indexed): The `numpy.where` function can be combined with boolean arrays to easily -generate of coordinate arrays for values which meet some condition: +generate coordinate arrays for values which meet some condition: ``` @@ -1052,6 +1117,47 @@ print(np.atleast_2d(r).T) ``` +<a class="anchor" id="appendix-the-numpy-matrix"></a> +## Appendix D: The Numpy `matrix` + + +By now you should be aware that a Numpy `array` does not behave in quite the +same way as a Matlab matrix. The primary difference between Numpy and Matlab +is that in Numpy, the `*` operator denotes element-wise multiplication, +gwhereas in Matlab, `*` denotes matrix multiplication. + + +Numpy does support the `@` operator for matrix multiplication, but if this is +a complete show-stopper for you - if you just can't bring yourself to write `A +@ B` to denote the matrix product of `A` and `B` - if you _must_ have your +code looking as Matlab-like as possible, then you should look into the Numpy +[`matrix`](https://docs.scipy.org/doc/numpy/reference/generated/numpy.matrix.html) +data type. + + +The `matrix` is an alternative to the `array` which essentially behaves more +like a Matlab matrix: + +* `matrix` objects always have exactly two dimensions. +* `a * b` denotes matrix multiplication, rather than elementwise + multiplication. +* `matrix` objects have `.H` and `.I` attributes, which are convenient ways to + access the conjugate transpose and inverse of the matrix respectively. + + +Note however that use of the `matrix` type is _not_ widespread, and if you use +it you will risk confusing others who are familiar with the much more commonly +used `array`, and who need to work with your code. In fact, the official Numpy +documentation [recommends against using the `matrix` +type](https://docs.scipy.org/doc/numpy-dev/user/numpy-for-matlab-users.html#array-or-matrix-which-should-i-use). + + +But if you are writing some very maths-heavy code, and you want your code to +be as clear and concise, and maths/Matlab-like as possible, then the `matrix` +type is there for you. Just make sure you document your code well to make it +clear to others what is going on! + + <a class="anchor" id="useful-references"></a> ## Useful references @@ -1061,4 +1167,5 @@ print(np.atleast_2d(r).T) * [Broadcasting in Numpy](https://docs.scipy.org/doc/numpy/user/basics.broadcasting.html) * [Indexing in Numpy](https://docs.scipy.org/doc/numpy/reference/arrays.indexing.html) * [Random sampling in `numpy.random`](https://docs.scipy.org/doc/numpy/reference/routines.random.html) -* [Python slicing](https://www.pythoncentral.io/how-to-slice-listsarrays-and-tuples-in-python/) \ No newline at end of file +* [Python slicing](https://www.pythoncentral.io/how-to-slice-listsarrays-and-tuples-in-python/) +* [Numpy for Matlab users](https://docs.scipy.org/doc/numpy-dev/user/numpy-for-matlab-users.html) diff --git a/getting_started/nifti.ipynb b/getting_started/05_nifti.ipynb similarity index 78% rename from getting_started/nifti.ipynb rename to getting_started/05_nifti.ipynb index 994edd80c08830bcf5e2d0ab2bb116f1ac5f4029..f157ec0113f48fb0a2d900017b926ed1db17116d 100644 --- a/getting_started/nifti.ipynb +++ b/getting_started/05_nifti.ipynb @@ -15,17 +15,9 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "(182, 218, 182)\n" - ] - } - ], + "outputs": [], "source": [ "import numpy as np\n", "import nibabel as nib\n", @@ -65,17 +57,9 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "(1.0, 1.0, 1.0)\n" - ] - } - ], + "outputs": [], "source": [ "voxsize = imhdr.get_zooms()\n", "print(voxsize)" @@ -92,21 +76,9 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "4\n", - "[[ -1. 0. 0. 90.]\n", - " [ 0. 1. 0. -126.]\n", - " [ 0. 0. 1. -72.]\n", - " [ 0. 0. 0. 1.]]\n" - ] - } - ], + "outputs": [], "source": [ "sform = imhdr.get_sform()\n", "sformcode = imhdr['sform_code']\n", @@ -127,7 +99,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -147,25 +119,7 @@ ] } ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.5.2" - } - }, + "metadata": {}, "nbformat": 4, "nbformat_minor": 2 } diff --git a/getting_started/nifti.md b/getting_started/05_nifti.md similarity index 100% rename from getting_started/nifti.md rename to getting_started/05_nifti.md diff --git a/getting_started/scripts.ipynb b/getting_started/08_scripts.ipynb similarity index 63% rename from getting_started/scripts.ipynb rename to getting_started/08_scripts.ipynb index 7b87ec6388573c9cebb864bdbea98da654593e9c..1d3d705d5b636c281ce97e6a527431c69075847d 100644 --- a/getting_started/scripts.ipynb +++ b/getting_started/08_scripts.ipynb @@ -18,9 +18,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "#!/usr/bin/env python" @@ -38,9 +36,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "#!/usr/bin/env fslpython" @@ -60,9 +56,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "import subprocess as sp\n", @@ -79,9 +73,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "spobj = sp.run(['ls'], stdout = sp.PIPE)" @@ -97,9 +89,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "spobj = sp.run('ls -la'.split(), stdout = sp.PIPE)\n", @@ -119,9 +109,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "import os\n", @@ -143,9 +131,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "commands = \"\"\"\n", @@ -176,9 +162,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "import sys\n", @@ -200,19 +184,10 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Usage: bash <input image> <output image>\n" - ] - } - ], + "outputs": [], "source": [ - "%%bash\n", "#!/bin/bash\n", "if [ $# -lt 2 ] ; then\n", " echo \"Usage: $0 <input image> <output image>\"\n", @@ -238,21 +213,9 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "ename": "IndexError", - "evalue": "list index out of range", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mIndexError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m<ipython-input-2-f7378930c369>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m()\u001b[0m\n\u001b[1;32m 13\u001b[0m \u001b[0mspobj\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0msp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrun\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mfsldir\u001b[0m\u001b[0;34m+\u001b[0m\u001b[0;34m'/bin/fslstats'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0moutfile\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m'-V'\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstdout\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0msp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mPIPE\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 14\u001b[0m \u001b[0msout\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mspobj\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mstdout\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdecode\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'utf-8'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 15\u001b[0;31m \u001b[0mvol_vox\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mfloat\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msout\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msplit\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 16\u001b[0m \u001b[0mvol_mm\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mfloat\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msout\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msplit\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 17\u001b[0m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'Volumes are: '\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mvol_vox\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m' in voxels and '\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mvol_mm\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m' in mm'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mIndexError\u001b[0m: list index out of range" - ], - "output_type": "error" - } - ], + "outputs": [], "source": [ "#!/usr/bin/env fslpython\n", "import os, sys\n", @@ -272,55 +235,9 @@ "vol_mm = float(sout.split()[1])\n", "print('Volumes are: ', vol_vox, ' in voxels and ', vol_mm, ' in mm')" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [] } ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.2" - }, - "toc": { - "colors": { - "hover_highlight": "#DAA520", - "running_highlight": "#FF0000", - "selected_highlight": "#FFD700" - }, - "moveMenuLeft": true, - "nav_menu": { - "height": "105px", - "width": "252px" - }, - "navigate_menu": true, - "number_sections": true, - "sideBar": true, - "threshold": 4.0, - "toc_cell": false, - "toc_section_display": "block", - "toc_window_display": false - } - }, + "metadata": {}, "nbformat": 4, "nbformat_minor": 2 } diff --git a/getting_started/scripts.md b/getting_started/08_scripts.md similarity index 100% rename from getting_started/scripts.md rename to getting_started/08_scripts.md