2. Python Variables, Including Lists and Tuples, and Arrays from Package Numpy#

Estimated time to complete: 90 to 120 minutes.

2.1. Foreword#

With this and all future units, start by creating your own Jupyter notebook (named “unit02.ipynb” etc.) perhaps copying relevant cells from this notebook and then adding your work.

If you also wish to start practicing with the Spyder IDE, then in addition use it to create a Python code file “unit02.py” with that code, and run the commands there too. Later you might find it preferable to develop code in Spyder and then copy the working code and notes into anotebook for final presentation — Spyder has better tools for “debugging”.

2.2. Numerical variables#

The first step beyond using Python merely as a calculator is storing value in variables, for reuse later in more elaborate calculations. For example, to find both roots of a quadratic equation

\[ax^2 + bx + c = 0\]

we want the values of each coefficient and are going to use each of them twice, which we might want to do without typing in each coefficient twice over.

Example#

We can solve the specific equation

\[2x^2 - 8x + 6 = 0\]

using the quadratic formula. But first we need to get the square root function:

from math import sqrt

Then the rest looks almost like normal mathematical notation:

a = 2
b = -10
c = 8
root0 = (-b - sqrt(b**2 - 4 * a * c))/(2 * a)
root1 = (-b + sqrt(b**2 - 4 * a * c))/(2 * a)

(Aside: why did I number the roots 0 and 1 instead of 1 and 2? The answer is coming up soon.)

Where are the results? They have been stored in variables rather than printed out, so to see them, use the print function:

print('The smaller root is', root0, 'and the larger root is', root1)
The smaller root is 1.0 and the larger root is 4.0

Aside: This is the first mention of the function print(), for output to the screen or to files. You can probably learn enough about its usage from examples in this and subsequent units of the course, but for more information see also these notes on formatted output and some text string manipulation

A short-cut for printing the value of a variable is to simply enter its name:

root0
1.0

You can also do this for multiple variables, as with:

root0, root1
(1.0, 4.0)

Note that the output is parenthesized: this, as will be explained below, is a tuple

2.3. Text variables#

Other information can be put into variables, such as strings of text:

LastName = 'LeMesurier'
FirstName = "Brenton"
print('Hello, my name is', FirstName, LastName)
Hello, my name is Brenton LeMesurier

Note that either ‘single quotes’ or “double quotes” can be use to surround text, but one must be consistent within each piece of text.

2.4. Lists#

Python has several ways of grouping together information into one variable. We first look at lists, which can collect all kinds of information together:

coefficients = [2, -10, 8]
name = ["LeMesurier", "Brenton"]
phone = [9535917]
print(coefficients, name, phone)
[2, -10, 8] ['LeMesurier', 'Brenton'] [9535917]

Lists can be combined by “addition”, which is concatenation:

name + phone
['LeMesurier', 'Brenton', 9535917]

Individual entries (“elements”) can be extracted from lists; note that Python always counts from 0, and indices go in [brackets], not (parentheses) or {braces}:

LastName = name[0]
FirstName = name[1]
print(FirstName, LastName)
Brenton LeMesurier

and we can modify list elements this way too:

name[1] = 'John'
print(name[1])
print(name)
John
['LeMesurier', 'John']

We can use the list of coefficients to specify the quadratic, and store both roots in a new list.

But let’s shorten the name first, by making “q” a synonym for “coefficients”:

q = coefficients
print('The list of coefficients is', q)
The list of coefficients is [2, -10, 8]
roots = [(-q[1] - sqrt(q[1]**2 - 4 * q[0] * q[2]))/(2 * q[0]),
         (-q[1] + sqrt(q[1]**2 - 4 * q[0] * q[2]))/(2 * q[0])]
print('The list of roots is', roots)
print('The individual roots are', roots[0], 'and', roots[1])
The list of roots is [1.0, 4.0]
The individual roots are 1.0 and 4.0

See now why I enumerated the roots from 0 previously?

For readability, you might want to “unpack” the coefficients by copying into individual variables, and then use the more familiar formulas above:

a = q[0]
b = q[1]
c = q[2]
roots = [(-b - sqrt(b**2 - 4 * a * c))/(2 * a),
         (-b + sqrt(b**2 - 4 * a * c))/(2 * a)]
