From c3f2a235d0c488c5ebb8872111ffd5e281500274 Mon Sep 17 00:00:00 2001 From: Saransh Chopra Date: Thu, 15 Aug 2024 11:21:33 +0100 Subject: [PATCH] port entire Part 1 / Intro to Python --- ch01python/015variables.ipynb.py | 111 +++------ ch01python/016functions.ipynb.py | 158 ++++++------- ch01python/017using_functions.ipynb.py | 223 ++---------------- ch01python/023types.ipynb.py | 314 +++++++++---------------- ch01python/025containers.ipynb.py | 264 +++++++++++++++------ ch01python/028dictionaries.ipynb.py | 103 ++++---- ch01python/029structures.ipynb.py | 63 +++-- ch01python/030MazeSolution.ipynb.py | 8 + ch01python/032conditionality.ipynb.py | 118 ++++------ ch01python/035looping.ipynb.py | 71 +++--- ch01python/036MazeSolution2.ipynb.py | 24 +- ch01python/037comprehensions.ipynb.py | 80 +++---- ch01python/050import.ipynb.py | 48 ++-- 13 files changed, 681 insertions(+), 904 deletions(-) diff --git a/ch01python/015variables.ipynb.py b/ch01python/015variables.ipynb.py index 4d5f8f4c..3e834140 100644 --- a/ch01python/015variables.ipynb.py +++ b/ch01python/015variables.ipynb.py @@ -12,9 +12,9 @@ # --- # %% [markdown] -# ## Variables +# # Variables # -# ### Variable assignment +# ## 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. @@ -38,7 +38,7 @@ greeting = "hello world" # %% [markdown] -# ### Naming variables +# ## 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). @@ -66,7 +66,27 @@ two_plus_two = 5 # %% [markdown] -# ### Undefined variables and `None` +# ## _Aside:_ Reading error messages + +# %% [markdown] +# 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 + +# %% +number_1 = 1 +number_2 = "2" +sum_of_numbers = number_1 + number_2 + +# %% [markdown] +# 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] +# ## Undefined variables and `None` # # If we try to evaluate a variable that hasn't ever been defined, we get an error. @@ -111,7 +131,7 @@ # [Software Carpentry](https://swcarpentry.github.io/python-novice-inflammation/01-intro/index.html). # %% [markdown] -# ### Reassignment and multiple labels +# ## Reassignment and multiple labels # %% [markdown] # We can reassign a variable - that is change what is in the box the variable labels. @@ -140,7 +160,7 @@ print(nom) # %% [markdown] -# ### Variables and memory +# ## 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. # @@ -168,83 +188,8 @@ # _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] -# ### 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'. - -# %% -type(name) - -# %% [markdown] -# Depending on its type, an object can have different *properties*: data fields Inside the object. - -# %% [markdown] -# Consider a Python complex number for example: - -# %% -z = 3 + 1j - -# %% [markdown] -# We can see what properties and methods an object has available using the `dir` function: - -# %% -dir(z) - -# %% [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 - -# %% -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. - -# %% [markdown] -# When we try to access a property that doesn't exist, we get an error: - -# %% -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! - -# %% [markdown] -# We don't yet know what is meant by `AttributeError`, or "Traceback". - -# %% -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. - -# %% [markdown] -# We can also see that the error message: -# > 'complex' object has no attribute 'wrong' -# -# ...tells us something important. Even if we don't understand the rest, this is useful for debugging! - # %% [markdown] -# ### Variables in notebooks and kernels +# ## Variables in notebooks and kernels # %% [markdown] # When code cells are executed in a notebook, the variable names and values of the referenced objects persist between cells @@ -267,7 +212,7 @@ # %% [markdown] # 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 +# ## Cell run order # # Cells do **not** have to be evaluated in the order they appear in a notebook. # diff --git a/ch01python/016functions.ipynb.py b/ch01python/016functions.ipynb.py index a1659fea..99412302 100644 --- a/ch01python/016functions.ipynb.py +++ b/ch01python/016functions.ipynb.py @@ -12,10 +12,10 @@ # --- # %% [markdown] -# ## Functions +# # Functions # %% [markdown] -# ### Definition +# ## Definition # %% [markdown] # @@ -28,100 +28,86 @@ 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"): - return f"Very good, {name}" +def jeeves(name="Sir"): + return "Very good, " + name # %% 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: - # %% -def jeeves(greeting="Very good", name="Sir"): - return f"{greeting}, {name}" +def product(x=5, y=7): + return x * y # %% -jeeves() - -# %% -jeeves("Hello") - -# %% -jeeves(name = "John") +product(9) # %% -jeeves(greeting="Suits you") +product(y=11) # %% -jeeves("Hello", "Producer") +product() # %% [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 +# # %% def double_inplace(vec): vec[:] = [element * 2 for element in vec] -z = list(range(4)) + +z = [1, 2, 3, 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. # -# 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: - -# %% -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,7 +121,21 @@ def double(vec): # %% [markdown] -# ### Early Return +# ## Early Return + +# %% +def isbigger(x, limit=20): + if x > limit: + return True + print("Got here") + return False + + +isbigger(25, 15) + +# %% +isbigger(40, 15) + # %% [markdown] # @@ -144,34 +144,30 @@ def double(vec): # # -# %% [markdown] -# 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 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 +# If a vector is supplied to a function with a `*`, its elements # are used to fill each of a function's arguments. # # @@ -179,13 +175,15 @@ def extend(to, vec, pad): # %% def arrow(before, after): - return f"{before} -> {after}" + return str(before) + " -> " + str(after) -arrow(1, 3) + +print(arrow(1, 3)) # %% x = [1, -1] -arrow(*x) + +print(arrow(*x)) # %% [markdown] # @@ -198,17 +196,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,37 +217,23 @@ 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") + print(key + " -> " + value) -# %% [markdown] -# These different approaches can be mixed: - -# %% -def somefunc(a, b, *args, **kwargs): - print("A:", a) - print("B:", b) - print("args:", args) - print("keyword args", kwargs) - - -# %% -somefunc(1, 2, 3, 4, 5, fish="Haddock") +arrowify(neutron="n", proton="p", electron="e") diff --git a/ch01python/017using_functions.ipynb.py b/ch01python/017using_functions.ipynb.py index 2755d823..585c2462 100644 --- a/ch01python/017using_functions.ipynb.py +++ b/ch01python/017using_functions.ipynb.py @@ -12,39 +12,25 @@ # --- # %% [markdown] -# ## Using functions +# # 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. +# 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 +# ## 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: +# 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: +# 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: +# 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") @@ -62,108 +48,37 @@ print(total_length) # %% [markdown] -# ### Using methods +# ## Getting help on functions # -# 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. -# -# Objects can have both properties and methods: - -# %% -z = 1 + 5j - -# %% -z.real - -# %% -z.conjugate() - -# %% -z.conjugate - -# %% [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 +# 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 an alternative way of displaying the documentation for a function is to write the function -# names followed by a question mark character `?` +# In Jupyter notebooks an alternative way of displaying the documentation for a function is to write the function names followed by a question mark character `?` # %% # max? # %% [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. +# ## Positional and keyword arguments and default values # -# 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] -# ### 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 +# 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 +# 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_) +# 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) @@ -178,22 +93,17 @@ 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 +# 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 +# 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 +# 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) @@ -205,106 +115,19 @@ round(number=pi, ndigits=None) # %% [markdown] -# ### Functions are just a type of object! +# ## Functions are objects # -# 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 +# 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 # %% -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] -# ### Operators -# -# 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. -# -# 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". -# -# 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"! -# -# 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: +my_print = print +my_print # %% -print(2 + 3 * 4) +help(my_print) # %% -print((2 + 3) * 4) +my_print("Hello") # %% [markdown] -# *Supplementary material*: [Python operator precedence](https://docs.python.org/3/reference/expressions.html#operator-precedence). +# 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..33188941 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] +# 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. +# ## 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. +# A `tuple` is an immutable sequence: +# +# +# # %% -x = 0, -type(x) +my_tuple = ("Hello", "World") # %% -my_tuple = ("Hello", "World") -my_tuple[0] = "Goodbye" +my_tuple # %% -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%20%3D%20%20%22James%20Philip%20John%20Hetherington%22.split%28%22%20%22%29%0A%0Aname%5B0%5D%20%3D%20%22Dr%22%0Aname%5B1%3A3%5D%20%3D%20%5B%22Griffiths-%22%5D%0Aname.append%28%22PhD%22%29%0A%0Aname%20%3D%20%22Bilbo%20Baggins%22&cumulative=false&curInstr=0&heapPrimitives=true&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false) 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,38 @@ 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. -# -# *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). +# 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. # %% [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" - -# %% -"Hello" is "Hello" - -# %% [markdown] -# This can be useful in understanding problems like the one above: - # %% -x = range(3) -y = x -z = x[:] +my3numbers = list(range(3)) +print(my3numbers) # %% -x == y +[0, 1, 2] == my3numbers # %% -x is y +[0, 1, 2] is my3numbers -# %% -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..f5ee1b14 100644 --- a/ch01python/028dictionaries.ipynb.py +++ b/ch01python/028dictionaries.ipynb.py @@ -12,10 +12,10 @@ # --- # %% [markdown] -# ## Dictionaries +# # Dictionaries # %% [markdown] -# ### The Python Dictionary +# ## The Python Dictionary # %% [markdown] # Python supports a container type called a dictionary. @@ -28,154 +28,131 @@ # %% names = "Martin Luther King".split(" ") - -# %% names[1] # %% [markdown] # In a dictionary, we look up an element using **another object of our choice**: # %% -chapman = {"name": "Graham", "age": 48, - "Jobs": ["Comedian", "Writer"] } - -# %% -chapman +me = {"name": "James", "age": 39, "Jobs": ["Programmer", "Teacher"]} # %% -chapman['Jobs'] +print(me) # %% -chapman['age'] +print(me["Jobs"]) # %% -type(chapman) +print(type(me)) # %% [markdown] -# ### Keys and Values +# ## Keys and Values # %% [markdown] # The things we can use to look up with are called **keys**: # %% -chapman.keys() +me.keys() # %% [markdown] # The things we can look up are called **values**: # %% -chapman.values() +me.values() # %% [markdown] # When we test for containment on a `dict` we test on the **keys**: # %% -'Jobs' in chapman +"Jobs" in me # %% -'Graham' in chapman +"James" in me # %% -'Graham' in chapman.values() +"James" in me.values() # %% [markdown] -# ### Immutable Keys Only +# ## 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 = { - ("Lamb", "Mint"): True, - ("Bacon", "Chocolate"): False - } +good_match = {("Lamb", "Mint"): True, ("Bacon", "Chocolate"): False} # %% [markdown] # but: # %% -illegal = { - ["Lamb", "Mint"]: True, - ["Bacon", "Chocolate"]: False - } +illegal = {[1, 2]: 3} # %% [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 I think it's really interesting! # %% [markdown] -# ### No guarantee of order (before Python 3.7) +# ## No guarantee of order # %% [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). +# 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 +# ## Sets # %% [markdown] # A set is a `list` which cannot contain the same element twice. -# We make one by calling `set()` on any sequence, e.g. a list or string. # %% -name = "Graham Chapman" -unique_letters = set(name) +university = "University College London" +unique_letters = set(university) # %% unique_letters -# %% [markdown] -# Or by defining a literal like a dictionary, but without the colons: - -# %% -primes_below_ten = { 2, 3, 5, 7} - # %% -type(unique_letters) +print("".join(unique_letters)) # %% -type(primes_below_ten) - -# %% -unique_letters +"".join(["a", "b", "c"]) # %% [markdown] -# This will be easier to read if we turn the set of letters back into a string, with `join`: +# It has no particular order, but is really useful for checking or storing **unique** values. # %% -"".join(unique_letters) +alist = [1, 2, 3] +is_unique = len(set(alist)) == len(alist) +print(is_unique) # %% [markdown] -# A set has no particular order, but is really useful for checking or storing **unique** values. +# ## Safe Lookup -# %% [markdown] -# Set operations work as in mathematics: +# %% +x = {"a": 1, "b": 2} # %% -x = set("Hello") -y = set("Goodbye") +x["a"] # %% -x & y # Intersection +x["fish"] # %% -x | y # Union +x.get("a") # %% -y - x # y intersection with complement of x: letters in Goodbye but not in Hello +x.get("fish") -# %% [markdown] -# 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. +# %% +x.get("fish", "tuna") == "tuna" diff --git a/ch01python/029structures.ipynb.py b/ch01python/029structures.ipynb.py index 8439ed97..34937c41 100644 --- a/ch01python/029structures.ipynb.py +++ b/ch01python/029structures.ipynb.py @@ -12,34 +12,32 @@ # --- # %% [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 # data. Later in the course, we'll see how we can define our own types, with their own attributes, properties, and methods. But probably the most common approach is to use nested structures of lists, dictionaries, and sets to model our data. For example, an address might be modelled as a dictionary with appropriately named fields: # %% -UCL = { - 'City': 'London', - 'Street': 'Gower Street', - 'Postcode': 'WC1E 6BT' -} +UCL = {'City': 'London', + 'Street': 'Gower Street', + 'Postcode': 'WC1E 6BT'} # %% -Chapman = { +MyHouse = { 'City': 'London', - 'Street': 'Southwood ln', - 'Postcode': 'N6 5TB' + 'Street': 'Waterson Street', + 'Postcode': 'E2 8HH' } # %% [markdown] # A collection of people's addresses is then a list of dictionaries: # %% -addresses = [UCL, Chapman] +addresses = [UCL, MyHouse] # %% addresses @@ -48,10 +46,10 @@ # A more complicated data structure, for example for a census database, might have a list of residents or employees at each address: # %% -UCL['people'] = ['Jeremy','Leonard', 'James', 'Henry'] +UCL["People"] = ["Clare", "James", "Owain"] # %% -Chapman['people'] = ['Graham', 'David'] +MyHouse["People"] = ["Sue", "James"] # %% addresses @@ -63,46 +61,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. James 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. Sue 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": ["Me", "Sue"]}, + {"name": "Edinburgh", "capacity": 1, "residents": ["Dave"]}, + {"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..2c8d8f60 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,29 @@ # 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 - +x = -3 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 - -if x < 0: - print(f"{x} is negative") - -# %% [markdown] -# The first time through, the print statement never happened. + print(x, "is negative") + print("This is controlled") +print("Always run this") # %% [markdown] -# The **controlled** statements are indented. Once we remove the indent, the statements will once again happen regardless. +# 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] -# ### 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 +67,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 = "dlgkhdglkhgkjhdkjgh" -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,91 +90,78 @@ 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" > "King's" # %% [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 - -# %% [markdown] -# Any statement that evaluates to `True` or `False` can be used to control an `if` Statement. +0 == False # %% [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") +if not (x > 0 and type(x) == int): + print(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("") # %% -bool("Graham") +bool("James") # %% bool([]) # %% -bool(['a']) +bool(["a"]) # %% bool({}) # %% -bool({'name': 'Graham'}) +bool({"name": "James"}) # %% bool(0) @@ -194,6 +169,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 ==: @@ -201,10 +179,10 @@ [] == False # %% -bool([]) == False +bool([]) == bool(False) # %% [markdown] -# ### Indentation +# ## Indentation # %% [markdown] # In Python, indentation is semantically significant. @@ -215,30 +193,17 @@ # In the notebook, and most good editors, when you press ``, you get four spaces. # -# %% [markdown] -# No indentation when it is expected, results in an error: - -# %% -x = 2 - -# %% -if x > 0: -print(x) - -# %% [markdown] -# but: - # %% if x > 0: 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,7 +211,6 @@ # %% if x > 0: # print x - print("Hello") # %% [markdown] @@ -260,7 +224,5 @@ # %% if x > 0: - # print x pass - print("Hello") diff --git a/ch01python/035looping.ipynb.py b/ch01python/035looping.ipynb.py index df9b0fa8..cdf95fb0 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,33 +48,44 @@ repetition = 3 else: repetition = 1 - sarcasm.append(letter * repetition) "".join(sarcasm) # %% [markdown] -# The above is a little puzzle, work through it to understand why it does what it does. +# 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} +founded = {"James": 1976, "UCL": 1826, "Cambridge": 1209} current_year = now.year -for thing in founded: - print(thing, " is ", current_year - founded[thing], "years old.") +for x in founded: + print(x, "is", current_year - founded[x], "years old.") + +# %% +thing = "UCL" + +founded[thing] + +# %% +founded + +# %% +founded.items() # %% [markdown] -# ### Unpacking and Iteration +# ## Unpacking and Iteration # %% [markdown] # @@ -84,7 +95,7 @@ # # %% -triples = [ +triples=[ [4, 11, 15], [39, 4, 18] ] @@ -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(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(name, "is", current_year - founded[name], "years old.") # %% [markdown] -# ### Break, Continue +# ## Break, Continue # %% [markdown] # @@ -135,18 +149,17 @@ # %% 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 you don't `break`, but I've never found that useful. +# These aren't useful that often, but are worth knowing about. There's also an optional `else` clause on loops, executed only if if the loop gets through all it's iterations without a `break`, but I've never found that useful. # %% [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..71e20e85 100644 --- a/ch01python/036MazeSolution2.ipynb.py +++ b/ch01python/036MazeSolution2.ipynb.py @@ -56,12 +56,22 @@ # We can count the occupants and capacity like this: # %% -capacity = 0 -occupancy = 0 -for name, room in house.items(): - capacity += room['capacity'] - occupancy += len(room['people']) -print(f"House can fit {capacity} people, and currently has: {occupancy}.") +for room_name in house: + print(room_name) + +# %% +house + +# %% +house.values() + +# %% +running_total = 0 + +for room_data in house.values(): + running_total += len(room_data["people"]) + +print(running_total) # %% [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). +# As a side note, note how we included the values of `running_total` 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..2f4951c2 100644 --- a/ch01python/037comprehensions.ipynb.py +++ b/ch01python/037comprehensions.ipynb.py @@ -12,36 +12,33 @@ # --- # %% [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)] +[2 ** x for x in range(10)] # %% [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: @@ -53,11 +50,10 @@ # Consider the following, and make sure you understand why it works: # %% -"".join([letter for letter in "Eric Idle" - if letter.lower() not in 'aeiou']) +"".join([letter for letter in "James Hetherington" if letter.lower() not in "aeiou"]) # %% [markdown] -# ### Comprehensions versus building lists with `append`: +# ## Comprehensions versus building lists with `append`: # %% [markdown] # This code: @@ -67,10 +63,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 +75,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 +105,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 +129,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 +149,38 @@ 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: # -# analysed_data = 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 +# analysed_data = list(map(analyse, data)) # %% [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..be8ede45 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,28 +48,39 @@ 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] -# **Supplementary Materials**: Review the [list of standard library modules](https://docs.python.org/library/). +# **Supplementary Materials**: Review the list of standard library modules: https://docs.python.org/3/library/ # %% [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: 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,18 +88,15 @@ # 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)`. # Things can be imported *from* modules to become part of the current module: -# %% -import math -math.sin(math.pi) - # %% from math import sin + sin(math.pi) # %% [markdown] @@ -98,20 +106,30 @@ # It *is* possible to import **everything** from a module, but you risk name clashes. # %% +pi = "pie" + + +def sin(x): + print("eat " + x) + + from math import * + 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) # %% pi = 3 from math import pi as realpi + print(sin(pi), sin(realpi))