**Note**: if you are a matlab user, you may want to look at

- This great cheat sheet showing common matlab commands and their python counterparts
- Numpy for matlab users

In python, there are a few ways to manage collections of information:

- A
`list`

is built-in to the language, and can contain any type - A
`tuple`

is also built-in to the language, and is similar to a list, but is immutable. - A numpy array is provided by the
`numpy`

package in python and is useful for holding collections of numbers. - A dictionary provides a map - typically between strings and other things like strings, numbers, arrays, etc., and can be nested.

A *list* in python is enclosed by square brackets, and can contain any types (including other lists):

In [1]:

```
# A list of integers:
x = [1,2,3,4]
# A list of strings:
fruits = ['apple','banana','orange']
# A list of many things, including other lists!
my_list = [ 2, 'a string', 5, 3.14, x, fruits ]
print(my_list)
print(my_list.index(x)) # find where x appears (4)
print(my_list[2]) # 5
```

[2, 'a string', 5, 3.14, [1, 2, 3, 4], ['apple', 'banana', 'orange']] 4 5

** Caution**: copying arrays in python is unique:

In [2]:

```
a = [1,2,3]
b = a
```

makes `b`

alias to `a`

. This means that

In [3]:

```
b[1]=5
```

also changes `a[1]`

to 5:

In [4]:

```
print(a,b)
```

[1, 5, 3] [1, 5, 3]

To make a full copy of an array, use the `copy`

method:

In [5]:

```
b = a # b and a are the same array
c = a.copy() # c and a are different arrays with the same contents
c[2] = 19
print(a,b,c)
```

[1, 5, 3] [1, 5, 3] [1, 5, 19]

Frequently we want to perform operations on lists

Method | Description | Example |
---|---|---|

`len` |
Returns the number of entries in the list | `len(my_list)` |

`append(entry)` |
Append an entry to the list | `my_list.append( 'another entry' )` |

`insert(index,entry)` |
Insert an entry in the list | `my_list.insert(2,10)` |

`extend(list2)` |
append a list | `my_list.append( ('a','b',x) )` |

`+` |
append a list (same as `extend` ) |
`my_list + my_list` |

`remove(entry)` |
Append an entry to the list | `my_list.remove( x )` |

`sort()` |
sort the list - only works when the list is homogeneous | `x.sort()` |

`reverse()` |
reverses the entries in the list | `my_list.reverse()` |

`pop(index)` |
return and remove the element in the list at `index` |
`my_list.pop(3)` |

`count(entry)` |
return the number of occurences of `entry` in the list |
`my_list.count(2)` |

`index(entry)` |
return the index at which the first occurence of `entry` occurs |
`my_list.index(x)` |

You can also use the keyword `in`

with a list:

In [6]:

```
fruits = ['apple','pear','banana','payapa']
if 'banana' in fruits:
print('Yes!')
else:
print('No!')
```

Yes!

Indexing a list is done with the `[]`

operator, and is 0-based (0 indicates the first entry in the list). For example, given:

In [7]:

```
fruits = ['apple','banana','grapefruit','orange']
```

the following table shows various indexing operations:

Operation | Result | Description |
---|---|---|

`fruits[1]` |
`banana` |
Accesses the second element in the list |

`fruits[-1]` |
`orange` |
Accesses the last element in the list |

`fruits[-2]` |
`grapefruit` |
Accesses the second to last element in the list |

`fruits[:2]` |
`[apple,banana]` |
The first two elements in the list |

`fruits[1:3]` |
`[banana,grapefruit]` |
The second and third elements in the list |

The `:`

operator allows us to perform *slicing*, accessing a subset of the entries in a list.

You can also use slicing to replace elements of a list. For example:

In [8]:

```
x = [1,2,3,4]
y = x[1:3] # note that this doesn't make a copy! y is refering to part of x
y.reverse()
print("y: ", y)
x[0:2] = y
print("x: ", x)
```

y: [3, 2] x: [3, 2, 3, 4]

There are a few ways to loop over lists. This is perhaps best illustrated by a few examples.

In [9]:

```
fruits = ['apple','banana','orange']
for i in fruits:
print(i)
```

apple banana orange