print('The list of roots is again', roots)
The list of roots is again [1.0, 4.0]

The equals sign = creates synonyms for lists; not copies#

Note that it says above that the statement q = coefficients makes q is a synonym for coefficients, not a copy of its values. To see this, note that when we make a change to q it also applies to coefficients (and vice versa):

print("q is", q)
print("coefficients is", coefficients)
q[0] = 4
print("q is now", q)
print("coefficients is now", coefficients)
q is [2, -10, 8]
coefficients is [2, -10, 8]
q is now [4, -10, 8]
coefficients is now [4, -10, 8]

To avoid confusion below, let’s change the value back:

coefficients[0] = 2

Looking at the end of a list, with negative indices#

Python allows you to count backwards from the end of a list, by using negative indices:

  • index -1 refers to the last element

  • index -k refers to the element k from the end.

For example:

digits = [1, 2, 3, 4, 5, 6, 7, 8, 9]
print('The last digit is', digits[-1])
print('The third to last digit is', digits[-3])
The last digit is 9
The third to last digit is 7

This also works with the arrays and tuples introduced below.

2.5. Tuples#

One other useful kind of Python collection is a tuple, which is a lot like a list except that it is immutable: you cannot change individual elements. Tuples are denoted by surrounding the elements with parentheses “(…)” in place of the brackets “[…]” used with lists:

q_tuple = (2, -10, 8)
q_tuple
(2, -10, 8)
print(q_tuple)
(2, -10, 8)
q_tuple[2]
8

Actually, we have seen tuples before without the name being mentioned: when a list of expressions is put on one line separated by commas, the result is a tuple. This is because when creating a tuple, the surrounding parentheses can usually be omitted:

name = "LeMesurier", "Brenton"
print(name)
('LeMesurier', 'Brenton')

Tuples can be concatenated by “addition”, as for lists:

name_and_contact_info = name + ('843-953-5917', 'RSS 344')
print(name_and_contact_info)
('LeMesurier', 'Brenton', '843-953-5917', 'RSS 344')

2.6. Naming rules for variables#

There are some rules limiting which names can be used for variables:

  • The first character must be a letter.

  • All characters must be “alphanumeric”: only letters of digits.

  • However, the underscore “_” (typed with “shift dash”) is an honorary letter: it can be used where you are tempted to have a space.

Note well: no dashes “-” or spaces, or any other punctuation.

When you are tempted to use a space in a name, such as when the name is a descriptive phrase, it is recommended to either use an underscore or to capitalize the first leter of each new word. (I have illustated both options above.)

Exercise A#

It will soon be convenient to group the input data to and output values from a calculation in tuples.

Do this by rewriting the quadratic solving exercise using a tuple “coefficients” containing the coefficients (a, b, c) of a quadratic \(ax^2 + bx + c\) and putting the roots into a tuple named “roots”.

Break this up into three steps, each in its own code cell (an organizational pattern that will be important later):

  1. Input: create the input tuple.

  2. Calculation: use this tuple to compute the tuple of roots.

  3. Output: print the roots.

This is only a slight variation of what is done above with lists, but the difference will be important later.

The immutability of tuples (and also of text strings)#

As mentioned above, a major difference from lists is that tuples are immutable; their contents cannot be changed: I cannot change the lead cofficient of the quadratic above with

q_tuple[0] = 4
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Input In [23], in <cell line: 1>()
----> 1 q_tuple[0] = 4

TypeError: 'tuple' object does not support item assignment

This difference between mutable objects like lists and immutable ones like tuples comes up in multiple places in Python. The one other case that we are most likely to encounter in this course is strings of text, which are in some sense “tuples of characters”. For example, the characters of a string can be addressed with indices, and concatenated:

language = "Python"
print(f"The initial letter of '{language}' is '{language[0]}'")
print(f"The first three letters are '{language[0:3]}'")
languageversion = language + ' 3'
print(f"We are using version '{languageversion}'")
The initial letter of 'Python' is 'P'
The first three letters are 'Pyt'
We are using version 'Python 3'

Aside: Here a new feature of printing and string manipulation is used, “f-string formatting” (introduced in Python version 3.6). For details, see the notes on formatted output and some text string manipulation mentioned above.

Also, as with tuples, one cannot change the entries of a string via indexing; we cannot “lowercase” that name with

