From 075df22da9c16f583c693e141e5c326e3d341245 Mon Sep 17 00:00:00 2001
From: Paul McCarthy <pauldmccarthy@gmail.com>
Date: Wed, 7 Feb 2018 12:36:32 +0000
Subject: [PATCH] Sorted out modules/packages practical

---
 advanced_topics/modules_and_packages.ipynb | 226 ++++++++++++++++++++-
 advanced_topics/modules_and_packages.md    | 180 +++++++++++++++-
 2 files changed, 396 insertions(+), 10 deletions(-)

diff --git a/advanced_topics/modules_and_packages.ipynb b/advanced_topics/modules_and_packages.ipynb
index 722d9c0..cb1f719 100644
--- a/advanced_topics/modules_and_packages.ipynb
+++ b/advanced_topics/modules_and_packages.ipynb
@@ -20,7 +20,22 @@
     "\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/`."
+    "`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)"
    ]
   },
   {
@@ -37,6 +52,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
+    "<a class=\"anchor\" id=\"what-is-a-module\"></a>\n",
     "## What is a module?\n",
     "\n",
     "\n",
@@ -64,6 +80,7 @@
     "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",
@@ -106,6 +123,7 @@
     "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",
@@ -128,6 +146,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
+    "<a class=\"anchor\" id=\"importing-everything-from-a-module\"></a>\n",
     "### Importing everything from a module\n",
     "\n",
     "\n",
@@ -178,7 +197,7 @@
    "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 outweighs the\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:"
@@ -200,6 +219,10 @@
    "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:"
@@ -221,7 +244,30 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "## What happens when I import a module?\n",
+    "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",
@@ -285,14 +331,184 @@
    "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",
-    "# Useful references\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 in Python](https://docs.python.org/3.5/tutorial/modules.html)"
+    "* [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)"
    ]
   }
  ],
diff --git a/advanced_topics/modules_and_packages.md b/advanced_topics/modules_and_packages.md
index 3506a93..f459a1c 100644
--- a/advanced_topics/modules_and_packages.md
+++ b/advanced_topics/modules_and_packages.md
@@ -17,12 +17,26 @@ 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?
 
 
@@ -42,6 +56,7 @@ 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
 
 
@@ -68,6 +83,7 @@ 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
 
 
@@ -82,6 +98,7 @@ print(add(1, 3))
 ```
 
 
+<a class="anchor" id="importing-everything-from-a-module"></a>
 ### Importing everything from a module
 
 
@@ -116,7 +133,7 @@ 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 outweighs the
+requires you to type more code, the benefits of doing this far outweigh the
 hassle of typing a few extra characters - it becomes much easier to read and
 trace through code when the functions you use are accessed through a namespace
 for each module:
@@ -129,6 +146,9 @@ 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
@@ -143,7 +163,21 @@ print('string add: ', sf.add(1, 2))
 ```
 
 
-## What happens when I import a module?
+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
@@ -190,12 +224,148 @@ 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()
+```
 
 
-# Useful references
+<a class="anchor" id="useful-references"></a>
+## Useful references
 
-* [Modules in Python](https://docs.python.org/3.5/tutorial/modules.html)
\ No newline at end of file
+* [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
-- 
GitLab