Here, `i`

is an *iterator* that represents each entry in the list

Here we use the `range()`

function, which builds a range space for the loop. This allows us to use `i`

as an index:

In [10]:

```
fruits = ['apple','banana','orange']
for i in range(0,len(fruits)):
print('Fruit {} = {}'.format(i,fruits[i]))
```

Fruit 0 = apple Fruit 1 = banana Fruit 2 = orange

`range(lo,hi)`

creates a range of integers from ** lo** to

`hi-1`

List comprehensions can be used to quickly build lists conforming to specific patterns. For example, if we wanted to build a list $$x_i=i^2, \quad i=1\ldots4$$ we can do this by:

In [11]:

```
x = [i**2 for i in range(1,4)]
print(x)
```

[1, 4, 9]

Similarly, to achieve $y_i=2^i, \; i=1\ldots 8$, we can do:

In [12]:

```
y = [2**i for i in range(1,9)]
print(y)
```

[2, 4, 8, 16, 32, 64, 128, 256]

List comprehensions provide a relatively simple syntax to build these types of lists.

In python, a *tuple* is like a list, but has a few differences:

- It is declared using
`()`

rather than`[]`

- It is immutable - it cannot be changed once it is built

Elements in a tuple are accessed in the same way as lists, using the `[]`

operator.

Generally, you will use lists rather than tuples.

The list functionality in python is not as useful as it could be when it comes to numerical operations. For example, you cannot perform mathematical operations on lists. This is where numpy comes in.

Unlike a Python list, a Numpy arrays is *homogeneous* - they contain entries of the same type (e.g., integer, real, complex).

Here and below, we will assume that you have:

In [13]:

```
import numpy as np
```

so that we can use ** np.** to shorten reference to numpy functions.

A numpy array is characterized by its *shape* and the type of elements it contains.

In [14]:

```
x = np.array( [1,2,3] ) # a 1-dimensional row vector
y = np.array( [ [1,2,3],[4,5,6] ] ) # a 2-dimensional matrix
print(x.shape,x)
print(y.shape,y)
```

(3,) [1 2 3] (2, 3) [[1 2 3] [4 5 6]]

In [15]:

```
x = np.array( [1,2,3], dtype=complex )
print(x) ## [ 1.+0.j 2.+0.j 3.+0.j ]
```

[1.+0.j 2.+0.j 3.+0.j]

Function | Description |
---|---|

`linspace(lo,hi,npts)` |
Builds a 1D array with a `npts` entries between `lo` and `hi` |

`arange(lo,hi,spacing)` |
Builds a 1D array spaced with `spacing` starting at `lo` and ending near `hi` |

`logspace(lo,hi,npts)` |
Builds a 1D array with `npts` points between $10^\mathrm{lo}$ and $10^\mathrm{hi}$ |

`meshgrid(x,y,...)` |
Given vectors specifying the range of each axis, builds a grid. |

Command | Description |
---|---|

`empty(shape)` |
Build an empty array. |

`empty_like(a)` |
Build an empty array shaped like `a` |

`eye(N)` |
Build a 2D identity matrix with `N` rows and columns |

`ones(shape)` |
build an array of ones with the specified `shape` |

`ones_like(a)` |
Build an array of ones shaped like `a` |

`zeros(shape)` |
build an array of zeros with the specified `shape` |

`zeros_like(a)` |
Build an array of zeros shaped like `a` |

`full(shape,val)` |
Build an array of the specified shape filled with `val` |

`full_like(a,val)` |
Build an array shaped like `a` filled with `val` |

`random.random(shape)` |
Build an array of random numbers with the specified shape |

Examples:

In [16]:

```
x = np.ones( [1,3] ) # 3-element row vector
y = np.empty_like( x ) # empty 3-element row vector
z = np.zeros( [3,3] ) # 3x3 matrix
p = np.full_like(z,np.pi) # 3x3 matrix full of 𝜋
r = np.random.random([2,3])
```

`dypte`

argument to specify the type of array to build (see above).

For more information on many other ways of building arrays, see the numpy docs

Most of the functions mentioned above such as `zeros`

, `ones`

, `full`

, `eye`