language[0] = "p"
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Input In [25], in <cell line: 1>()
----> 1 language[0] = "p"

TypeError: 'str' object does not support item assignment

2.7. Numpy arrays: for vectors, matrices, and beyond#

Many mathematical calculations involve vectors, matrices and other arrays of numbers. At first glance, Python lists and tuples look like vectors, but as seen above, “addition” of such objects does not do what you want with vectors.

Thus we need a type of object that is specifically an array of numbers of the same type that can be manipulatd like a vector or matrix. There is not a suitable entity for this in the core Python language, but Python has a method to add features using modules and packages, and the most important one for us is Numpy: this provides for suitable numerical arrays through objects of type ndarray, and provides tools for working with them, like the function array() for creating arrays from lists. (Numpy also provides a large collection of other tools for numerical computing, as we will see later.)

Importing modules#

One way to make Numpy available is to import it with just

import numpy

Then the function array is accessed by its “fully-qualified name” numpy.array, and we can create an ndarray that serves for storing a vector:

u = numpy.array([1, 2, 3])
u
array([1, 2, 3])
print(u)
[1 2 3]

Note: As you might have noticed above, displaying the value of a variable by simply typing its name describes it in more detail than the print function, with a description that could be used to create the object. Thus I will sometimes use both display methods below, as a reminder of the syntax and semantics of Numpy arrays.

As seen above, if we just want that one function, we can import it specifically with the command

from numpy import array

and then it can be referered to by its short name alone:

v = array([4, 5, 6, 7])
print(v)
[4 5 6 7]

Notes#

  1. Actually, Python’s core collection of resources does provide another kind of object called an array, but we will never use that in this course, and I advise you to avoid it: the Numpy ndarray type of array is far better for what we want to do! The name “ndarray” refers to the possibility of creating n-dimensional arrays — for example, to store matrices — which is one of several important advantages.

  2. There is another add-on package Pylab, which contains most of Numpy plus some stuff for graphics (from package Matplotlib, which we will meet later, in Plotting Graphs with Matplotlib.) That is intended to reproduce a Matlab-like environment, especially when used in Spyder, which is deliberately Matlab-like. So you could instead use from pylab import *, and that will sometimes be more convenient. However, when you search for documentation, you will find it by searching for numpy, not for pylab. For example the full name for function array is numpy.array and once we import Numpy with import numpy we can get help on that with the command help(numpy.array).

Beware: this help information is sometimes very lengthy, and “expert-friendly” rather than “beginner-friendly”.
Thus, now is a good time to learn that when the the up-array and down-array keys get to the top or bottom of a cell in a notebook, they keep moving to the previous or next cell, skipping past the output of any code cell.

help(numpy.array)
Help on built-in function array in module numpy:

