diff --git a/ch01python/00pythons.ipynb.py b/ch01python/00pythons.ipynb.py index 8d0d63de..43365bf8 100644 --- a/ch01python/00pythons.ipynb.py +++ b/ch01python/00pythons.ipynb.py @@ -12,224 +12,252 @@ # --- # %% [markdown] -# # Introduction to Python - -# %% [markdown] -# ## Introduction - -# %% [markdown] -# ### Why teach Python? - -# %% [markdown] +# # Introduction +# +# ## Why teach Python? # # * In this first session, we will introduce [Python](http://www.python.org). # * This course is about programming for data analysis and visualisation in research. # * It's not mainly about Python. # * But we have to use some language. # - -# %% [markdown] # ### Why Python? - -# %% [markdown] # -# * Python is quick to program in -# * Python is popular in research, and has lots of libraries for science -# * Python interfaces well with faster languages +# * Python has a readable [syntax](https://en.wikipedia.org/wiki/Syntax_(programming_languages)) +# that makes it relatively quick to pick up. +# * Python is popular in research, and has lots of libraries for science. +# * Python interfaces well with faster languages. # * Python is free, so you'll never have a problem getting hold of it, wherever you go. # - -# %% [markdown] +# # ### Why write programs for research? - -# %% [markdown] # -# * Not just labour saving -# * Scripted research can be tested and reproduced +# * Not just labour saving. +# * Scripted research can be tested and reproduced. +# +# ### Sensible Input - Reasonable Output # - -# %% [markdown] -# ### Sensible Input - Reasonable Output - -# %% [markdown] # Programs are a rigorous way of describing data analysis for other researchers, as well as for computers. # -# Computational research suffers from people assuming each other's data manipulation is correct. By sharing codes, -# which are much more easy for a non-author to understand than spreadsheets, we can avoid the "SIRO" problem. The old saw "Garbage in Garbage out" is not the real problem for science: +# Computational research suffers from people assuming each other's data manipulation is correct. +# By sharing _readable_, _reproducible_ and _well-tested_ code, which makes all of the data processing +# steps used in an analysis explicit and checks that each of those steps behaves as expected, we enable +# other researchers to understand and assesss the validity of those analysis steps for themselves. +# In a research code context the problem is generally not so much _garbage in, garbage out_, but _sensible +# input, reasonable output_: 'black-box' analysis pipelines that given sensible looking data inputs produce +# reasonable appearing but incorrect analyses as outputs. # -# * Sensible input -# * Reasonable output +# ## Many kinds of Python # +# ### Python notebooks # - -# %% [markdown] -# ## Many kinds of Python - -# %% [markdown] -# ### The Jupyter Notebook - -# %% [markdown] -# The easiest way to get started using Python, and one of the best for research data work, is the Jupyter Notebook. - -# %% [markdown] -# In the notebook, you can easily mix code with discussion and commentary, and mix code with the results of that code; -# including graphs and other data visualisations. +# A particularly easy way to get started using Python, and one particularly suited to the sort of +# exploratory work common in a research context, is using [Jupyter](https://jupyter.org/https://jupyter.org/) +# notebooks. +# +# In a notebook, you can easily mix code with discussion and commentary, and display the results +# outputted by code alongside the code itself, including graphs and other data visualisations. +# +# For example if we wish to plot a figure-eight curve +# ([lemniscate](https://en.wikipedia.org/wiki/Lemniscate_of_Gerono)), we can include the parameteric +# equations $x = \sin(2\theta) / 2, y = \cos(\theta), \theta \in [0, 2\pi)$ which mathematically define +# the curve as well as corresponding Python code to plot the curve and the output of that code all within +# the same notebook: # %% -### Make plot -# %matplotlib inline -import math - +# Plot lemniscate curve import numpy as np import matplotlib.pyplot as plt -theta = np.arange(0, 4 * math.pi, 0.1) -eight = plt.figure() -axes = eight.add_axes([0, 0, 1, 1]) -axes.plot(0.5 * np.sin(theta), np.cos(theta / 2)) +theta = np.linspace(0, 2 * np.pi, 100) +x = np.sin(2 * theta) / 2 +y = np.cos(theta) +fig, ax = plt.subplots(figsize=(3, 6)) +lines = ax.plot(x, y) # %% [markdown] -# These notes are created using Jupyter notebooks and you may want to use it during the course. However, Jupyter notebooks won't be used for most of the activities and exercises done in class. To get hold of a copy of the notebook, follow the setup instructions shown on the course website, use the installation in Desktop@UCL (available in the teaching cluster rooms or [anywhere](https://www.ucl.ac.uk/isd/services/computers/remote-access/desktopucl-anywhere)), or go clone the [repository](https://github.com/UCL/rsd-engineeringcourse) on GitHub. - -# %% [markdown] -# Jupyter notebooks consist of discussion cells, referred to as "markdown cells", and "code cells", which contain Python. This document has been created using Jupyter notebook, and this very cell is a **Markdown Cell**. +# +# #### Notebook cells +# +# Jupyter notebooks consist of sequence of _cells_. Cells can be of two main types: +# +# * _Markdown cells_: Cells containing descriptive text and discussion with rich-text formatting +# via the [Markdown](https://en.wikipedia.org/wiki/Markdown) text markup language. +# * _Code cells_: Cells containing Python code, which is displayed with syntax highlighting. +# The results returned by the computation performed when running the cell are displayed below the cell as the cell _output_, with Jupyter having a _rich display_ system allowing embedding a range of different outputs including for example static images, videos and interactive widgets. +# +# The document you are currently reading is a Jupyter notebook, and this text you are reading is +# Markdown cell in the notebook. Below we see an example of a code cell. # %% print("This cell is a code cell") -# %% [markdown] -# Code cell inputs are numbered, and show the output below. +# %% [markdown] jp-MarkdownHeadingCollapsed=true +# Code cell inputs are numbered, with the cell output shown immediately below the input. Here the output +# is the text that we instruct the cell to print to the standard output stream. Cells will also display +# a representation of the value outputted by the last line in the cell, if any. For example + +# %% +print("This text will be displayed\n") +"This is text will also be displayed\n" # %% [markdown] -# Markdown cells contain text which uses a simple format to achive pretty layout, -# for example, to obtain: +# There is a small difference in the formatting of the output here, with the `print` function displaying +# the text without quotation mark delimiters and with any _escaped_ special characters (such as the +# `"\n"` newline character here) processed. +# +# #### Markdown formatting # -# **bold**, *italic* +# The Markdown language used in Markdown cells provides a simple way to add basic text formatting to the +# rendered output while aiming to be retain the readability of the original Markdown source. For example +# to achieve the following rendered output text +# +# **bold**, *italic*, ~~striketrough~~, `monospace` # # * Bullet # # > Quote # -# We write: +# [Link to search](https://duckduckgo.com/) # -# **bold**, *italic* +# We can use the following Markdown text # -# * Bullet +# ```Markdown +# **bold**, *italic*, ~~striketrough~~, `monospace` # -# > Quote +# * Bullet # -# See the Markdown documentation at [This Hyperlink](http://daringfireball.net/projects/markdown/) - -# %% [markdown] -# ### Typing code in the notebook - -# %% [markdown] -# When working with the notebook, you can either be in a cell, typing its contents, or outside cells, moving around the notebook. -# -# * When in a cell, press escape to leave it. When moving around outside cells, press return to enter. -# * Outside a cell: -# * Use arrow keys to move around. -# * Press `b` to add a new cell below the cursor. -# * Press `m` to turn a cell from code mode to markdown mode. -# * Press `shift`+`enter` to calculate the code in the block. -# * Press `h` to see a list of useful keys in the notebook. -# * Inside a cell: -# * Press `tab` to suggest completions of variables. (Try it!) - -# %% [markdown] -# *Supplementary material*: Learn more about [Jupyter notebooks](https://jupyter.org/). - -# %% [markdown] -# The `%%` at the beginning of a cell is called *magics*. There's a [large list of them available](https://ipython.readthedocs.io/en/stable/interactive/magics.html) and you can [create your own](http://ipython.readthedocs.io/en/stable/config/custommagics.html). +# > Quote # - -# %% [markdown] -# ### Python at the command line - -# %% [markdown] -# Data science experts tend to use a "command line environment" to work. You'll be able to learn this at our ["Software Carpentry" workshops](http://github-pages.arc.ucl.ac.uk/software-carpentry/), which cover other skills for computationally based research. - -# %% language="bash" -# # Above line tells Python to execute this cell as *shell code* -# # not Python, as if we were in a command line +# [Link to search](https://duckduckgo.com/) +# ``` # -# python -c "print(2 * 4)" - -# %% [markdown] -# ### Python scripts - -# %% [markdown] -# Once you get good at programming, you'll want to be able to write your own full programs in Python, which work just -# like any other program on your computer. Here are some examples: - -# %% language="bash" -# echo "print(2 * 4)" > eight.py -# python eight.py - -# %% [markdown] -# We can make the script directly executable (on Linux or Mac) by inserting a [hashbang](https://en.wikipedia.org/wiki/Shebang_(Unix%29)) and [setting the permissions](http://v4.software-carpentry.org/shell/perm.html) to execute. +# For more information +# [see this tutorial in the official Jupyter documentation](https://jupyter-notebook.readthedocs.io/en/stable/examples/Notebook/Working%20With%20Markdown%20Cells.html). +# +# #### Editing and running cells in the notebook +# +# When working with the notebook, you can either be editing the content of a cell (termed _edit mode_), +# or outside the cells, navigating around the notebook (termed _command mode_). +# +# * When in _edit mode_ in a cell, press esc to leave it and change to _command mode_. +# * When navigating between cells in _command mode_, press enter to change in to _edit mode_ in the selected cell. +# * When in _command mode_: +# * The currently selected cell will be shown by a
blue highlight
to the left of the cell. +# * Use the arrow keys and to navigate up and down between cells. +# * Press a to add a new cell above the currently selected cell. +# * Press b to add a new cell below the currently selected cell. +# * Press dd to delete the currently selected cell. +# * Press m to change a code cell to a Markdown cell. +# * Press y to change a Markdown cell to a code cell. +# * Press shift+l to toggle displaying line numbers on the currently selected cell. +# * Press shift+enter to run the code in a currently selected code cell and move to the next cell. +# * Press ctrl+enter to run the code in a currently selected code cell and keep the current cell selected. +# * Press ctrl+shift+c to access the command palette and search useful actions in the notebook. +# * When in _edit mode_: +# * Press tab to suggest completions of variable names and object attribute. (Try it!) +# * Press shift+tab when in the argument list of a function to display a pop-up showing documentation for the function. +# +# *Supplementary material*: Learn more about +# [Jupyter notebooks](https://jupyter-notebook.readthedocs.io/en/stable/notebook.html/). +# [Jupyter lab](https://jupyter.org/). +# +# ### Python interpreters +# +# An alternative to running Python code via a notebook interface is to run commands in a +# Python _interpreter_ (also known as an _interactive shell_ or _read-eval-print-loop (REPL)_). +# This is similar in concept to interacting with your operating system via a command-line interface +# such as the `bash` or `zsh` shells in Linux and MacOS or `Command Prompt` in Windows. A Python +# interpreter provides a _prompt_ into which we can type Python code corresponding to commands we +# wish to execute; we then execute this code by hitting enter with any output from the +# computation being displayed before returning to the prompt again. # -# Note, the `%%writefile` cell magic will write the contents of the cell to the file `fourteen.py`. - -# %% -# %%writefile fourteen.py -# #! /usr/bin/env python -print(2 * 7) - -# %% language="bash" -# chmod u+x fourteen.py -# ./fourteen.py - -# %% [markdown] -# ### Python Modules # %% [markdown] -# A Python module is a file that contains a set of related functions or other code. The filename must have a `.py` extension. +# ### Python libraries +# +# A very common requirement in research (and all other!) programming is needing to reuse code in +# multiple different files. While it may seem that copying-and-pasting is an adequate solution to this +# problem, this should generally be avoided wherever possible and code which we wish to reuse +# _factored out_ in to _libraries_ which we we can _import_ in to other files to access the functionality +# of this code. # -# We can write our own Python modules that we can import and use in other scripts or even in this notebook: +# Compared to copying and pasting code, writing and using libraries has the major advantage of meaning +# if we spot a bug in the code we only need to fix it once in the underlying library, and we straight away +# have the fixed code available everywhere the library is used rather than having to separately implement +# the fix in each file it is used. This similarly applies to for example adding new features to a piece of +# code. By creating libraries we also make it easier for other researchers to use our code. # +# While it is simple to use libraries within a notebook (and we have already seen examples of this when we +# imported the Python libraries NumPy and Matplotlib in the figure-eight plot example above), it is non-trivial +# to use code from one notebook in another without copying-and-pasting. To create Python libraries we therefore +# generally write the code in to text files with a `.py` extension which in Python terminology are called _modules_. +# The code can in these file can then be used in notebooks (or other modules) using the Python `import` statement. +# For example the cell below creates a file `draw_eight.py` in the same directory as this notebook containing Python +# code defining a _function_ (we will cover how to define and call functions later in the course) which creates a +# figure-eight plot and return the figure object. # %% -# %%writefile draw_eight.py -# Above line tells the notebook to treat the rest of this -# cell as content for a file on disk. -import math +# %%writefile draw_eight.py +# The above line tells the notebook to write the rest of the cell content to a file draw_eight.py import numpy as np import matplotlib.pyplot as plt def make_figure(): - """Plot a figure of eight.""" - - theta = np.arange(0, 4 * math.pi, 0.1) - eight = plt.figure() - axes = eight.add_axes([0, 0, 1, 1]) - axes.plot(0.5 * np.sin(theta), np.cos(theta / 2)) - - return eight - + theta = np.linspace(0, 2 * np.pi, 100) + fig, ax = plt.subplots(figsize=(3, 6)) + ax.plot(np.sin(2 * theta) / 2, np.cos(theta)) + return fig # %% [markdown] -# In a real example, we could edit the file on disk -# using a code editor such as [VS code](https://code.visualstudio.com/). - -# %% -import draw_eight # Load the library file we just wrote to disk +# Note, in a real example, we could edit the file on disk using a code editor (or IDE) rather than using `%%writefile` +# +# We can use this code in the notebook by _importing_ the `draw_eight` module and then _calling_ the +# `make_figure` function defined in the module. # %% -image = draw_eight.make_figure() +import draw_eight # Load the library +fig = draw_eight.make_figure() # %% [markdown] # Note, we can import our `draw_eight` module in this notebook only if the file is in our current working directory (i.e. the folder this notebook is in). # # To allow us to import our module from anywhere on our computer, or to allow other people to reuse it on their own computer, we can create a [Python package](https://packaging.python.org/en/latest/). +# We will cover how to import and use functionality from libraries, how to install third-party libraries +# and how to write your own libraries that can be shared and used by other in this course. # +# ### Python scripts +# +# While Jupyter notebooks are a great medium for learning how to use Python and for exploratory work, +# there are some drawbacks: +# +# * The require Jupyter Lab (or a similar application) to be installed to run the notebook. +# * It can be difficult to run notebooks non-interactively, for example when scheduling a job on a cluster [such as those offered by UCL Research Computing](https://www.rc.ucl.ac.uk/docs/Background/Cluster_Computing). +# * The flexibility of being able to run the code in cells in any order can also make it difficult to reason how outputs were produced and can lead to non-reproducible analyses. +# +# In some settings it can therefore be preferrable to write Python _scripts_ - that is files (typically with +# a `.py` extension) which contain Python code which completely describes a computational task to perform +# and that can be run by passing the name of the script file to the `python` program in a command-line +# environment. Optionally scripts may also allow passing in arguments from the command-line to control +# the execution of the script. As scripts are generally run from text-based terminals, non-text outputs such +# as images will generally be saved to files on disk. +# +# Python scripts are well suited to for example for describing computationally demanding simulations or analyses +# to run as long jobs on a remote server or cluster, or tasks where the input and output is mainly at the file level +# - for instance batch processing a series of data files. -# %% [markdown] -# ### Python packages +# ### Python libraries/packages # # A package is a collection of modules that can be installed on our computer and easily shared with others. We will learn how to create packages later on in this course. # # There is a huge variety of available packages to do pretty much anything. For instance, try `import antigravity` or `import this`. # +# ### IDEs +# +# IDEs are Interactive Development Environments and it's what we will be using in this course. +# We will be demonstrating it through [VS Code](https://code.visualstudio.com/) but you could use whichever you like, *e.g.*, [spyder](https://www.spyder-ide.org/), [pycharm](https://www.jetbrains.com/pycharm/), ...). +# We won't be using notebooks, except for these notes so you can download and experiment with them. +# However, we will be learning how to build libraries, and they need to be composed of python files rather than notebooks. +# When working with an IDE, you'll get access to a Python interpreter and you can run scripts directly from the interface as well as use tools like the debugger, test frameworks and git from within it. diff --git a/ch01python/015variables.ipynb.py b/ch01python/015variables.ipynb.py index 1a2513c0..8933da51 100644 --- a/ch01python/015variables.ipynb.py +++ b/ch01python/015variables.ipynb.py @@ -12,248 +12,214 @@ # --- # %% [markdown] -# ## Variables - -# %% [markdown] -# ### Variable Assignment +# # Variables +# +# ## Variable assignment +# +# Python has built-in support for arithmetic expressions and so can be used as a calculator. When we evaluate an expression in Python, the result is displayed, but not necessarily stored anywhere. -# %% [markdown] -# When we generate a result, the answer is displayed, but not kept anywhere. +# %% +2 + 2 # %% -2 * 3 +4 * 2.5 # %% [markdown] -# If we want to get back to that result, we have to store it. We put it in a box, with a name on the box. This is a **variable**. +# If we want to access the result in subsequent code, we have to store it. We put it in a box, with a name on the box. This is a _variable_. In Python we _assign_ a value to a variable using the _assignment operator_ `=` # %% -six = 2 * 3 - -# %% -print(six) +four = 2 + 2 +four # %% [markdown] -# If we look for a variable that hasn't ever been defined, we get an error. +# As well as numeric [literal values](https://en.wikipedia.org/wiki/Literal_(computer_programming)) Python also has built in support for representing textual data as sequences of characters, which in computer science terminology are termed [_strings_](https://en.wikipedia.org/wiki/String_(computer_science)). Strings in Python are indicated by enclosing their contents in either a pair of single quotation marks `'...'` or a pair of double quotation marks `"..."`, for example # %% -print(seven) +greeting = "hello world" # %% [markdown] -# That's **not** the same as an empty box, well labeled: +# ## Naming variables +# +# We can name variables with any combination of lower and uppercase characters, digits and underscores `_` providing the first character is not a digit and the name is not a [reserved keyword](https://docs.python.org/3/reference/lexical_analysis.html#keywords). # %% -nothing = None +fOuR = 4 # %% -print(nothing) +four_integer = 4 # %% -type(None) - -# %% [markdown] -# (None is the special python value for a no-value variable.) - -# %% [markdown] -# *Supplementary Materials*: There's more on variables at [Software Carpentry's Python lesson](https://swcarpentry.github.io/python-novice-inflammation/01-intro.html). - -# %% [markdown] -# Anywhere we could put a raw number, we can put a variable label, and that works fine: +integer_4 = 4 # %% -print(5 * six) +# invalid as name cannot begin with a digit +4_integer = 4 # %% -scary = six * six * six +# invalid as for is a reserved word +for = 4 + +# %% [markdown] +# It is good practice to give variables descriptive and meaningful names to help make code self-documenting. As most modern development environments (including Jupyter Lab!) offer _tab completion_ there is limited disadvantage from a keystroke perspective of using longer names. Note however that the names we give variables only have meaning to us: # %% -print(scary) +two_plus_two = 5 # %% [markdown] -# ### Reassignment and multiple labels +# ## _Aside:_ Reading error messages # %% [markdown] -# But here's the real scary thing: it seems like we can put something else in that box: - -# %% -scary = 25 +# We have already seen a couple of examples of Python error messages. It is important, when learning to program, to develop an ability to read an error message and find, from what can initially seem like confusing noise, the bit of the error message which tells you where the problem is. +# +# For example consider the following # %% -print(scary) - -# %% [markdown] -# Note that **the data that was there before has been lost**. +number_1 = 1 +number_2 = "2" +sum_of_numbers = number_1 + number_2 # %% [markdown] -# No labels refer to it any more - so it has been "Garbage Collected"! We might imagine something pulled out of the box, and thrown on the floor, to make way for the next occupant. +# We may not yet know what `TypeError` or `Traceback` refer to. However, we can see that the error happens on the _third_ line of our code cell. We can also see that the error message: +# +# > `unsupported operand type(s) for +: 'int' and 'str'` +# +# ...tells us something important. Even if we don't understand the rest, this is useful for debugging! # %% [markdown] -# In fact, though, it is the **label** that has moved. We can see this because we have more than one label refering to the same box: +# ## Undefined variables and `None` +# +# If we try to evaluate a variable that hasn't ever been defined, we get an error. # %% -name = "Eric" +seven -# %% -nom = name +# %% [markdown] +# In Python names are case-sensitive so for example `six`, `Six` and `SIX` are all different variable names # %% -print(nom) +six = 6 +six # %% -print(name) +Six # %% [markdown] -# And we can move just one of those labels: +# There is a special `None` keyword in Python which can be assigned to variables to indicate a variable with no-value. This is _not_ the same as an undefined variable, and it's more like to an empty box: # %% -nom = "Idle" +nothing # %% -print(name) +nothing = None +nothing # %% -print(nom) - -# %% [markdown] -# So we can now develop a better understanding of our labels and boxes: each box is a piece of space (an *address*) in computer memory. -# Each label (variable) is a reference to such a place. - -# %% [markdown] -# When the number of labels on a box ("variables referencing an address") gets down to zero, then the data in the box cannot be found any more. - -# %% [markdown] -# After a while, the language's "Garbage collector" will wander by, notice a box with no labels, and throw the data away, **making that box -# available for more data**. - -# %% [markdown] -# Old fashioned languages like C and Fortran don't have Garbage collectors. So a memory address with no references to it -# still takes up memory, and the computer can more easily run out. +print(nothing) # %% [markdown] -# So when I write: +# Anywhere we can use a literal value, we can instead use a variable name, for example # %% -name = "Michael" - -# %% [markdown] -# The following things happen: - -# %% [markdown] -# 1. A new text **object** is created, and an address in memory is found for it. -# 1. The variable "name" is moved to refer to that address. -# 1. The old address, containing "James", now has no labels. -# 1. The garbage collector frees the memory at the old address. - -# %% [markdown] -# **Supplementary materials**: There's an online python tutor which is great for visualising memory and references. Try the [scenario we just looked at](http://www.pythontutor.com/visualize.html#code=name%20%3D%20%22Eric%22%0Anom%20%3D%20name%0Aprint%28nom%29%0Aprint%28name%29%0Anom%20%3D%20%22Idle%22%0Aprint%28name%29%0Aprint%28nom%29%0Aname%20%3D%20%22Michael%22%0Aprint%28name%29%0Aprint%28nom%29%0A&cumulative=false&curInstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false). -# -# Labels are contained in groups called "frames": our frame contains two labels, 'nom' and 'name'. - -# %% [markdown] -# ### Objects and types - -# %% [markdown] -# An object, like `name`, has a type. In the online python tutor example, we see that the objects have type "str". -# `str` means a text object: Programmers call these 'strings'. +5 + four * six # %% -type(name) +scary = six * six * six +scary # %% [markdown] -# Depending on its type, an object can have different *properties*: data fields Inside the object. +# *Supplementary Materials*: There's more on variables at +# [Software Carpentry](https://swcarpentry.github.io/python-novice-inflammation/01-intro/index.html). # %% [markdown] -# Consider a Python complex number for example: - -# %% -z = 3 + 1j +# ## Reassignment and multiple labels # %% [markdown] -# We can see what properties and methods an object has available using the `dir` function: +# We can reassign a variable - that is change what is in the box the variable labels. # %% -dir(z) +scary = 25 +scary # %% [markdown] -# You can see that there are several methods whose name starts and ends with `__` (e.g. `__init__`): these are special methods that Python uses internally, and we will discuss some of them later on in this course. The others (in this case, `conjugate`, `img` and `real`) are the methods and fields through which we can interact with this object. - -# %% -type(z) - -# %% -z.real +# The data that was previously labelled by the variable is lost. No labels refer to it any more - so it has been [_garbage collected_](https://en.wikipedia.org/wiki/Garbage_collection_(computer_science)). We might imagine something pulled out of the box, and disposed of, to make way for the next occupant. In reality, though, it is the _label_ that has moved. +# +# We can see this more clearly if we have more than one label referring to the same box # %% -z.imag - -# %% [markdown] -# A property of an object is accessed with a dot. - -# %% [markdown] -# The jargon is that the "dot operator" is used to obtain a property of an object. +name = "Grace Hopper" +nom = name +print(name) +print(nom) # %% [markdown] -# When we try to access a property that doesn't exist, we get an error: +# and we move just one of those labels: # %% -z.wrong - -# %% [markdown] -# ### Reading error messages. - -# %% [markdown] -# It's important, when learning to program, to develop an ability to read an error message and find, from in amongst -# all the confusing noise, the bit of the error message which tells you what to change! +nom = "Grace Brewster Murray Hopper" +print(name) +print(nom) # %% [markdown] -# We don't yet know what is meant by `AttributeError`, or "Traceback". +# ## Variables and memory +# +# We can now better understand our mental model of variables as labels and boxes: each box is a piece of space (an *address*) in computer memory. Each label (_variable_) is a reference to such a place and the data contained in the memory defines an _object_ in Python. Python objects come in different types - so far we have encountered both numeric (integer) and textual (string) types - more on this later. +# +# When the number of labels on a box (_variables referencing an address_) gets down to zero, then the data in the box cannot be accessed any more. This will trigger Python's garbage collector, which will then 'empty' the box (_deallocated the memory at the address_), making it available again to store new data. +# +# Lower-level languages such as C and Fortran do not have garbage collectors as a standard feature. So a memory address with no references to it and which has not been specifically marked as free remains unavailable for other usage, which can lead to difficult to fix [_memory leak_](https://en.wikipedia.org/wiki/Memory_leak) bugs. +# +# When we execute # %% -z2 = 5 - 6j -print("Gets to here") -print(z.wrong) -print("Didn't get to here") - -# %% [markdown] -# But in the above, we can see that the error happens on the **third** line of our code cell. +name = "Grace Hopper" +nom = name +nom = "Grace Brewster Murray Hopper" +name = "Admiral Hopper" # %% [markdown] -# We can also see that the error message: -# > 'complex' object has no attribute 'wrong' +# the following happens # -# ...tells us something important. Even if we don't understand the rest, this is useful for debugging! +# 1. A new text (_string_) object `"Grace Hopper"` is created at a free address in memory and the variable `name` is set to refer to that address +# 2. The variable `nom` is set to refer to the object at the address referenced by `name` +# 3. A new text (_string_) object `"Grace Brewster Murray Hopper"` is created at a free address in memory and the variable `nom` is set to refer to that address +# 4. A new text (_string_) object `"Admiral Hopper"` is created at a free address in memory, the variable `name` is set to refer to that address and the garbage collector deallocates the memory used to hold `"Grace Hopper"` as this memory is no longer referenced by any variables. # %% [markdown] -# ### Variables and the notebook kernel +# _Supplementary materials_: The website [Python Tutor](https://pythontutor.com/) has a great interactive tool for visualizing how memory and references work in Python which is great for visualising memory and references. +# Try the [scenario we just looked at](https://pythontutor.com/visualize.html#code=name%20%3D%20%22Grace%20Hopper%22%0Anom%20%3D%20name%0Anom%20%3D%20%22Grace%20Brewster%20Murray%20Hopper%22%0Aname%20%3D%20%22Admiral%20Hopper%22&cumulative=false&curInstr=4&heapPrimitives=true&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false). # %% [markdown] -# When I type code in the notebook, the objects live in memory between cells. +# ## Variables in notebooks and kernels -# %% -number = 0 +# %% [markdown] +# When code cells are executed in a notebook, the variable names and values of the referenced objects persist between cells # %% -print(number) +number = 1 # %% [markdown] -# If I change a variable: +# There if we change a variable in one cell # %% number = number + 1 -# %% -print(number) - # %% [markdown] # It keeps its new value for the next cell. -# %% [markdown] -# But cells are **not** always evaluated in order. - -# %% [markdown] -# If I now go back to Input 31, reading `number = number + 1`, I can run it again, with Shift-Enter. The value of `number` will change from 2 to 3, then from 3 to 4 - but the output of the next cell (containing the `print` statement) will not change unless I rerun that too. Try it! - -# %% [markdown] -# So it's important to remember that if you move your cursor around in the notebook, it doesn't always run top to bottom. +# %% +number # %% [markdown] -# **Supplementary material**: (1) [Jupyter notebook documentation](https://jupyter-notebook.readthedocs.io/en/latest/). +# In Jupyter terminology the Python process in which we run the code in a notebook in is termed a _kernel_. The _kernel_ stores the variable names and referenced objects created by any code cells in the notebook that have been previously run. The `Kernel` menu in the menu bar at the top of the JupyterLab interface has an option `Restart kernel...`. Running this will restart the kernel currently being used by the notebook, clearing any currently defined variables and returning the kernel to a 'clean' state. As you cannot restore a kernel once restarted a warning message is displayed asking you to confirm you wish to restart. +# +# ## Cell run order +# +# Cells do **not** have to be evaluated in the order they appear in a notebook. +# +# If we go back to the code cell above with contents `number = number + 1`, and run it again, with shift+enter then `number` will change from 2 to 3, then from 3 to 4. Try it! +# +# However, running cells out of order like this can make it hard to keep track of what values are currently assigned to variables. It also makes it difficult to reproduce computations as getting the same output requires rerunning the cells in the same order, and it will not always be possible to reconstruct what the order used was. +# +# The number in square brackets in the prompt to the left of code cells, for example `[1]:` indicates the position in the overall cell run order of the last run of the cell. While this allows establishing if a cell last ran before or after another cell, if some cells are run multiple times then their previous run counter values will be overwritten so we lose information about the run order. +# +# In general if you are using notebooks in your own research you should try to make sure the notebook run and produce the desired outputs when the cells are executed sequentially from top to bottom. The `Kernel` menu provides an option to restart the current kernel and run all cells in order from top to bottom. If you just want to run a subset of the cells there is also an option to restart and run all cells from the top to the currently selected cell. The commands are useful for checking that a notebook will produce the expected output and run without errors when the cells are executed in order. diff --git a/ch01python/04functions.ipynb.py b/ch01python/016functions.ipynb.py similarity index 51% rename from ch01python/04functions.ipynb.py rename to ch01python/016functions.ipynb.py index a1659fea..673b79c7 100644 --- a/ch01python/04functions.ipynb.py +++ b/ch01python/016functions.ipynb.py @@ -12,10 +12,10 @@ # --- # %% [markdown] -# ## Functions +# # Functions # %% [markdown] -# ### Definition +# ## Definition # %% [markdown] # @@ -28,17 +28,25 @@ def double(x): return x * 2 -print(double(5), double([5]), double('five')) + +# %% +double(5) + +# %% +double([5]) + +# %% +double("five") # %% [markdown] -# ### Default Parameters +# ## Default Parameters # %% [markdown] # We can specify default values for parameters: # %% -def jeeves(name = "Sir"): +def jeeves(name="Sir"): return f"Very good, {name}" @@ -46,13 +54,12 @@ def jeeves(name = "Sir"): jeeves() # %% -jeeves('John') +jeeves("James") # %% [markdown] # If you have some parameters with defaults, and some without, those with defaults **must** go later. - -# %% [markdown] +# # If you have multiple default arguments, you can specify neither, one or both: # %% @@ -77,51 +84,50 @@ def jeeves(greeting="Very good", name="Sir"): # %% [markdown] -# ### Side effects +# ## Side effects # %% [markdown] +# # Functions can do things to change their **mutable** arguments, # so `return` is optional. # # This is pretty awful style, in general, functions should normally be side-effect free. # -# Here is a contrived example of a function that makes plausible use of a side-effect +# Here is a contrived example of a function that makes plausible use of a side-effect. +# +# Note that the function below uses `[:]`. This is used to update the contents of +# the list, and though this function is not returning anything, it's changing the +# elements of the list. # %% def double_inplace(vec): vec[:] = [element * 2 for element in vec] -z = list(range(4)) + +z = [0, 1, 2, 3] # This could be simplified using list(range(4)) double_inplace(z) print(z) -# %% -letters = ['a', 'b', 'c', 'd', 'e', 'f', 'g'] -letters[:] = [] - - # %% [markdown] -# In this example, we're using `[:]` to access into the same list, and write it's data. +# In this example, we're using `[:]` to access into the same list, and write its data. Whereas, if we do # -# vec = [element*2 for element in vec] +# vec = [element * 2 for element in vec] # -# would just move a local label, not change the input. - -# %% [markdown] -# But I'd usually just write this as a function which **returned** the output: +# would just move a local label, not change the input - *i.e.*, a new container is created and the label `vec` is moved from the old one to the new one. +# +# A more common case would be to this as a function which **returned** the output: -# %% + # %% def double(vec): return [element * 2 for element in vec] - # %% [markdown] -# Let's remind ourselves of the behaviour for modifying lists in-place using `[:]` with a simple array: +# Let's remind ourselves of this behaviour with a simple array: # %% x = 5 x = 7 -x = ['a', 'b', 'c'] +x = ["a", "b", "c"] y = x # %% @@ -135,44 +141,88 @@ def double(vec): # %% [markdown] -# ### Early Return - -# %% [markdown] -# -# Return without arguments can be used to exit early from a function +# ## Early Return # +# Having multiple `return` statements is a common practice in programming. +# These `return` statements can be placed far from each other, allowing a +# function to return early if a specific condition is met. # +# For example, a function `isbigger` could be written as: +# ``` +# def isbigger(x, limit=20): +# return x > limit +# ``` +# However, what if you want to print a message on the screen when a smaller +# value has been found? That's what we do below, where the function below +# returns early if a number greater than given limit. + +# %% +def isbigger(x, limit=20): + if x > limit: + return True + print("Value is smaller") + return False + + +isbigger(25, 15) + +# %% +isbigger(40, 15) + + +# %% [markdown] # +# The dynamic typing of Python also makes it easy to return different types +# of values based on different conditions, but such code is not considered +# a good practice. It is also a good practice to have a default return value +# in the function if it is returning something in the first place. For instance, +# the function below could use an `elif` or an `else` condition for the second +# `return` statement, but that would not be a good practice. In those cases, +# Python would be using the implicit `return` statement. For example, what's +# returned in the following example when the argument `x` is equal to the `limit`? + +# %% +def isbigger(x, limit=20): + if x > limit: + return True + elif x < limit: + print("Value is smaller") + return False + +# %% +# Write your own code to find out # %% [markdown] +# +# Return without arguments can be used to exit early from a function +# # Here's a slightly more plausibly useful function-with-side-effects to extend a list with a specified padding datum. # %% def extend(to, vec, pad): if len(vec) >= to: - return # Exit early, list is already long enough. - + return # Exit early, list is already long enough. vec[:] = vec + [pad] * (to - len(vec)) # %% -x = list(range(3)) -extend(6, x, 'a') +x = [1, 2, 3] +extend(6, x, "a") print(x) # %% -z = range(9) -extend(6, z, 'a') +z = list(range(9)) +extend(6, z, "a") print(z) # %% [markdown] -# ### Unpacking arguments +# ## Unpacking arguments # %% [markdown] # -# If a vector is supplied to a function with a '*', its elements -# are used to fill each of a function's arguments. +# If a vector is supplied to a function with a `*`, its elements +# are used to fill each of a function's arguments. # # # @@ -181,11 +231,13 @@ def extend(to, vec, pad): def arrow(before, after): return f"{before} -> {after}" -arrow(1, 3) + +print(arrow(1, 3)) # %% x = [1, -1] -arrow(*x) + +print(arrow(*x)) # %% [markdown] # @@ -198,17 +250,17 @@ def arrow(before, after): # %% charges = {"neutron": 0, "proton": 1, "electron": -1} + +# %% +charges.items() + +# %% for particle in charges.items(): print(arrow(*particle)) # %% [markdown] -# -# -# - -# %% [markdown] -# ### Sequence Arguments +# ## Sequence Arguments # %% [markdown] # Similiarly, if a `*` is used in the **definition** of a function, multiple @@ -219,24 +271,25 @@ def doubler(*sequence): return [x * 2 for x in sequence] -# %% -doubler(1, 2, 3) - -# %% -doubler(5, 2, "Wow!") +print(doubler(1, 2, 3, "four")) # %% [markdown] -# ### Keyword Arguments +# ## Keyword Arguments # %% [markdown] -# If two asterisks are used, named arguments are supplied inside the function as a dictionary: +# +# If two asterisks are used, named arguments are supplied as a dictionary: +# +# +# # %% def arrowify(**args): for key, value in args.items(): print(f"{key} -> {value}") + arrowify(neutron="n", proton="p", electron="e") @@ -250,6 +303,4 @@ def somefunc(a, b, *args, **kwargs): print("args:", args) print("keyword args", kwargs) - -# %% somefunc(1, 2, 3, 4, 5, fish="Haddock") diff --git a/ch01python/016using_functions.ipynb.py b/ch01python/016using_functions.ipynb.py deleted file mode 100644 index dd176f96..00000000 --- a/ch01python/016using_functions.ipynb.py +++ /dev/null @@ -1,241 +0,0 @@ -# --- -# jupyter: -# jekyll: -# display_name: Using Functions -# jupytext: -# notebook_metadata_filter: -kernelspec,jupytext,jekyll -# text_representation: -# extension: .py -# format_name: percent -# format_version: '1.3' -# jupytext_version: 1.15.2 -# --- - -# %% [markdown] -# ## Using Functions - -# %% [markdown] -# ### Calling functions - -# %% [markdown] -# We often want to do things to our objects that are more complicated than just assigning them to variables. - -# %% -len("pneumonoultramicroscopicsilicovolcanoconiosis") - -# %% [markdown] -# Here we have "called a function". - -# %% [markdown] -# The function `len` takes one input, and has one output. The output is the length of whatever the input was. - -# %% [markdown] -# Programmers also call function inputs "parameters" or, confusingly, "arguments". - -# %% [markdown] -# Here's another example: - -# %% -sorted("Python") - -# %% [markdown] -# Which gives us back a *list* of the letters in Python, sorted alphabetically (more specifically, according to their [Unicode order](https://www.ssec.wisc.edu/~tomw/java/unicode.html#x0000)). - -# %% [markdown] -# The input goes in brackets after the function name, and the output emerges wherever the function is used. - -# %% [markdown] -# So we can put a function call anywhere we could put a "literal" object or a variable. - -# %% -len('Jim') * 8 - -# %% -x = len('Mike') -y = len('Bob') -z = x + y - -# %% -print(z) - -# %% [markdown] -# ### Using methods - -# %% [markdown] -# Objects come associated with a bunch of functions designed for working on objects of that type. We access these with a dot, just as we do for data attributes: - -# %% -"shout".upper() - -# %% [markdown] -# These are called methods. If you try to use a method defined for a different type, you get an error: - -# %% -x = 5 - -# %% -type(x) - -# %% -x.upper() - -# %% [markdown] -# If you try to use a method that doesn't exist, you get an error: - -# %% -x.wrong - -# %% [markdown] -# Methods and properties are both kinds of **attribute**, so both are accessed with the dot operator. - -# %% [markdown] -# Objects can have both properties and methods: - -# %% -z = 1 + 5j - -# %% -z.real - -# %% -z.conjugate() - -# %% -z.conjugate - -# %% [markdown] -# ### Functions are just a type of object! - -# %% [markdown] -# Now for something that will take a while to understand: don't worry if you don't get this yet, we'll -# look again at this in much more depth later in the course. -# -# If we forget the (), we realise that a *method is just a property which is a function*! - -# %% -z.conjugate - -# %% -type(z.conjugate) - -# %% -somefunc = z.conjugate - -# %% -somefunc() - -# %% [markdown] -# Functions are just a kind of variable, and we can assign new labels to them: - -# %% -sorted([1, 5, 3, 4]) - -# %% -magic = sorted - -# %% -type(magic) - -# %% -magic(["Technology", "Advanced"]) - -# %% [markdown] -# ### Getting help on functions and methods - -# %% [markdown] -# The 'help' function, when applied to a function, gives help on it! - -# %% -help(sorted) - -# %% [markdown] -# The 'dir' function, when applied to an object, lists all its attributes (properties and methods): - -# %% -dir("Hexxo") - -# %% [markdown] -# Most of these are confusing methods beginning and ending with __, part of the internals of python. - -# %% [markdown] -# Again, just as with error messages, we have to learn to read past the bits that are confusing, to the bit we want: - -# %% -"Hexxo".replace("x", "l") - -# %% -help("FIsh".replace) - -# %% [markdown] -# ### Operators - -# %% [markdown] -# Now that we know that functions are a way of taking a number of inputs and producing an output, we should look again at -# what happens when we write: - -# %% -x = 2 + 3 - -# %% -print(x) - -# %% [markdown] -# This is just a pretty way of calling an "add" function. Things would be more symmetrical if add were actually written -# -# x = +(2, 3) -# -# Where '+' is just the name of the name of the adding function. - -# %% [markdown] -# In python, these functions **do** exist, but they're actually **methods** of the first input: they're the mysterious `__` functions we saw earlier (Two underscores.) - -# %% -x.__add__(7) - -# %% [markdown] -# We call these symbols, `+`, `-` etc, "operators". - -# %% [markdown] -# The meaning of an operator varies for different types: - -# %% -"Hello" + "Goodbye" - -# %% -[2, 3, 4] + [5, 6] - -# %% [markdown] -# Sometimes we get an error when a type doesn't have an operator: - -# %% -7 - 2 - -# %% -[2, 3, 4] - [5, 6] - -# %% [markdown] -# The word "operand" means "thing that an operator operates on"! - -# %% [markdown] -# Or when two types can't work together with an operator: - -# %% -[2, 3, 4] + 5 - -# %% [markdown] -# To do this, put: - -# %% -[2, 3, 4] + [5] - -# %% [markdown] -# Just as in Mathematics, operators have a built-in precedence, with brackets used to force an order of operations: - -# %% -print(2 + 3 * 4) - -# %% -print((2 + 3) * 4) - -# %% [markdown] -# *Supplementary material*: [Python operator precedence](https://docs.python.org/3/reference/expressions.html#operator-precedence). diff --git a/ch01python/017using_functions.ipynb.py b/ch01python/017using_functions.ipynb.py new file mode 100644 index 00000000..1728aaac --- /dev/null +++ b/ch01python/017using_functions.ipynb.py @@ -0,0 +1,133 @@ +# --- +# jupyter: +# jekyll: +# display_name: Using Functions +# jupytext: +# notebook_metadata_filter: -kernelspec,jupytext,jekyll +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.2 +# --- + +# %% [markdown] +# # Using functions +# +# Functions in Python (and other programming languages) are modular units of code which specify a set of instructions to perform when the function is _called_ in code. Functions may take one or more input values as _arguments_ and may _return_ an output value. Functions can also have _side-effects_ such as printing information to a display. +# +# ## Calling functions in Python +# +# Python provides a range of useful [built-in functions](https://docs.python.org/3/library/functions.html) for performing common tasks. For example the `len` function returns the length of a sequence object (such as a string) passed as input argument. To _call_ a function in Python we write the name of the function followed by a pair of parentheses `()`, with any arguments to the function being put inside the parentheses: + +# %% +len("pneumonoultramicroscopicsilicovolcanoconiosis") + +# %% [markdown] +# If a function can accept more than one argument they are separated by commas. For example the built-in `max` function when passed a pair of numeric arguments returns the larger value from the pair: + +# %% +max(1, 5) + +# %% [markdown] +# Another built-in function which we have already seen several times is `print`. Unlike `len` and `max`, `print` does not have an explicit return value as its purpose is to print a string representation of the argument(s) to a text display such as the output area of a notebook code cell. For functions like `print` which do not have an explicit return value, the special null value `None` we encountered previously will be used as the value of a call to the function if used in an expression or assigned to a variable: + +# %% +return_value = print("Hello") +print(return_value) + +# %% [markdown] +# Function calls can be placed anywhere we can use a literal value or a variable name, for example + +# %% +name = "Jim" +len(name) * 8 + +# %% +total_length = len("Mike") + len("Bob") +print(total_length) + +# %% [markdown] +# ## Getting help on functions +# +# The built-in `help` function, when passed a function, prints documentation for the function, which typically includes a description of the what arguments can be passed and what the function returns. For example + +# %% +help(max) + +# %% [markdown] +# In Jupyter notebooks and ipython consoles an alternative way of displaying the documentation for a function is to write the function names followed by a question mark character `?` + +# %% +# max? + +# %% [markdown] +# ## Positional and keyword arguments and default values +# +# There are two ways of passing arguments to function in Python. In the examples so far the function arguments have only been identified by the position they appear in the argument list of the function. An alternative is to use named or _keyword_ arguments, by prefixing some or all of the arguments with the argument name followed by an equals sign. For example, there is a built-in function `round` which rounds decimal numbers to a specified precision. Using the `help` function we can read the documentation for `round` to check what arguments it accepts +# + +# %% +help(round) + +# %% [markdown] +# We see that `round` accepts two arguments, a `number` argument which specifies the number to round and a `ndigits` argument which specifies the number of decimal digits to round to. One way to call `round` is by passing positional arguments in the order specified in function signature `round(number, ndigits=None)` with the first argument corresponding to `number` and the second `ndigits`. For example + +# %% +pi = 3.14159265359 +round(pi, 2) + +# %% [markdown] +# To be more expicit about which parameters of the function the arguments we are passing correspond to, we can instead however pass the arguments by name (as _keyword arguments_) + +# %% +round(number=pi, ndigits=2) + +# %% [markdown] +# We can in-fact mix and match position and keyword arguments, _providing that all keyword arguments come after any positional arguments_ + +# %% +round(pi, ndigits=2) + +# %% +round(number=pi, 2) + +# %% [markdown] +# Unlike positional arguments the ordering of keyword arguments does not matter so for example the following is also valid and equivalent to the calls above + +# %% +round(ndigits=2, number=pi) + +# %% [markdown] +# In the documentation for `round` we see that the second argument in the function signature is written `ndigits=None`. This indicates that `ndigits` is an _optional_ argument which takes the default value `None` if it is not specified. The documentation further states that +# +# > The return value is an integer if `ndigits` is omitted or `None` +# +# which indicates that when `ndigits` is left as its default value (that is the argument is omitted or explicitly set to `None`) the `round` function returns the value of `number` rounded to the nearest integer. The following are all equivalent therefore + +# %% +round(pi) + +# %% +round(number=pi) + +# %% +round(number=pi, ndigits=None) + +# %% [markdown] +# ## Functions are objects +# +# A powerful feature of Python (and one that can take a little while to wrap your head around) is that functions are just a particular type of object and so can be for example assigned to variables or passed as arguments to other functions. We have in fact already seen examples of this when using the `help` function, with a function passed as the (only) argument to `help`. We can also assign functions to variables + +# %% +my_print = print +my_print + +# %% +help(my_print) + +# %% +my_print("Hello") + +# %% [markdown] +# While giving function aliases like this may not seem particularly useful at the moment, we will see that the ability to pass functions to other functions and assign functions to variables can be very useful in certain contexts. diff --git a/ch01python/023types.ipynb.py b/ch01python/023types.ipynb.py index 0efc6f33..acf05610 100644 --- a/ch01python/023types.ipynb.py +++ b/ch01python/023types.ipynb.py @@ -12,332 +12,240 @@ # --- # %% [markdown] -# ## Types +# # Types # %% [markdown] -# We have seen that Python objects have a 'type': +# We have so far encountered several different 'types' of Python object: +# +# - integer numbers, for example `42`, +# - real numbers, for example `3.14`, +# - strings, for example `"abc"`, +# - functions, for example `print`, +# - the special 'null'-value `None`. +# +# The built-in function `type` when passed a single argument will return the type of the argument object. For example # %% -type(5) - -# %% [markdown] -# ### Floats and integers - -# %% [markdown] -# Python has two core numeric types, `int` for integer, and `float` for real number. +type(42) # %% -one = 1 -ten = 10 -one_float = 1.0 -ten_float = 10. +type("abc") # %% [markdown] -# The zero after a decimal point is optional - it is the **Dot** makes it a float. However, it is better to always include the zero to improve readability. - -# %% -tenth= one_float / ten_float - -# %% -tenth +# ## Converting between types +# +# The Python type names such as `int` (integer numbers) and `str` (strings) can be used like functions to construct _instances_ of the type, typically by passing an object of another type which can be converted to the type being called. For example we can use `int` to convert a string of digits to an integer # %% -type(one) +int("123") # %% -type(one_float) +int("2") + int("2") # %% [markdown] -# The meaning of an operator varies depending on the type it is applied to! - -# %% -print(1 + 2) # returns an integer +# or to convert a decimal number to an integer (in this case by truncating off the decimal part) # %% -print(1.0 + 2.0) # returns a float +int(2.1) # %% [markdown] -# The division by operator always returns a `float`, whether it's applied to `float`s or `int`s. +# Conversely we can use `str` to convert numbers to strings # %% -10 / 3 +str(123) # %% -10.0 / 3 - -# %% -10 / 3.0 +str(3.14) # %% [markdown] -# To perform integer division we need to use the `divmod` function, which returns the quotiant and remainder of the division. +# ## Object attributes and methods +# +# Objects in Python have _attributes_: named values associated with the object and which are referenced to using the dot operator `.` followed by the attribute name, for example `an_object.attribute` would access (if it exists) the attribute named `attribute` of the object `an_object`. Each type has a set of pre-defined attributes. +# +# Object attributes can reference arbitrary data associated with the object, or special functions termed _methods_ which have access to both any argument passed to them _and_ any other attributes of the object, and which are typically used to implement functionality connected to a particular object type. +# +# For example the `float` type used by Python to represent non-integer numbers (more on this in a bit) has attribute `real` and `imaginary` which can be used to access the real and imaginary components when considering the value as a [complex number](https://en.wikipedia.org/wiki/Complex_number) # %% -quotiant, remainder = divmod(10, 3) -print(f"{quotiant=}, {remainder=}") +a_number = 12.5 +print(a_number.real) +print(a_number.imag) # %% [markdown] -# Note that if either of the input type are `float`, the returned values will also be `float`s. +# Objects of `str` type (strings) have methods `upper` and `lower` which both take no arguments, and respectively return the string in all upper case or all lower case characters # %% -divmod(10, 3.0) +a_string = "Hello world!" +print(a_string.upper()) +print(a_string.lower()) # %% [markdown] -# There is a function for every built-in type, which is used to convert the input to an output of the desired type. +# If you try to access an attribute not defined for a particular type you will get an error # %% -x = float(5) -type(x) +a_string.real # %% -divmod(10, float(3)) +a_number.upper() # %% [markdown] -# I lied when I said that the `float` type was a real number. It's actually a computer representation of a real number -# called a "floating point number". Representing $\sqrt 2$ or $\frac{1}{3}$ perfectly would be impossible in a computer, so we use a finite amount of memory to do it. +# We can list all of the attributes of an object by passing the object to the built-in `dir` function, for example # %% -N = 10000.0 -sum([1 / N] * int(N)) +print(dir(a_string)) # %% [markdown] -# *Supplementary material*: +# The attributes with names beginning and ending in double underscores `__` are special methods that implement the functionality associated with applying operators (and certain built-in functions) to objects of that type, and are generally not accessed directly. # -# * [Python's documentation about floating point arithmetic](https://docs.python.org/tutorial/floatingpoint.html); -# * [How floating point numbers work](http://floating-point-gui.de/formats/fp/); -# * Advanced: [What Every Computer Scientist Should Know About Floating-Point Arithmetic](http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html). +# In Jupyter notebooks we can also view an objects properties using tab-completion by typing the object name followed by a dot `.` then hitting tab # %% [markdown] -# ### Strings - -# %% [markdown] -# Python has a built in `string` type, supporting many -# useful methods. +# ## Operators +# +# Now that we know more about types, functions and methods we should look again at what happens when we write: # %% -given = "Terry" -family = "Jones" -full = given + " " + family +four = 2 + 2 # %% [markdown] -# So `+` for strings means "join them together" - *concatenate*. +# The addition symbol `+` here is an example of what is termed an _operator_, in particular `+` is a _binary operator_ as it applies to pairs of values. +# +# We can think of the above code as equivalent to something like +# +# ```Python +# four = add(2, 2) +# ``` +# +# where `add` is the name of a function which takes two arguments and returns their sum. +# +# In Python, these functions _do_ exist, but they're actually _methods_ of the first input: they're the mysterious double-underscore `__` surrounded attributes we saw previously. For example the addition operator `+` is associated with a special method `__add__` # %% -print(full.upper()) +two = 2 +two.__add__(two) # %% [markdown] -# As for `float` and `int`, the name of a type can be used as a function to convert between types: +# The meaning of an operator varies for different types. For example for strings the addition operator `+` implements string concatenation (joining). # %% -ten, one +"Hello" + " " + "world" # %% -print(ten + one) - -# %% -print(float(str(ten) + str(one))) +"2" + "2" # %% [markdown] -# We can remove extraneous material from the start and end of a string: +# Sometimes we get an error when a type doesn't have an operator: # %% -" Hello ".strip() +"Hello" - "world" # %% [markdown] -# Note that you can write strings in Python using either single (`' ... '`) or double (`" ... "`) quote marks. The two ways are equivalent. However, if your string includes a single quote (e.g. an apostrophe), you should use double quotes to surround it: - -# %% -"Terry's animation" +# The word "operand" means "thing that an operator operates on"! # %% [markdown] -# And vice versa: if your string has a double quote inside it, you should wrap the whole string in single quotes. +# Or when two types can't work together with an operator: # %% -'"Wow!", said John.' - -# %% [markdown] -# ### Lists - -# %% [markdown] -# Python's basic **container** type is the `list`. +"2" + 5 # %% [markdown] -# We can define our own list with square brackets: +# Just as in mathematics, operators in Python have a built-in precedence, with brackets used to force an order of operations: # %% -[1, 3, 7] +print(2 + 3 * 4) # %% -type([1, 3, 7]) +print((2 + 3) * 4) # %% [markdown] -# Lists *do not* have to contain just one type: - -# %% -various_things = [1, 2, "banana", 3.4, [1,2] ] - -# %% [markdown] -# We access an **element** of a list with an `int` in square brackets: - -# %% -various_things[2] - -# %% -index = 0 -various_things[index] +# *Supplementary material*: [Python operator precedence](https://docs.python.org/3/reference/expressions.html#operator-precedence) # %% [markdown] -# Note that list indices start from zero. +# ## Floats and integers # %% [markdown] -# We can use a string to join together a list of strings: +# Python has two core numeric types, `int` for integers, and `float` for real numbers. # %% -name = ["Sir", "Michael", "Edward", "Palin"] -print("==".join(name)) +integer_one = 1 +integer_ten = 10 +float_one = 1.0 +float_ten = 10. # %% [markdown] -# And we can split up a string into a list: +# Binary arithmetic operators applied to objects of `float` and `int` types will return a `float` # %% -"Ernst Stavro Blofeld".split(" ") +integer_one * float_ten # %% -"Ernst Stavro Blofeld".split("o") +float_one + integer_one # %% [markdown] -# And combine these: +# In Python there are two division operators `/` and `//` which implement different mathematical operations. The _true division_ (or just _division_) operator `/` implements what we usually think of by division, such that for two `float` values `x` and `y` `z = x / y` is another `float` values such that `y * z` is (to within machine precision) equal to `x`. The _floor division_ operator `//` instead implements the operation of dividing and rounding down to the nearest integer. # %% -"->".join("John Ronald Reuel Tolkein".split(" ")) - -# %% [markdown] -# A matrix can be represented by **nesting** lists -- putting lists inside other lists. +float_one / float_ten # %% -identity = [[1, 0], [0, 1]] +float_one / integer_ten # %% -identity[0][0] - -# %% [markdown] -# ... but later we will learn about a better way of representing matrices. - -# %% [markdown] -# ### Ranges - -# %% [markdown] -# Another useful type is range, which gives you a sequence of consecutive numbers. In contrast to a list, ranges generate the numbers as you need them, rather than all at once. -# -# If you try to print a range, you'll see something that looks a little strange: +float_one // float_ten # %% -range(5) - -# %% [markdown] -# We don't see the contents, because *they haven't been generatead yet*. Instead, Python gives us a description of the object - in this case, its type (range) and its lower and upper limits. - -# %% [markdown] -# We can quickly make a list with numbers counted up by converting this range: +integer_one // integer_ten # %% -count_to_five = range(5) -print(list(count_to_five)) - -# %% [markdown] -# Ranges in Python can be customised in other ways, such as by specifying the lower limit or the step (that is, the difference between successive elements). You can find more information about them in the [official Python documentation](https://docs.python.org/3/library/stdtypes.html#ranges). - -# %% [markdown] -# ### Sequences +integer_one // float_ten # %% [markdown] -# Many other things can be treated like `lists`. Python calls things that can be treated like lists `sequences`. +# In reality the `float` type does not exactly represent real numbers (as being able to represent all real numbers to arbitrary precision is impossible in a object with uses a finite amount of memory) but instead represents real-numbers as a finite-precision 'floating-point' approximation. This has many important implications for the implementation of numerical algorithms. We will not have time to cover this topic here but the following resources can be used to learn more for those who are interested. +# +# *Supplementary material*: +# +# * [Floating point arithmetic in Python](https://docs.python.org/3/tutorial/floatingpoint.html) +# * [Floating point guide](http://floating-point-gui.de/formats/fp/) +# * Advanced: [What Every Computer Scientist Should Know About Floating-Point Arithmetic](http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html) # %% [markdown] -# A string is one such *sequence type*. +# ## Strings # %% [markdown] -# Sequences support various useful operations, including: -# - Accessing a single element at a particular index: `sequence[index]` -# - Accessing multiple elements (a *slice*): `sequence[start:end_plus_one]` -# - Getting the length of a sequence: `len(sequence)` -# - Checking whether the sequence contains an element: `element in sequence` -# -# The following examples illustrate these operations with lists, strings and ranges. - -# %% -print(count_to_five[1]) - -# %% -print("Palin"[2]) - -# %% -count_to_five = range(5) - -# %% -count_to_five[1:3] - -# %% -"Hello World"[4:8] - -# %% -len(various_things) - -# %% -len("Python") - -# %% -name +# Python built-in `string` type, supports many useful operators and methods. As we have seen already the addition operator can be used to concatenate strings # %% -"Edward" in name - -# %% -3 in count_to_five - -# %% [markdown] -# ### Unpacking +given = "Grace" +family = "Hopper" +full = given + " " + family # %% [markdown] -# Multiple values can be **unpacked** when assigning from sequences, like dealing out decks of cards. - -# %% -mylist = ['Hello', 'World'] -a, b = mylist -print(b) +# The multiplication operator `*` can be used to repeat strings # %% -range(4) +"Badger " * 3 -# %% -zero, one, two, three = range(4) +# %% [markdown] +# Methods such as `upper` and `lower` can be used to alter the case of the string characters. # %% -two +print(full.lower()) +print(full.upper()) # %% [markdown] -# If there is too much or too little data, an error results: - -# %% -zero, one, two, three = range(7) +# The `replace` method can be used to replace characters # %% -zero, one, two, three = range(2) +full.replace("c", "p") # %% [markdown] -# Python provides some handy syntax to split a sequence into its first element ("head") and the remaining ones (its "tail"): +# The `count` method can be used to count the occurences of particular characters in the string # %% -head, *tail = range(4) -print("head is", head) -print("tail is", tail) +full.count("p") # %% [markdown] -# Note the syntax with the \*. The same pattern can be used, for example, to extract the middle segment of a sequence whose length we might not know: +# We can use `strip` to remove extraneous whitespace from the start and end of a string: # %% -one, *two, three = range(10) - -# %% -print("one is", one) -print("two is", two) -print("three is", three) +" Hello ".strip() diff --git a/ch01python/025containers.ipynb.py b/ch01python/025containers.ipynb.py index 56943c6d..0d532c60 100644 --- a/ch01python/025containers.ipynb.py +++ b/ch01python/025containers.ipynb.py @@ -12,20 +12,153 @@ # --- # %% [markdown] -# ## Containers +# # Containers +# +# Containers are a data type that _contains_ other objects. + +# %% [markdown] +# ## Lists + +# %% [markdown] +# Python's basic **container** type is the `list` + +# %% [markdown] +# We can define our own list with square brackets: + +# %% +[1, 3, 7] + +# %% +type([1, 3, 7]) + +# %% [markdown] +# Lists *do not* have to contain just one type: + +# %% +various_things = [1, 2, "banana", 3.4, [1, 2]] + +# %% [markdown] +# We access an **element** of a list with an `int` in square brackets: + +# %% +one = 1 +two = 2 +three = 3 + +# %% +my_new_list = [one, two, three] + +# %% +middle_value_in_list = my_new_list[1] + +# %% +middle_value_in_list + +# %% +[1, 2, 3][1] + +# %% +various_things[2] + +# %% +index = 2 +various_things[index] + +# %% [markdown] +# Note that list indices start from zero. + +# %% [markdown] +# We can quickly make a list containing a range of consecutive integer numbers using the built-in `range` function + +# %% +count_to_five = list(range(5)) +print(count_to_five) + +# %% [markdown] +# We can use a string to join together a list of strings: + +# %% +name = ["Grace", "Brewster", "Murray", "Hopper"] +print(" ".join(name)) + +# %% [markdown] +# And we can split up a string into a list: + +# %% +"Ernst Stavro Blofeld".split(" ") + +# %% [markdown] +# We can an item to a list: + +# %% +name.append("BA") +print(" ".join(name)) + +# %% [markdown] +# Or we can add more than one: + +# %% +name.extend(["MS", "PhD"]) +print(" ".join(name)) + +# %% [markdown] +# Or insert values at different points in the list + +# %% +name.insert(0, "Admiral") +print(" ".join(name)) + +# %% [markdown] +# ## Sequences # %% [markdown] -# ### Checking for containment. +# Many other things can be treated like `lists`. Python calls things that can be treated like lists _sequences_. + +# %% [markdown] +# A string is one such *sequence type* + +# %% +print(count_to_five[1]) +print("James"[2]) + +# %% +print(count_to_five[1:3]) +print("Hello World"[4:8]) + +# %% +print(len(various_things)) +print(len("Python")) + +# %% +len([[1, 2], 4]) + +# %% [markdown] +# ## Unpacking + +# %% [markdown] +# Multiple values can be **unpacked** when assigning from sequences, like dealing out decks of cards. + +# %% +mylist = ["Goodbye", "Cruel"] +a, b = mylist +print(a) + +# %% +a = mylist[0] +b = mylist[1] + +# %% [markdown] +# ## Checking for containment # %% [markdown] # The `list` we saw is a container type: its purpose is to hold other objects. We can ask python whether or not a # container contains a particular item: # %% -'Dog' in ['Cat', 'Dog', 'Horse'] +"Dog" in ["Cat", "Dog", "Horse"] # %% -'Bird' in ['Cat', 'Dog', 'Horse'] +"Bird" in ["Cat", "Dog", "Horse"] # %% 2 in range(5) @@ -33,58 +166,61 @@ # %% 99 in range(5) +# %% +"a" in "cat" + # %% [markdown] -# ### Mutability +# ## Mutability # %% [markdown] -# A list can be modified: +# An list can be modified: # %% -name = "Sir Michael Edward Palin".split(" ") +name = "Grace Brewster Murray Hopper".split(" ") print(name) # %% -name[0] = "Knight" -name[1:3] = ["Mike-"] -name.append("FRGS") +name[0:3] = ["Admiral"] +name.append("PhD") print(" ".join(name)) # %% [markdown] -# ### Tuples +# ## Tuples # %% [markdown] # A `tuple` is an immutable sequence. It is like a list, execpt it cannot be changed. It is defined with round brackets. # %% -x = 0, -type(x) +my_tuple = ("Hello", "World") # %% -my_tuple = ("Hello", "World") -my_tuple[0] = "Goodbye" +my_tuple + +# %% [markdown] +# Trying to modify one of its values will fail: # %% -type(my_tuple) +my_tuple[0] = "Goodbye" # %% [markdown] # `str` is immutable too: # %% fish = "Hake" -fish[0] = 'R' +fish[0] = "R" # %% [markdown] # But note that container reassignment is moving a label, **not** changing an element: # %% -fish = "Rake" ## OK! +fish = "Rake" ## OK! # %% [markdown] -# *Supplementary material*: Try the [online memory visualiser](http://www.pythontutor.com/visualize.html#code=name+%3D++%22Sir+Michael+Edward+Palin%22.split%28%22+%22%29%0A%0Aname%5B0%5D+%3D+%22Knight%22%0Aname%5B1%3A3%5D+%3D+%5B%22Mike-%22%5D%0Aname.append%28%22FRGS%22%29%0A&mode=display&origin=opt-frontend.js&cumulative=false&heapPrimitives=true&textReferences=false&py=3&rawInputLstJSON=%5B%5D&curInstr=0) for this one. +# *Supplementary material*: Try the [online memory visualiser](http://www.pythontutor.com/visualize.html#code=name+%3D++%22Sir+Michael+Edward+Palin%22.split%28%22+%22%29%0A%0Aname%5B0%5D+%3D+%22Knight%22%0Aname%5B1%3A3%5D+%3D+%5B%22Mike-%22%5D%0Aname.append%28%22FRGS%22%29%0A%0Aname%20%3D%20%22King%20Arthur%22&&mode=display&origin=opt-frontend.js&cumulative=false&heapPrimitives=true&textReferences=false&py=3&rawInputLstJSON=%5B%5D&curInstr=0) for this one. # %% [markdown] -# ### Memory and containers +# ## Memory and containers # %% [markdown] # @@ -95,56 +231,52 @@ # %% x = list(range(3)) -x +print(x) # %% y = x -y +print(y) # %% z = x[0:3] y[1] = "Gotcha!" +print(x) +print(y) +print(z) # %% -x +z[2] = "Really?" +print(x) +print(y) +print(z) -# %% -y +# %% [markdown] +# *Supplementary material*: This one works well at the [memory visualiser](http://www.pythontutor.com/visualize.html#code=x%20%3D%20%5B%22What's%22,%20%22Going%22,%20%22On%3F%22%5D%0Ay%20%3D%20x%0Az%20%3D%20x%5B0%3A3%5D%0A%0Ay%5B1%5D%20%3D%20%22Gotcha!%22%0Az%5B2%5D%20%3D%20%22Really%3F%22&cumulative=false&curInstr=0&heapPrimitives=true&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false). # %% -z +x = ["What's", "Going", "On?"] +y = x +z = x[0:3] -# %% +y[1] = "Gotcha!" z[2] = "Really?" # %% x -# %% -y - -# %% -z - # %% [markdown] -# *Supplementary material*: This one works well at the [memory visualiser](http://www.pythontutor.com/visualize.html#code=x+%3D+%5B%22What's%22,+%22Going%22,+%22On%3F%22%5D%0Ay+%3D+x%0Az+%3D+x%5B0%3A3%5D%0A%0Ay%5B1%5D+%3D+%22Gotcha!%22%0Az%5B2%5D+%3D+%22Really%3F%22&mode=display&origin=opt-frontend.js&cumulative=false&heapPrimitives=true&textReferences=false&py=3&rawInputLstJSON=%5B%5D&curInstr=0). - -# %% [markdown] -# The explanation: While `y` is a second label on the *same object*, `z` is a separate object with the same data. Writing `x[:]` creates a new list containing all the elements of `x` (remember: `[:]` is equivalent to `[0:]`). This is the case whenever we take a slice from a list, not just when taking all the elements with `[:]`. -# -# The difference between `y=x` and `z=x[:]` is important! +# The explanation: While `y` is a second label on the *same object*, `z` is a separate object with the same data. # %% [markdown] # Nested objects make it even more complicated: # %% -x = [['a', 'b'] , 'c'] +x = [["a", "b"], "c"] y = x z = x[0:2] -# %% -x[0][1] = 'd' -z[1] = 'e' +x[0][1] = "d" +z[1] = "e" # %% x @@ -156,52 +288,40 @@ z # %% [markdown] -# Try the [visualiser](http://www.pythontutor.com/visualize.html#code=x%20%3D%20%5B%5B'a',%20'b'%5D,%20'c'%5D%0Ay%20%3D%20x%0Az%20%3D%20x%5B0%3A2%5D%0A%0Ax%5B0%5D%5B1%5D%20%3D%20'd'%0Az%5B1%5D%20%3D%20'e'&mode=display&origin=opt-frontend.js&cumulative=false&heapPrimitives=true&textReferences=false&py=3&rawInputLstJSON=%5B%5D&curInstr=0) again. +# Try the [visualiser](http://www.pythontutor.com/visualize.html#code=x%3D%5B%5B'a','b'%5D,'c'%5D%0Ay%3Dx%0Az%3Dx%5B0%3A2%5D%0A%0Ax%5B0%5D%5B1%5D%3D'd'%0Az%5B1%5D%3D'e'&cumulative=false&curInstr=5&heapPrimitives=true&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false) +# again. # # *Supplementary material*: The copies that we make through slicing are called *shallow copies*: we don't copy all the objects they contain, only the references to them. This is why the nested list in `x[0]` is not copied, so `z[0]` still refers to it. It is possible to actually create copies of all the contents, however deeply nested they are - this is called a *deep copy*. Python provides methods for that in its standard library, in the `copy` module. You can read more about that, as well as about shallow and deep copies, in the [library reference](https://docs.python.org/3/library/copy.html). # %% [markdown] -# ### Identity vs Equality +# ## Identity versus equality # # # Having the same data is different from being the same actual object # in memory: # %% -[1, 2] == [1, 2] - -# %% -[1, 2] is [1, 2] +print([1, 2] == [1, 2]) +print([1, 2] is [1, 2]) # %% [markdown] -# The == operator checks, element by element, that two containers have the same data. +# The `==` operator checks, element by element, that two containers have the same data. # The `is` operator checks that they are actually the same object. -# %% [markdown] -# But, and this point is really subtle, for immutables, the python language might save memory by reusing a single instantiated copy. This will always be safe. - # %% -"Hello" == "Hello" +my3numbers = list(range(3)) +print(my3numbers) # %% -"Hello" is "Hello" - -# %% [markdown] -# This can be useful in understanding problems like the one above: +[0, 1, 2] == my3numbers # %% -x = range(3) -y = x -z = x[:] - -# %% -x == y +[0, 1, 2] is my3numbers -# %% -x is y - -# %% -x == z +# %% [markdown] +# But, and this point is really subtle, for immutables, the Python language might save memory by reusing a single instantiated copy. This will always be safe. # %% -x is z +word = "Hello" +print("Hello" == word) +print("Hello" is word) diff --git a/ch01python/028dictionaries.ipynb.py b/ch01python/028dictionaries.ipynb.py index 2bf86719..51f3bcf9 100644 --- a/ch01python/028dictionaries.ipynb.py +++ b/ch01python/028dictionaries.ipynb.py @@ -1,7 +1,7 @@ # --- # jupyter: # jekyll: -# display_name: Dictionaries +# display_name: Dictionaries and Sets # jupytext: # notebook_metadata_filter: -kernelspec,jupytext,jekyll # text_representation: @@ -12,10 +12,10 @@ # --- # %% [markdown] -# ## Dictionaries +# # Dictionaries and Sets # %% [markdown] -# ### The Python Dictionary +# ## The Python Dictionary # %% [markdown] # Python supports a container type called a dictionary. @@ -28,8 +28,6 @@ # %% names = "Martin Luther King".split(" ") - -# %% names[1] # %% [markdown] @@ -43,13 +41,13 @@ chapman # %% -chapman['Jobs'] +print(chapman["Jobs"]) # %% -chapman['age'] +print(chapman["age"]) # %% -type(chapman) +print(type(chapman)) # %% [markdown] # ### Keys and Values @@ -70,22 +68,22 @@ # When we test for containment on a `dict` we test on the **keys**: # %% -'Jobs' in chapman +"Jobs" in chapman # %% -'Graham' in chapman +"Graham" in chapman # %% -'Graham' in chapman.values() +"Graham" in chapman.values() # %% [markdown] # ### Immutable Keys Only # %% [markdown] # The way in which dictionaries work is one of the coolest things in computer science: -# the "hash table". The details of this are beyond the scope of this course, but we will consider some aspects in the section on performance programming. +# the "hash table". This is way beyond the scope of this course, but it has a consequence: # -# One consequence of this implementation is that you can only use **immutable** things as keys. +# You can only use **immutable** things as keys. # %% good_match = { @@ -103,26 +101,50 @@ } # %% [markdown] -# Remember -- square brackets denote lists, round brackets denote `tuple`s. +# *Supplementary material*: You can start to learn about [the 'hash table'](https://www.youtube.com/watch?v=h2d9b_nEzoA). Though this video is **very** advanced, it's really interesting! # %% [markdown] -# ### No guarantee of order (before Python 3.7) - -# %% [markdown] -# -# Another consequence of the way dictionaries used to work is that there was no guaranteed order among the -# elements. However, since Python 3.7, it's guaranteed that dictionaries return elements in the order in which they were inserted. Read more about [why that changed and how it is still fast](https://stackoverflow.com/a/39980744/1087595). -# -# +# ### No guarantee of order # +# Another consequence of the way dictionaries work is that there's no guaranteed order among the +# elements: + # %% -my_dict = {'0': 0, '1':1, '2': 2, '3': 3, '4': 4} +my_dict = {"0": 0, "1": 1, "2": 2, "3": 3, "4": 4} print(my_dict) print(my_dict.values()) # %% [markdown] -# ### Sets +# ### Safe Lookup +# +# Some times you want a program to keep working even when a key is looked up but it's not there. +# Python dictionaries offers that through the `get` method. + +# %% +x = {"a": 1, "b": 2} + +# %% +x["a"] + +# %% +x["fish"] + +# %% +x.get("a") + +# %% +x.get("fish") + +# %% [markdown] +# By default `get` returns `None` if the key searched is not in the dictionary. +# However, you can change that default by adding what's the value you want it to return. + +# %% +x.get("fish", "tuna") == "tuna" + +# %% [markdown] +# ## Sets # %% [markdown] # A set is a `list` which cannot contain the same element twice. @@ -142,10 +164,9 @@ primes_below_ten = { 2, 3, 5, 7} # %% -type(unique_letters) +print(type(unique_letters)) +print(type(primes_below_ten)) -# %% -type(primes_below_ten) # %% unique_letters @@ -154,10 +175,21 @@ # This will be easier to read if we turn the set of letters back into a string, with `join`: # %% -"".join(unique_letters) +print("".join(unique_letters)) + +# %% [markdown] +# `join` uses the character give to be what joins the sequence given: + +# %% +"-".join(["a", "b", "c"]) # %% [markdown] -# A set has no particular order, but is really useful for checking or storing **unique** values. +# Note that a set has no particular order, but is really useful for checking or storing **unique** values. + +# %% +alist = [1, 2, 3] +is_unique = len(set(alist)) == len(alist) +print(is_unique) # %% [markdown] # Set operations work as in mathematics: @@ -179,3 +211,4 @@ # Your programs will be faster and more readable if you use the appropriate container type for your data's meaning. # Always use a set for lists which can't in principle contain the same data twice, always use a dictionary for anything # which feels like a mapping from keys to values. + diff --git a/ch01python/029structures.ipynb.py b/ch01python/029structures.ipynb.py index 8439ed97..fb07c13b 100644 --- a/ch01python/029structures.ipynb.py +++ b/ch01python/029structures.ipynb.py @@ -12,10 +12,10 @@ # --- # %% [markdown] -# ## Data structures +# # Data structures # %% [markdown] -# ### Nested Lists and Dictionaries +# ## Nested Lists and Dictionaries # %% [markdown] # In research programming, one of our most common tasks is building an appropriate *structure* to model our complicated @@ -29,7 +29,7 @@ } # %% -Chapman = { +Chapman_house = { 'City': 'London', 'Street': 'Southwood ln', 'Postcode': 'N6 5TB' @@ -39,7 +39,7 @@ # A collection of people's addresses is then a list of dictionaries: # %% -addresses = [UCL, Chapman] +addresses = [UCL, Chapman_house] # %% addresses @@ -51,7 +51,7 @@ UCL['people'] = ['Jeremy','Leonard', 'James', 'Henry'] # %% -Chapman['people'] = ['Graham', 'David'] +Chapman_house["People"] = ["Graham", "David"] # %% addresses @@ -63,46 +63,59 @@ # We can go further, e.g.: # %% -UCL['Residential'] = False +UCL["Residential"] = False # %% [markdown] # And we can write code against our structures: # %% -leaders = [place['people'][0] for place in addresses] -leaders +leaders = [place["People"][0] for place in addresses] +print(leaders) # %% [markdown] -# This was an example of a 'list comprehension', which have used to get data of this structure, and which we'll see more of in a moment... +# This was an example of a 'list comprehension', which is used to get data of this structure, and which we'll see more of in a moment... # %% [markdown] -# ### Exercise: a Maze Model. +# ## Exercise: a Maze Model. # %% [markdown] # Work with a partner to design a data structure to represent a maze using dictionaries and lists. # %% [markdown] # * Each place in the maze has a name, which is a string. -# * Each place in the maze has one or more people currently standing at it, by name. +# * Each place in the maze has zero or more people currently standing at it, by name. # * Each place in the maze has a maximum capacity of people that can fit in it. -# * From each place in the maze, you can go from that place to a few other places, using a direction like 'up', 'north', +# * For each place in the maze, you can go from that place to a few other places, using a direction like 'up', 'north', # or 'sideways' # %% [markdown] # Create an example instance, in a notebook, of a simple structure for your maze: # %% [markdown] -# * The front room can hold 2 people. Graham is currently there. You can go outside to the garden, or upstairs to the bedroom, or north to the kitchen. -# * From the kitchen, you can go south to the front room. It fits 1 person. -# * From the garden you can go inside to front room. It fits 3 people. David is currently there. -# * From the bedroom, you can go downstairs to the front room. You can also jump out of the window to the garden. It fits 2 people. +# * The living room can hold 2 people. Graham is currently there. You can go outside to the garden, or upstairs to the bedroom, or north to the kitchen. +# * From the kitchen, you can go south to the living room. It fits 1 person. +# * From the garden you can go inside to living room. It fits 3 people. David is currently there. +# * From the bedroom, you can go downstairs. You can also jump out of the window to the garden. It fits 2 people. # %% [markdown] # Make sure that your model: # -# * Allows empty rooms +# * Allows empty rooms. # * Allows you to jump out of the upstairs window, but not to fly back up. # * Allows rooms which people can't fit in. # %% [markdown] -# myhouse = [ "Your answer here" ] +# As an example of a similar problem, the following code could be used to represent a collection of cities, each of which has a name, a maximum capacity, and potentially some people currently living there. + +# %% +cities = [ + {"name": "London", "capacity": 8, "residents": ["Graham", "David"]}, + {"name": "Edinburgh", "capacity": 1, "residents": ["Henry"]}, + {"name": "Cardiff", "capacity": 1, "residents": []}, +] + +# %% [markdown] +# We can then check, for instance, how many people currently reside in the third city: + +# %% +len(cities[2]["residents"]) diff --git a/ch01python/030MazeSolution.ipynb.py b/ch01python/030MazeSolution.ipynb.py index 0a933f2d..c12d7a2a 100644 --- a/ch01python/030MazeSolution.ipynb.py +++ b/ch01python/030MazeSolution.ipynb.py @@ -63,3 +63,11 @@ # * Python allows code to continue over multiple lines, so long as sets of brackets are not finished. # * There is an **empty** person list in empty rooms, so the type structure is robust to potential movements of people. # * We are nesting dictionaries and lists, with string and integer data. + +# %% +people_so_far = 0 + +for room_name in house: + people_so_far = people_so_far + len(house[room_name]["people"]) + +print(people_so_far) diff --git a/ch01python/032conditionality.ipynb.py b/ch01python/032conditionality.ipynb.py index 2bac8b8e..770f5161 100644 --- a/ch01python/032conditionality.ipynb.py +++ b/ch01python/032conditionality.ipynb.py @@ -12,10 +12,10 @@ # --- # %% [markdown] -# ## Control and Flow +# # Control and Flow # %% [markdown] -# ### Turing completeness +# ## Turing completeness # %% [markdown] # Now that we understand how we can use objects to store and model our data, we only need to be able to control the flow of our @@ -30,40 +30,32 @@ # Once we have these, we can write computer programs to process information in arbitrary ways: we are *Turing Complete*! # %% [markdown] -# ### Conditionality +# ## Conditionality # %% [markdown] # Conditionality is achieved through Python's `if` statement: # %% -x = 5 - -if x < 0: - print(f"{x} is negative") - -# %% [markdown] -# The absence of output here means the if clause prevented the print statement from running. - -# %% -x = -10 - +x = -3 if x < 0: print(f"{x} is negative") + print("This is controlled") +print("Always run this") # %% [markdown] -# The first time through, the print statement never happened. +# The **controlled** statements are indented. Once we remove the indent, the statements will once again happen regardless of whether the `if` statement is true of false. # %% [markdown] -# The **controlled** statements are indented. Once we remove the indent, the statements will once again happen regardless. +# As a side note, note how we included the values of `x` in the first print statement. This is a handy syntax for building strings that contain the values of variables. You can read more about it at this [Python String Formatting Best Practices guide](https://realpython.com/python-string-formatting/#2-new-style-string-formatting-strformat) or in the [official documentation](https://docs.python.org/3/tutorial/inputoutput.html#formatted-string-literals). # %% [markdown] -# ### Else and Elif +# ## Else and Elif # %% [markdown] # Python's if statement has optional elif (else-if) and else clauses: # %% -x = 5 +x = -3 if x < 0: print("x is negative") else: @@ -78,22 +70,21 @@ else: print("x is positive") - # %% [markdown] -# Try editing the value of x here, and note that other sections are found. +# Try editing the value of x here, and note which section of the code is run and which are not. # %% -choice = 'high' +choice = "low" -if choice == 'high': +if choice == "high": print(1) -elif choice == 'medium': +elif choice == "medium": print(2) else: print(3) # %% [markdown] -# ### Comparison +# ## Comparison # %% [markdown] # `True` and `False` are used to represent **boolean** (true or false) values. @@ -102,73 +93,60 @@ 1 > 2 # %% [markdown] -# Comparison on strings is alphabetical. +# Comparison on strings is alphabetical - letters earlier in the alphabet are 'lower' than later letters. # %% -"UCL" > "KCL" - -# %% [markdown] -# But case sensitive: +"A" < "Z" # %% -"UCL" > "kcl" +"UCL" > "KCL" # %% [markdown] -# There's no automatic conversion of the **string** True to true: +# There's no automatic conversion of the **string** True to the **boolean variable** `True`: # %% True == "True" # %% [markdown] -# In python two there were subtle implied order comparisons between types, but it was bad style to rely on these. -# In python three, you cannot compare these. +# Be careful not to compare values of different types. At best, the language won't allow it and an issue an error, at worst it will allow it and do some behind-the-scenes conversion that may be surprising. # %% -'1' < 2 +"1" < 2 -# %% -'5' < 2 +# %% [markdown] +# Any statement that evaluates to `True` or `False` can be used to control an `if` Statement. Experiment with numbers (integers and floats) - what is equivalent to `True`? # %% -'1' > 2 +0 == False # %% [markdown] -# Any statement that evaluates to `True` or `False` can be used to control an `if` Statement. - -# %% [markdown] -# ### Automatic Falsehood +# ## Automatic Falsehood # %% [markdown] # Various other things automatically count as true or false, which can make life easier when coding: # %% mytext = "Hello" - -# %% if mytext: print("Mytext is not empty") - -# %% mytext2 = "" - -# %% if mytext2: print("Mytext2 is not empty") # %% [markdown] -# We can use logical not and logical and to combine true and false: +# We can use logical `not` and logical `and` to combine true and false: # %% x = 3.2 if not (x > 0 and isinstance(x, int)): - print(x,"is not a positive integer") + print(f"{x} is not a positive integer") # %% [markdown] # `not` also understands magic conversion from false-like things to True or False. # %% -not not "Who's there!" # Thanks to Mysterious Student +not not "Who's there!" #  Thanks to Mysterious Student # %% bool("") @@ -180,13 +158,13 @@ bool([]) # %% -bool(['a']) +bool(["a"]) # %% bool({}) # %% -bool({'name': 'Graham'}) +bool({"name": "Graham"}) # %% bool(0) @@ -194,6 +172,9 @@ # %% bool(1) +# %% +not 2 == 3 + # %% [markdown] # But subtly, although these quantities evaluate True or False in an if statement, they're not themselves actually True or False under ==: @@ -204,7 +185,7 @@ bool([]) == False # %% [markdown] -# ### Indentation +# ## Indentation # %% [markdown] # In Python, indentation is semantically significant. @@ -233,12 +214,12 @@ print(x) # %% [markdown] -# ###  Pass +# ## Pass # %% [markdown] # -# A statement expecting identation must have some indented code. -# This can be annoying when commenting things out. (With `#`) +# A statement expecting identation must have some indented code or it will create an error. +# This can be annoying when commenting things out (with `#`) inside a loop or conditional statement. # # # @@ -246,14 +227,12 @@ # %% if x > 0: # print x - print("Hello") # %% [markdown] # # -# -# So the `pass` statement is used to do nothing. +# So the `pass` statement (or `...`) is used to do nothing. # # # @@ -262,5 +241,4 @@ if x > 0: # print x pass - print("Hello") diff --git a/ch01python/035looping.ipynb.py b/ch01python/035looping.ipynb.py index df9b0fa8..b9623503 100644 --- a/ch01python/035looping.ipynb.py +++ b/ch01python/035looping.ipynb.py @@ -12,25 +12,24 @@ # --- # %% [markdown] -# ### Iteration +# ## Iteration # %% [markdown] # Our other aspect of control is looping back on ourselves. # -# We use `for` ... `in` to "iterate" over lists: +# We use `for` ... `in` ... to "iterate" over lists: # %% mylist = [3, 7, 15, 2] -# %% -for whatever in mylist: - print(whatever ** 2) +for element in mylist: + print(element ** 2) # %% [markdown] -# Each time through the loop, the variable in the `value` slot is updated to the **next** element of the sequence. +# Each time through the loop, the value in the `element` slot is updated to the **next** value in the sequence. # %% [markdown] -# ### Iterables +# ## Iterables # %% [markdown] # @@ -41,6 +40,7 @@ # %% vowels = "aeiou" + sarcasm = [] for letter in "Okay": @@ -48,7 +48,6 @@ repetition = 3 else: repetition = 1 - sarcasm.append(letter * repetition) "".join(sarcasm) @@ -57,13 +56,14 @@ # The above is a little puzzle, work through it to understand why it does what it does. # %% [markdown] -# ###  Dictionaries are Iterables +# ## Dictionaries are Iterables # %% [markdown] -# All sequences are iterables. Some iterables (things you can `for` loop over) are not sequences (things with you can do `x[5]` to), for example sets and dictionaries. +# All sequences are iterables. Some iterables (things you can `for` loop over) are not sequences (things with you can do `x[5]` to), for example **sets**. # %% import datetime + now = datetime.datetime.now() founded = {"Eric": 1943, "UCL": 1826, "Cambridge": 1209} @@ -71,10 +71,21 @@ current_year = now.year for thing in founded: - print(thing, " is ", current_year - founded[thing], "years old.") + print(f"{thing} is {current_year - founded[thing]} years old.") + +# %% +thing = "UCL" + +founded[thing] + +# %% +founded + +# %% +founded.items() # %% [markdown] -# ### Unpacking and Iteration +# ## Unpacking and Iteration # %% [markdown] # @@ -93,6 +104,12 @@ for whatever in triples: print(whatever) +# %% +a, b = [36, 7] + +# %% +b + # %% for first, middle, last in triples: print(middle) @@ -106,24 +123,21 @@ # # # -# for example, to iterate over the items in a dictionary as pairs: +# For example, to iterate over the items in a dictionary as pairs: # # # # %% -things = {"Eric": [1943, 'South Shields'], - "UCL": [1826, 'Bloomsbury'], - "Cambridge": [1209, 'Cambridge']} - -print(things.items()) +for name, year in founded.items(): + print(f"{name} is {current_year - year} years old.") # %% -for name, year in founded.items(): - print(name, " is ", current_year - year, "years old.") +for name in founded: + print(f"{name} is {current_year - founded[name]} years old.") # %% [markdown] -# ### Break, Continue +# ## Break, Continue # %% [markdown] # @@ -135,18 +149,47 @@ # %% for n in range(50): - if n == 20: + if n == 20: break if n % 2 == 0: continue print(n) +# %% [markdown] +# These aren't useful that often, but are worth knowing about. There's also an optional `else` clause on loops, executed only if the loop gets through all its iterations without a `break` or `return`. This can help to keep the code cleaner avoiding to have a condition to check that something happened. + # %% [markdown] -# These aren't useful that often, but are worth knowing about. There's also an optional `else` clause on loops, executed only if you don't `break`, but I've never found that useful. +# For example if someone goes to a cheese shop and wants a piece of cheese from one of their favourites could be written as it follows. + +# %% +shop = ['Wenslydale (shop assistant)', 'Bazouki player', 'Customer'] +for cheese_type in ['red Leciester', 'Tilsit', 'Caerphilly']: + if found := cheese_type in shop: + print(f"Buy {cheese_type} and leave") + +if not found: + print("Left empty-handed") + +# %% [markdown] +# Whereas using `else` the second `if` condition and the `found` variable wouldn't be needed: + +# %% +shop = ['Wenslydale (shop assistant)', 'Bazouki player', 'Customer'] +for cheese_type in ['Red Leciester', 'Tilsit', 'Caerphilly']: + if cheese_type in shop: + print(f"Buy {cheese_type} and leave") + break +else: + print("Left empty-handed") + +# %% [markdown] +# Note that we've used the `:=` walrus operator (introduced in python 3.8 from [PEP572](https://peps.python.org/pep-0572/)). +# The example above is based on [The Cheese Shop](http://www.montypython.50webs.com/scripts/Series_3/61.htm) sketch from the Monty Python. The sketch goes over a longer list of cheeses. + # %% [markdown] -# ### Classroom exercise: the Maze Population +# ## Exercise: the Maze Population # %% [markdown] # Take your maze data structure. Write a program to count the total number of people in the maze, and also determine the total possible occupants. diff --git a/ch01python/036MazeSolution2.ipynb.py b/ch01python/036MazeSolution2.ipynb.py index d756deb5..40ad2909 100644 --- a/ch01python/036MazeSolution2.ipynb.py +++ b/ch01python/036MazeSolution2.ipynb.py @@ -63,5 +63,3 @@ occupancy += len(room['people']) print(f"House can fit {capacity} people, and currently has: {occupancy}.") -# %% [markdown] -# As a side note, note how we included the values of `capacity` and `occupancy` in the last line. This is a handy syntax for building strings that contain the values of variables. You can read more about it at this [Python String Formatting Best Practices guide](https://realpython.com/python-string-formatting/#2-new-style-string-formatting-strformat) or in the [official documentation](https://docs.python.org/3/tutorial/inputoutput.html#formatted-string-literals). diff --git a/ch01python/037comprehensions.ipynb.py b/ch01python/037comprehensions.ipynb.py index 09e65960..f4c69e03 100644 --- a/ch01python/037comprehensions.ipynb.py +++ b/ch01python/037comprehensions.ipynb.py @@ -12,36 +12,34 @@ # --- # %% [markdown] -# ## Comprehensions +# # Comprehensions # %% [markdown] -# ### The list comprehension +# ## The list comprehension # %% [markdown] # If you write a for loop **inside** a pair of square brackets for a list, you magic up a list as defined. # This can make for concise but hard to read code, so be careful. -# %% -[2 ** x for x in range(10)] - -# %% [markdown] -# Which is equivalent to the following code without using comprehensions: - # %% result = [] for x in range(10): result.append(2 ** x) - -result + +print(result) # %% [markdown] -# You can do quite weird and cool things with comprehensions: +# is the same as # %% -[len(str(2 ** x)) for x in range(10)] +result = [2 ** x for x in range(10)] +print(result) # %% [markdown] -# ### Selection in comprehensions +# You can do quite weird and cool things with comprehensions: + +# %% +[len(str(2 ** x)) for x in range(20)] # %% [markdown] # You can write an `if` statement in comprehensions too: @@ -57,7 +55,7 @@ if letter.lower() not in 'aeiou']) # %% [markdown] -# ### Comprehensions versus building lists with `append`: +# ## Comprehensions versus building lists with `append` # %% [markdown] # This code: @@ -67,10 +65,10 @@ for x in range(30): if x % 3 == 0: result.append(2 ** x) -result +print(result) # %% [markdown] -# Does the same as the comprehension above. The comprehension is generally considered more readable. +# does the same as the comprehension above. The comprehension is generally considered more readable. # %% [markdown] # Comprehensions are therefore an example of what we call 'syntactic sugar': they do not increase the capabilities of the language. @@ -79,10 +77,10 @@ # Instead, they make it possible to write the same thing in a more readable way. # %% [markdown] -# Almost everything we learn from now on will be either syntactic sugar or interaction with something other than idealised memory, such as a storage device or the internet. Once you have variables, conditionality, and branching, your language can do anything. (And this can be proved.) +# Everything we learn from now on will be either syntactic sugar or interaction with something other than idealised memory, such as a storage device or the internet. Once you have variables, conditionality, and branching, your language can do anything. (And this can be proved.) # %% [markdown] -# ### Nested comprehensions +# ## Nested comprehensions # %% [markdown] # If you write two `for` statements in a comprehension, you get a single array generated over all the pairs: @@ -109,22 +107,22 @@ # Note that the list order for multiple or nested comprehensions can be confusing: # %% -[x+y for x in ['a', 'b', 'c'] for y in ['1', '2', '3']] +[x + y for x in ["a", "b", "c"] for y in ["1", "2", "3"]] # %% -[[x+y for x in ['a', 'b', 'c']] for y in ['1', '2', '3']] +[[x + y for x in ["a", "b", "c"]] for y in ["1", "2", "3"]] # %% [markdown] -# ### Dictionary Comprehensions +# ## Dictionary Comprehensions # %% [markdown] -# You can automatically build dictionaries, by using a list comprehension syntax, but with curly brackets and a colon: +# You can automatically build dictionaries by using a list comprehension syntax, but with curly brackets and a colon: # %% -{(str(x)) * 3: x for x in range(3)} +{((str(x)) * 3): x for x in range(3)} # %% [markdown] -# ### List-based thinking +# ## List-based thinking # %% [markdown] # Once you start to get comfortable with comprehensions, you find yourself working with containers, nested groups of lists @@ -133,7 +131,9 @@ # %% [markdown] # Given a way to analyse some dataset, we'll find ourselves writing stuff like: # -# analysed_data = [analyze(datum) for datum in data] +# analysed_data = [analyse(datum) for datum in data] +# +# analysed_data = map(analyse, data) # %% [markdown] # There are lots of built-in methods that provide actions on lists as a whole: @@ -151,36 +151,40 @@ sum([1, 2, 3]) # %% [markdown] -# My favourite is `map`, which, similar to a list comprehension, applies one function to every member of a list: +# One common method is `map`, which works like a simple list comprehension: it applies one function to every member of a list. # %% [str(x) for x in range(10)] +# %% +map(str, range(10)) + +# %% [markdown] +# Its output is this strange-looking `map` object, which can be iterated over (with a `for`) or turned into a list: + +# %% +for element in map(str, range(10)): + print(element) + # %% list(map(str, range(10))) # %% [markdown] -# So I can write: +# We can use `map` to process a set of `data` through a `analyse` function as: # -# analysed_data = map(analyse, data) +# analysed_data = list(map(analyse, data)) # # We'll learn more about `map` and similar functions when we discuss functional programming later in the course. # %% [markdown] -# ### Classroom Exercise: Occupancy Dictionary - -# %% [markdown] -# Take your maze data structure. First write an expression to print out a new dictionary, which holds, for each room, that room's capacity. The output should look like: - -# %% -{'bedroom': 1, 'garden': 3, 'kitchen': 1, 'living': 2} +# ## Exercise: Occupancy Dictionary # %% [markdown] -# Now, write a program to print out a new dictionary, which gives, +# Take your maze data structure. Write a program to print out a new dictionary, which gives, # for each room's name, the number of people in it. Don't add in a zero value in the dictionary for empty rooms. # %% [markdown] # The output should look similar to: # %% -{'garden': 1, 'living': 1} +{"bedroom": 1, "garden": 3, "kitchen": 1, "living": 2} diff --git a/ch01python/050import.ipynb.py b/ch01python/050import.ipynb.py index 0196103f..a0307c4b 100644 --- a/ch01python/050import.ipynb.py +++ b/ch01python/050import.ipynb.py @@ -12,10 +12,10 @@ # --- # %% [markdown] -# ## Using Libraries +# # Using Libraries # %% [markdown] -# ### Import +# ## Import # %% [markdown] # To use a function or type from a python library, rather than a **built-in** function or type, we have to import the library. @@ -48,14 +48,25 @@ math.pi # %% [markdown] -# You can always find out where on your storage medium a library has been imported from: +# You can find out where on your storage medium a library has been imported from: # %% -print(math.__file__[0:50]) -print(math.__file__[50:]) +print(math.__file__) # %% [markdown] -# Note that `import` does *not* install libraries. It just makes them available to your current notebook session, assuming they are already installed. Installing libraries is harder, and we'll cover it later. +# Some modules do not use the `__file__` attribute so you may get an error: +# +# ``` python +# AttributeError: module 'modulename' has no attribute '__file__' +# ``` +# +# If this is the case `print(modulename)` should display a description of the module object which will include an indication if the module is a 'built-in' module written in C (in which case the file path will not be specified) or the file path to the module otherwise. + +# %% +print(math) + +# %% [markdown] +# Note that `import` does *not* install libraries from PyPI. It just makes them available to your current notebook session, assuming they are already installed. Installing libraries is harder, and we'll cover it later. # So what libraries are available? Until you install more, you might have just the modules that come with Python, the *standard library*. # %% [markdown] @@ -64,12 +75,12 @@ # %% [markdown] # If you installed via Anaconda, then you also have access to a bunch of modules that are commonly used in research. # -# **Supplementary Materials**: Review the [list of modules that are packaged with Anaconda by default on different architectures](https://docs.anaconda.com/anaconda/packages/pkg-docs/) (modules installed by default are shown with ticks). +# **Supplementary Materials**: Review the [list of modules that are packaged with Anaconda by default on different architectures](http://docs.continuum.io/anaconda/pkg-docs.html) (choose your operating system and see which packages have a tick mark). # # We'll see later how to add more libraries to our setup. # %% [markdown] -# ### Why bother? +# ## Why bother? # %% [markdown] # Why bother with modules? Why not just have everything available all the time? @@ -77,7 +88,7 @@ # The answer is that there are only so many names available! Without a module system, every time I made a variable whose name matched a function in a library, I'd lose access to it. In the olden days, people ended up having to make really long variable names, thinking their names would be unique, and they still ended up with "name clashes". The module mechanism avoids this. # %% [markdown] -# ### Importing from modules +# ## Importing from modules # %% [markdown] # Still, it can be annoying to have to write `math.sin(math.pi)` instead of `sin(pi)`. @@ -85,11 +96,13 @@ # %% import math -math.sin(math.pi) + +print(math.sin(math.pi)) # %% from math import sin -sin(math.pi) + +print(sin(math.pi)) # %% [markdown] # Importing one-by-one like this is a nice compromise between typing and risk of name clashes. @@ -98,20 +111,31 @@ # It *is* possible to import **everything** from a module, but you risk name clashes. # %% +pi = "pie" + + +def sin(x): + print(f"eat {x}") + +print(sin(pie)) + from math import * -sin(pi) + +print(sin(pi)) # %% [markdown] -# ###  Import and rename +# ## Import and rename # %% [markdown] # You can rename things as you import them to avoid clashes or for typing convenience # %% import math as m -m.cos(0) + +print(m.cos(0)) # %% pi = 3 from math import pi as realpi + print(sin(pi), sin(realpi))