diff --git a/getting_started/03_file_management.ipynb b/getting_started/03_file_management.ipynb index 3aa9736d36d72f754dfeae3a7628d429afd2e55f..2dd68d75c84b58c1775c4740eadcfa19e97dd9ad 100644 --- a/getting_started/03_file_management.ipynb +++ b/getting_started/03_file_management.ipynb @@ -59,6 +59,8 @@ " * [Re-name subject directories](#re-name-subject-directories)\n", " * [Re-organise a data set](#re-organise-a-data-set)\n", " * [Solutions](#solutions)\n", + "* [Appendix: Exceptions](#appendix-exceptions)\n", + "\n", "\n", "\n", "<a class=\"anchor\" id=\"managing-files-and-directories\"></a>\n", @@ -456,9 +458,12 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "> This is the first time in this series of practicals that we have defined our\n", - "> own function, [hooray!](https://www.youtube.com/watch?v=zQiibNVIvK4) All\n", - "> function definitions in Python begin with the `def` keyword:\n", + "> This is the first time in a while that we have defined our own function,\n", + "> [hooray!](https://www.youtube.com/watch?v=zQiibNVIvK4). Here's a quick\n", + "> refresher on how to write functions in Python, in case you have forgotten.\n", + ">\n", + "> First of all, all function definitions in Python begin with the `def`\n", + "> keyword:\n", ">\n", "> ```\n", "> def myfunction():\n", @@ -550,10 +555,10 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "> Note here that `op.split` returns both the directory and base names - it is\n", - "> super easy to define a Python function that returns multiple values, simply by\n", - "> having it return a tuple. For example, the implementation of `op.split` might\n", - "> look something like this:\n", + "> Note here that `op.split` returns both the directory and base names - remember\n", + "> that it is super easy to define a Python function that returns multiple values,\n", + "> simply by having it return a tuple. For example, the implementation of\n", + "> `op.split` might look something like this:\n", ">\n", ">\n", "> ```\n", @@ -1031,6 +1036,367 @@ " formatter.get_style_defs('.highlight'),\n", " highlight(code, PythonLexer(), formatter)))" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "<a class=\"anchor\" id=\"appendix-exceptions\"></a>\n", + "## Appendix: Exceptions\n", + "\n", + "\n", + "At some point in your life, a piece of code that you write is inevitably going\n", + "to fail, and you are going to have to deal with it. This is particularly\n", + "relevant to file management tasks - many of the functions that have been\n", + "introduced in this practical can fail for all kinds of reasons, such as\n", + "incorrect permissions or ownership, lack of disk space, or a network file\n", + "system going down.\n", + "\n", + "\n", + "Any statement in Python can potentially result in an error. When a line of\n", + "code triggers an error, we say that it _raises_ the error (a.k.a. _throws_ in\n", + "other languages). When an error occurs, an `Exception` object is raised,\n", + "causing execution to stop at the line that caused the error:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "a = [1, 2, 3]\n", + "a.remove(4)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The word `Exception` is used instead of `Error` because not all exceptions are\n", + "errors. For example, when you type CTRL+C into a running Python program, a\n", + "`KeyboardInterrupt` exception will be raised.\n", + "\n", + "\n", + "> There are many different types of exceptions in Python - a list of all the\n", + "> built-in ones can be found\n", + "> [here](https://docs.python.org/3.5/library/exceptions.html). It is also easy\n", + "> to define your own exceptions by creating a sub-class of `Exception` (beyond\n", + "> the scope of this practical).\n", + "\n", + "\n", + "Fortunately Python gives us the capability to _catch_ exceptions when they are\n", + "raised, using the `try` and `except` keywords. As an example, let's say that\n", + "the user asked our program to create a directory somewhere on the file system.\n", + "A real program would need to handle situations in which that directory cannot\n", + "be created - we might do it like this in Python:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "dirpath = '/sbin/foo'\n", + "\n", + "try:\n", + " os.mkdir(dirpath)\n", + "\n", + "except OSError as e:\n", + " print('Could not create {}! Reason: {}'.format(dirpath, e))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this example, we have put the `os.mkdir` call inside a `try:` block. Now,\n", + "if it raises an `Exception` of type `OSError`, that `OSError` will be _caught_\n", + "and passed to the `except:` block. A `try` block must always followed by an\n", + "`except` block (and/or a `finally` block - keep reading).\n", + "\n", + "\n", + "The `except OSError as e:` line means: _if any code in the `try` block raises\n", + "an `Exception` of type `OSError`, then catch it, assign it to a variable\n", + "called `e`, and pass it to the code inside the `except` block._\n", + "\n", + "\n", + "### Catching different types of exceptions\n", + "\n", + "\n", + "It is common for a piece of code to have the potential to raise different\n", + "types of exceptions. Python allows you to have multiple `except` blocks\n", + "associated with a single `try` block, so you can handle different types of\n", + "exceptions in different ways. For example, you might want to print a useful\n", + "error message so the user knows what has gone wrong:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "numerator = '123'\n", + "denominator = 0\n", + "\n", + "try:\n", + " numerator = float(numerator)\n", + " print(numerator / denominator)\n", + "\n", + "except TypeError as e:\n", + " print('Numerator and/or denominator are of the wrong type!')\n", + " print(' ', e)\n", + "\n", + "except ValueError as e:\n", + " print('Numerator is not a float!')\n", + " print(' ', e)\n", + "\n", + "# Note that specifying a variable to refer\n", + "# to the Exception object is optional.\n", + "except ZeroDivisionError:\n", + " print('Denominator is zero!')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Experiment with the above code block - try out different values for the\n", + "`numerator` and `denominator`, and see what happens.\n", + "\n", + "\n", + "You can also specify different types of exceptions in a single `except`\n", + "statement:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "numerator = '123'\n", + "denominator = 0\n", + "try:\n", + " numerator = float(numerator)\n", + " print(numerator / denominator)\n", + "\n", + "except (TypeError, ZeroDivisionError) as e:\n", + " print('Numerator and/or denominator are of the '\n", + " 'wrong type, or the denominator is zero!')\n", + " print(' ', e)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### The catch-all approach\n", + "\n", + "\n", + "Instead of specifying all of the different types of exceptions that could\n", + "occur, it is possible to simply use a single `except` block to catch all\n", + "exceptions of type `Exception`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "numerator = 'abc'\n", + "denominator = 1\n", + "\n", + "try:\n", + " numerator = float(numerator)\n", + " print(numerator / denominator)\n", + "\n", + "except Exception as e:\n", + " print('Something is wrong with numerator or denominator!')\n", + " print(' ', e)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It is generally better practice to be as specific as possible when you are\n", + "catching exceptions, but sometimes all you care about is whether your code\n", + "worked or didn't, and in this case the you can simply use this catch-all\n", + "approach.\n", + "\n", + "\n", + "__Warning:__ Even though it is possible to, you should __never__ write a\n", + "`try`-`except` block like this:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " # do stuff\n", + " pass\n", + "\n", + "except:\n", + " # handle exceptions\n", + " pass" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You don't actually have to specify any exception type in an `except`\n", + "statement. But you should never do this! As we have already mentioned, not\n", + "all exceptions are errors. The above code will catch _all_ exceptions, even\n", + "those which do not inherit from the standard `Exception` class. This includes\n", + "important exceptions such as `KeyboardInterrupt` and `SystemExit`, which\n", + "control important aspects of your program's behaviour.\n", + "\n", + "\n", + "So you should always, at the very least, specify the `Exception` type:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " # do stuff\n", + " pass\n", + "\n", + "except Exception:\n", + " # handle exceptions\n", + " pass" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### The `finally` keyword\n", + "\n", + "\n", + "Sometimes, when you are performing a task, you might have some clean-up logic\n", + "that must be executed regardless of whether the task succeeded or failed. The\n", + "canonical example here is that if you open a file, you must make sure that to\n", + "close it when you are finished, otherwise its contents may be corrupted." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "f = open('raw_mri_data/subj_1/t1.nii', 'rb')\n", + "try:\n", + " f.write('ho hum')\n", + "\n", + "except IOError as e:\n", + " print('Error occurred!: ', e)\n", + "finally:\n", + " print('Closing file')\n", + " f.close()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It is possible to use `try` and `finally` without an `except` block. This is\n", + "useful if you have some code that needs some clean-up logic, but you don't\n", + "actually want to catch the exception - sometimes it is better for a program\n", + "to crash, rather than for errors to be silently suppressed, because it can\n", + "be easier to figure out what went wrong:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "f = open('raw_mri_data/subj_1/t1.nii', 'rb')\n", + "\n", + "try:\n", + " f.write('ho hum')\n", + "\n", + "finally:\n", + " print('Closing file')\n", + " f.close()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> The above was just an example - it is generally better practice to use the\n", + "> `with` statement when opening files.\n", + "\n", + "\n", + "You can read more about handling exceptions in Python\n", + "[here](https://docs.python.org/3.5/tutorial/errors.html).\n", + "\n", + "\n", + "### Raising exceptions\n", + "\n", + "\n", + "It is possible to generate your own exception at any point by using the\n", + "`raise` keyword, and passing it an `Exception` object:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "raise Exception('Kaboom!')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This can be useful if your code detects that something has gone wrong, and\n", + "needs to abort.\n", + "\n", + "\n", + "You can also raise an existing `Exception` from within an `except` block:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " print(1 / 0)\n", + "\n", + "except Exception:\n", + " print('Some error occurred!')\n", + " raise" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This can be useful if you want to print a message when an exception occurs,\n", + "but also allow the execption to be propagated upwards." + ] } ], "metadata": {}, diff --git a/getting_started/03_file_management.md b/getting_started/03_file_management.md index 10b9f042967cdcc990e3b8109e5cbbf0787a5456..63b47989412b35a5a805f997ddbc5e424254f44f 100644 --- a/getting_started/03_file_management.md +++ b/getting_started/03_file_management.md @@ -53,6 +53,8 @@ other sections as a reference. You might miss out on some neat tricks though. * [Re-name subject directories](#re-name-subject-directories) * [Re-organise a data set](#re-organise-a-data-set) * [Solutions](#solutions) +* [Appendix: Exceptions](#appendix-exceptions) + <a class="anchor" id="managing-files-and-directories"></a> @@ -799,3 +801,272 @@ def print_solution(extitle): formatter.get_style_defs('.highlight'), highlight(code, PythonLexer(), formatter))) ``` + + +<a class="anchor" id="appendix-exceptions"></a> +## Appendix: Exceptions + + +At some point in your life, a piece of code that you write is inevitably going +to fail, and you are going to have to deal with it. This is particularly +relevant to file management tasks - many of the functions that have been +introduced in this practical can fail for all kinds of reasons, such as +incorrect permissions or ownership, lack of disk space, or a network file +system going down. + + +Any statement in Python can potentially result in an error. When a line of +code triggers an error, we say that it _raises_ the error (a.k.a. _throws_ in +other languages). When an error occurs, an `Exception` object is raised, +causing execution to stop at the line that caused the error: + + +``` +a = [1, 2, 3] +a.remove(4) +``` + + +The word `Exception` is used instead of `Error` because not all exceptions are +errors. For example, when you type CTRL+C into a running Python program, a +`KeyboardInterrupt` exception will be raised. + + +> There are many different types of exceptions in Python - a list of all the +> built-in ones can be found +> [here](https://docs.python.org/3.5/library/exceptions.html). It is also easy +> to define your own exceptions by creating a sub-class of `Exception` (beyond +> the scope of this practical). + + +Fortunately Python gives us the capability to _catch_ exceptions when they are +raised, using the `try` and `except` keywords. As an example, let's say that +the user asked our program to create a directory somewhere on the file system. +A real program would need to handle situations in which that directory cannot +be created - we might do it like this in Python: + + +``` +import os + +dirpath = '/sbin/foo' + +try: + os.mkdir(dirpath) + +except OSError as e: + print('Could not create {}! Reason: {}'.format(dirpath, e)) +``` + + +In this example, we have put the `os.mkdir` call inside a `try:` block. Now, +if it raises an `Exception` of type `OSError`, that `OSError` will be _caught_ +and passed to the `except:` block. A `try` block must always followed by an +`except` block (and/or a `finally` block - keep reading). + + +The `except OSError as e:` line means: _if any code in the `try` block raises +an `Exception` of type `OSError`, then catch it, assign it to a variable +called `e`, and pass it to the code inside the `except` block._ + + +### Catching different types of exceptions + + +It is common for a piece of code to have the potential to raise different +types of exceptions. Python allows you to have multiple `except` blocks +associated with a single `try` block, so you can handle different types of +exceptions in different ways. For example, you might want to print a useful +error message so the user knows what has gone wrong: + + +``` +numerator = '123' +denominator = 0 + +try: + numerator = float(numerator) + print(numerator / denominator) + +except TypeError as e: + print('Numerator and/or denominator are of the wrong type!') + print(' ', e) + +except ValueError as e: + print('Numerator is not a float!') + print(' ', e) + +# Note that specifying a variable to refer +# to the Exception object is optional. +except ZeroDivisionError: + print('Denominator is zero!') +``` + + +Experiment with the above code block - try out different values for the +`numerator` and `denominator`, and see what happens. + + +You can also specify different types of exceptions in a single `except` +statement: + + +``` +numerator = '123' +denominator = 0 +try: + numerator = float(numerator) + print(numerator / denominator) + +except (TypeError, ZeroDivisionError) as e: + print('Numerator and/or denominator are of the ' + 'wrong type, or the denominator is zero!') + print(' ', e) +``` + + +### The catch-all approach + + +Instead of specifying all of the different types of exceptions that could +occur, it is possible to simply use a single `except` block to catch all +exceptions of type `Exception`: + + +``` +numerator = 'abc' +denominator = 1 + +try: + numerator = float(numerator) + print(numerator / denominator) + +except Exception as e: + print('Something is wrong with numerator or denominator!') + print(' ', e) + +``` + + +It is generally better practice to be as specific as possible when you are +catching exceptions, but sometimes all you care about is whether your code +worked or didn't, and in this case the you can simply use this catch-all +approach. + + +__Warning:__ Even though it is possible to, you should __never__ write a +`try`-`except` block like this: + + +``` +try: + # do stuff + pass + +except: + # handle exceptions + pass +``` + + +You don't actually have to specify any exception type in an `except` +statement. But you should never do this! As we have already mentioned, not +all exceptions are errors. The above code will catch _all_ exceptions, even +those which do not inherit from the standard `Exception` class. This includes +important exceptions such as `KeyboardInterrupt` and `SystemExit`, which +control important aspects of your program's behaviour. + + +So you should always, at the very least, specify the `Exception` type: + + +``` +try: + # do stuff + pass + +except Exception: + # handle exceptions + pass +``` + + +### The `finally` keyword + + +Sometimes, when you are performing a task, you might have some clean-up logic +that must be executed regardless of whether the task succeeded or failed. The +canonical example here is that if you open a file, you must make sure that to +close it when you are finished, otherwise its contents may be corrupted. + + +``` +f = open('raw_mri_data/subj_1/t1.nii', 'rb') +try: + f.write('ho hum') + +except IOError as e: + print('Error occurred!: ', e) +finally: + print('Closing file') + f.close() +``` + + +It is possible to use `try` and `finally` without an `except` block. This is +useful if you have some code that needs some clean-up logic, but you don't +actually want to catch the exception - sometimes it is better for a program +to crash, rather than for errors to be silently suppressed, because it can +be easier to figure out what went wrong: + + +``` +f = open('raw_mri_data/subj_1/t1.nii', 'rb') + +try: + f.write('ho hum') + +finally: + print('Closing file') + f.close() +``` + + +> The above was just an example - it is generally better practice to use the +> `with` statement when opening files. + + +You can read more about handling exceptions in Python +[here](https://docs.python.org/3.5/tutorial/errors.html). + + +### Raising exceptions + + +It is possible to generate your own exception at any point by using the +`raise` keyword, and passing it an `Exception` object: + + +``` +raise Exception('Kaboom!') +``` + + +This can be useful if your code detects that something has gone wrong, and +needs to abort. + + +You can also raise an existing `Exception` from within an `except` block: + + +``` +try: + print(1 / 0) + +except Exception: + print('Some error occurred!') + raise +``` + +This can be useful if you want to print a message when an exception occurs, +but also allow the execption to be propagated upwards.