array(...)
    array(object, dtype=None, *, copy=True, order='K', subok=False, ndmin=0,
          like=None)
    
    Create an array.
    
    Parameters
    ----------
    object : array_like
        An array, any object exposing the array interface, an object whose
        __array__ method returns an array, or any (nested) sequence.
    dtype : data-type, optional
        The desired data-type for the array.  If not given, then the type will
        be determined as the minimum type required to hold the objects in the
        sequence.
    copy : bool, optional
        If true (default), then the object is copied.  Otherwise, a copy will
        only be made if __array__ returns a copy, if obj is a nested sequence,
        or if a copy is needed to satisfy any of the other requirements
        (`dtype`, `order`, etc.).
    order : {'K', 'A', 'C', 'F'}, optional
        Specify the memory layout of the array. If object is not an array, the
        newly created array will be in C order (row major) unless 'F' is
        specified, in which case it will be in Fortran order (column major).
        If object is an array the following holds.
    
        ===== ========= ===================================================
        order  no copy                     copy=True
        ===== ========= ===================================================
        'K'   unchanged F & C order preserved, otherwise most similar order
        'A'   unchanged F order if input is F and not C, otherwise C order
        'C'   C order   C order
        'F'   F order   F order
        ===== ========= ===================================================
    
        When ``copy=False`` and a copy is made for other reasons, the result is
        the same as if ``copy=True``, with some exceptions for 'A', see the
        Notes section. The default order is 'K'.
    subok : bool, optional
        If True, then sub-classes will be passed-through, otherwise
        the returned array will be forced to be a base-class array (default).
    ndmin : int, optional
        Specifies the minimum number of dimensions that the resulting
        array should have.  Ones will be pre-pended to the shape as
        needed to meet this requirement.
    like : array_like
        Reference object to allow the creation of arrays which are not
        NumPy arrays. If an array-like passed in as ``like`` supports
        the ``__array_function__`` protocol, the result will be defined
        by it. In this case, it ensures the creation of an array object
        compatible with that passed in via this argument.
    
        .. versionadded:: 1.20.0
    
    Returns
    -------
    out : ndarray
        An array object satisfying the specified requirements.
    
    See Also
    --------
    empty_like : Return an empty array with shape and type of input.
    ones_like : Return an array of ones with shape and type of input.
    zeros_like : Return an array of zeros with shape and type of input.
    full_like : Return a new array with shape of input filled with value.
    empty : Return a new uninitialized array.
    ones : Return a new array setting values to one.
    zeros : Return a new array setting values to zero.
    full : Return a new array of given shape filled with value.
    
    
    Notes
    -----
    When order is 'A' and `object` is an array in neither 'C' nor 'F' order,
    and a copy is forced by a change in dtype, then the order of the result is
    not necessarily 'C' as expected. This is likely a bug.
    
    Examples
    --------
    >>> np.array([1, 2, 3])
    array([1, 2, 3])
    
    Upcasting:
    
    >>> np.array([1, 2, 3.0])
    array([ 1.,  2.,  3.])
    
    More than one dimension:
    
    >>> np.array([[1, 2], [3, 4]])
    array([[1, 2],
           [3, 4]])
    
    Minimum dimensions 2:
    
    >>> np.array([1, 2, 3], ndmin=2)
    array([[1, 2, 3]])
    
    Type provided:
    
    >>> np.array([1, 2, 3], dtype=complex)
    array([ 1.+0.j,  2.+0.j,  3.+0.j])
    
    Data-type consisting of more than one element:
    
    >>> x = np.array([(1,2),(3,4)],dtype=[('a','<i4'),('b','<i4')])
    >>> x['a']
    array([1, 3])
    
    Creating an array from sub-classes:
    
    >>> np.array(np.mat('1 2; 3 4'))
    array([[1, 2],
           [3, 4]])
    
    >>> np.array(np.mat('1 2; 3 4'), subok=True)
    matrix([[1, 2],
            [3, 4]])

The function help can also give information about a type of object, such as an ndarray. Note that ndarray is referred to as a class; if that jargon is unfamiliar, you can safely ignore it for now, but if curious you can look at the brief notes on Classes, Objects, Attributes, Methods: Very Basic Object-Oriented Programming in Python.

Beware: the help information for numpy.ndarray is even more long-winded, and tells you far more about numpy arrays than you need to know for now! So the command is commented out: remove the ‘#’ to run it.

#help(numpy.ndarray)

Creating arrays (from lists and otherwise)#

Numpy arrays (more pedantically, objects of type ndarray) are in some ways quite similar to lists, and as seen above, one way to create an array is to convert a list:

list0 = [1, 2, 3]
list1 = [4, 5, 6]
array0 = array(list0)
array1 = array(list1)
list0
[1, 2, 3]
array0
array([1, 2, 3])
print(list0)
[1, 2, 3]
print(array0)
[1 2 3]

We can skip the intermediate step of creating lists and instead create arrays directly:

array0 = array([1, 2, 3])
array1 = array([4, 5, 6])

Printing makes these seem very similar …

print('list0 =', list0)
print('array0 =', array0)
list0 = [1, 2, 3]
array0 = [1 2 3]

… and we can extract elements in the same way:

print('The first element of list0 is', list0[0])
print('The last element of array1 is', array1[-1])
The first element of list0 is 1
The last element of array1 is 6
list0
[1, 2, 3]
array0
array([1, 2, 3])

Numpy arrays understand vector arithmetic#

Addition and other arithmetic reveal some important differences:

print(list0 + list1)
[1, 2, 3, 4, 5, 6]
print(array0 + array1)
[5 7 9]
print(2 * list0)
[1, 2, 3, 1, 2, 3]
print(2 * array0)
[2 4 6]

Note what multiplication does to lists!