, etc. will create matrices as well - just give the appropriate shape.

Additionally, the diag function is very useful:

`diag(v,k)`

builds a matrix with`v`

on its`k`

^{th}diagonal. For example, the following builds a tridiagonal matrix:

In [17]:

```
n = 5
d = np.full(n,-3)
ud = np.full(n-1,1)
mat = np.diag(d,0) + np.diag(ud,1) + np.diag(ud,-1)
```

`diag(m,k)`

extracts the`k`

^{th}diagonal of the matrix`m`

In [18]:

```
m = np.random.random([5,5])
# extract the main diagonal of m
md = np.diag(m,0)
```

There are many array manipulation tools. Some of the more frequently used ones include:

Function | Description |
---|---|

`reshape(a,shape)` |
Reshape the data in `a` to a `shape` |

`ndarray.flat` |
Obtain an iterator over the array |

`transpose(a)` |
Transposes the array |

`tile(a,nrep)` |
Tile `a` `nrep` times. `nrep` can be an array. |

`flip(a,axis)` |
Reverse the elements along the `axis` dimension of `a` . |

`fliplr(a)` |
Flip the array in the left/right direction |

`flipud(a)` |
Flip the array in the up/down direction. |

In [19]:

```
a = np.linspace(0,4,10)
a[3] = -2
for i in a:
print(i)
```

You can also slice arrays:

In [20]:

```
a = np.random.random([4,4])
print(a)
a[1:2,1:3]
x = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
print(x[1:7:2])
```

And slice from the "back" of arrays:

In [21]:

```
a = np.arange(0,10,1)
print(a[-3:4:-1]) # start at third-to last, end at fifth
```

[7 6 5]

And slice all after:

In [22]:

```
a = np.arange(0,10,1)
print(a[5:])
```

[5 6 7 8 9]

Numpy arrays support typical mathematical operations like `+`

, `-`

, `*`

(element-wise multipy), `/`

(element-wise divide) and `**`

(element-wise exponentiation) provided the arrays are the same shape.

Numpy provides numerous mathematical functions that operate on arrays. For example:

In [23]:

```
x = np.linspace(-np.pi,np.pi)
y = np.sin(x)
z = x**2
```

In [24]:

```
n = 10
np.prod( np.arange(1,n+1,1) ) # the factorial of n
```

Out[24]:

3628800

Given the arrays $A = \left[\begin{array}{cc} 1 & 2\\ 3 & 4 \end{array}\right]$, $b=\left(\begin{array}{c} 5\\6\end{array}\right)$, and $c=\left(\begin{array}{c} 7\\8\end{array}\right)$,

In [25]:

```
A = np.array([[1,2],[3,4]])
b = np.array([[5],[6]])
c = np.array([[7],[8]])
```

For those coming from Matlab, the most familiar application of elemental multiplication is on arrays of the same shape. However, elemental multiplication does not require arrays to have the same shape. For example:

Operation | Description | Result |
---|---|---|

`A * A` |
Square elements in `A` |
$\begin{array}{cc} 1&4\\9&16\end{array}$ |

`b * c` |
Element-wise multiplication of `b` and `c` |
$\begin{array}{c} 35\\48\end{array}$ |

`A * b` |
Multiply a 2x2 by a 2x1. Results in a 2x2 | $\begin{array}{cc} 5&10\\18&24\end{array}$ |

`b.transpose() * c` |
Element-Multiply a row vector to a column vector | $\begin{array}{cc} 35&42\\40&48\end{array}$ |

While `*`

implies *elemental* multiplication, `@`

implies *matrix* multiplication.

Here are some example operations on the above arrays:

Operation | Result |
---|---|

`A @ b` |
$\begin{array}{c} 17 \\ 39 \end{array}$ |

`b.transpose() @ c` |
39 |

`b@c` |
error |

`b.transpose() @ A @ c` |
433 |

Among the other things that you should be aware of:

- Fourier Transform through the
`numpy.fft`

module. - Linear algebra through the
`numpy.linalg`

module. This includes things like dot products, matrix products, norms, matrix decomositions, etc. - Statistics.
- I/O to help with reading/writing arrays from/to disk.
- Advanced indexing tools.