Describing matrices as 2D arrays, or as “arrays of arrays of numbers”#

A list can have other lists as its elements, and likewise an array can be described as having other arrays as its elements, so that a matrix can be described as a succession of rows. First, a list of lists can be created:

listoflists = [list0, list1]
print(listoflists)
[[1, 2, 3], [4, 5, 6]]
listoflists[1][-1]
6

Then this can be converted to a two dimensional array:

matrix = array(listoflists)
print(matrix)
[[1 2 3]
 [4 5 6]]
matrix*3
array([[ 3,  6,  9],
       [12, 15, 18]])

We can also combine arrays into new arrays directly:

anothermatrix = array([array1, array0])
anothermatrix
array([[4, 5, 6],
       [1, 2, 3]])
print(anothermatrix)
[[4 5 6]
 [1 2 3]]

Note that we must use the notation array([…]) to do this; without the function array() we would get a list of arrays, which is a different animal, and much less fun for doing mathematics with:

listofarrays = [array1, array0]
listofarrays*3
[array([4, 5, 6]),
 array([1, 2, 3]),
 array([4, 5, 6]),
 array([1, 2, 3]),
 array([4, 5, 6]),
 array([1, 2, 3])]

Referring to array elements with double indices, or with successive single indices#

The elements of a multi-dimensional array can be referred to with multiple indices:

matrix[1,2]
6

but you can also use a single index to extract an “element” that is a row:

matrix[1]
array([4, 5, 6])

and you can use indices successively, to specify first a row and then an element of that row:

matrix[1][2]
6

This ability to manipulate rows of a matrix can be useful for linear algebra. For example, in row reduction we might want to subtract four times the first row from the second row, and this is done with:

print('Before the row operation, the matrix is:')
print(matrix)
matrix[1] -= 4 * matrix[0]  # Remember, this is short-hand for matrix[1] = matrix[1] - 4 * matrix[0]
print('After the row operation, it is:')
print(matrix)
Before the row operation, the matrix is:
[[1 2 3]
 [4 5 6]]
After the row operation, it is:
[[ 1  2  3]
 [ 0 -3 -6]]

Note well the effect of Python indexing starting at zero: the indices used with a vector or matrix are all one less than you might expect based on the notation seen in a linear algebra course.

Higher dimensional arrays#

Arrays with three or more indices are possible, though we will not see much of them in this course:

arrays_now_in_3D = array([matrix, anothermatrix])
arrays_now_in_3D
array([[[ 1,  2,  3],
        [ 0, -3, -6]],

       [[ 4,  5,  6],
        [ 1,  2,  3]]])
print(arrays_now_in_3D)
[[[ 1  2  3]
  [ 0 -3 -6]]

 [[ 4  5  6]
  [ 1  2  3]]]

Exercise B#

Create two arrays, containing the matrices $\( A = \left[ \begin{array}{cc} 2 & 3 \\ 1 & 4 \end{array} \right], \qquad B = \left[ \begin{array}{cc} 3 & 0 \\ 2 & 1 \end{array} \right] \)$ Then look at what is given by the formula

C = A * B

and what you get instead with the strange notation

D = A @ B

Explain in words what is going on in each case!

2.8. Submitting your work#

with notes on optionally making HTML and PDF translations of a notebook.

The main item to submit is a notebook; suggested name “pythonVariables.ipynb”.

HTML and PDF translations#

If you wish to also produce a version that is universally readable (for example, within OAKS) and that can be uploaded to an OAKS Dropbox, a two-step procedure is needed, giving a PDF file:

  1. In JupyterLab, produce an HTML translation with menu selection
    File > Export Notebook As ... > Export Notebook to HTML
    Unfortunately these HTML files get mangled if uploaded to an OAKS Dropbox, so the next step is:

  2. Open that HTML file in a web-bowser, and use that to produce a PDF translation; for example by “printing to PDF”.

Note: There is a JupyterLab command
File > Export Notebook As ... > Export Notebook to PDF
but unfortunately it does not work in some versions of JupyterLab.

If you are familiar with LaTeX, there is a way to get a somewhat nicer PDF file via LaTeX:

  1. Use the command File > Export Notebook As ... > Export Notebook to LaTeX

  2. Use your favorite LaTeX software to process that file into PDF.