Numpy array hints

AMath 585, Winter Quarter 2020 at the University of Washington. Developed by R.J. LeVeque and distributed under the BSD license. You are free to modify and use as you please, with attribution.

These notebooks are all available on Github.


This notebook contains a few hints about working with numpy arrays, particularly in the context of writing numerical solvers for differential equations.

For more information see, e.g.:

In [1]:
%matplotlib inline
In [2]:
from pylab import *

Data types

numpy arrrays are special objects designed for holding numerical values and allow doing arithmetic operations. So every element in an array has to have the same type, e.g. int, float. You can specify this when creating an array, e.g.

In [3]:
x = array([1,2,3], dtype=float)
x
Out[3]:
array([1., 2., 3.])

If you don't specify, it will figure it out and if necessary convert integers to floats (if at least one element you specify is a float, e.g.

In [4]:
x = array([1., 2,3])
print(x, x.dtype)
x = array([1,2,3])
print(x, x.dtype)
[1. 2. 3.] float64
[1 2 3] int64

Complex numbers can also be handled. Note that in Python 3+4j for example represents $3 + 4i$.

In [5]:
x = array([3j,2,3])
print(x, x.dtype)
[0.+3.j 2.+0.j 3.+0.j] complex128

For example, compute the matrices of a 2x2 skew-symmetric matrix:

In [6]:
A = array([[0, -1],[1,0]])
print('A = \n', A)
lam,V = eig(A)
print('Eigenvalues: ',lam)
print('Eigenvectors: \n',V)
A = 
 [[ 0 -1]
 [ 1  0]]
Eigenvalues:  [0.+1.j 0.-1.j]
Eigenvectors: 
 [[0.70710678+0.j         0.70710678-0.j        ]
 [0.        -0.70710678j 0.        +0.70710678j]]

Lambda functions

Note I used lam above, since lambda is a keyword in Python. A lambda function is an easy way to specify a one-line function. We use it in some notebooks to specify utrue, for example.

The following give equavalent functions:

In [7]:
def f(x,y):
    return x**2 + 3*y
print(f(2,3))

f = lambda x,y: x**2 + 3*y
print(f(2,3))
13
13

Indexing and slicing

Recall that Python is zero-based, so the first index in an array is index 0. It is also important to realize that if you want to select say the first 3 elements of an array x, this is denoted by x[0:4], e.g.

In [8]:
x = linspace(0,6,7)
print(x)
print(x[0:4])
[0. 1. 2. 3. 4. 5. 6.]
[0. 1. 2. 3.]

This could also be written as x[:4] (it starts at 0 if not specified) and so this means the first 4 elements.

Similarly, note that the range command often used in loops has similar behavior:

In [9]:
for j in range(10):
    print(j)
0
1
2
3
4
5
6
7
8
9

In selecting elements of an array, you can also do things like:

- `x[2:]` all elements from `x[2]` to the end
- `x[-1]` last element (and `x[-2]` is the next-to-last, etc.
- `x[:-1]` all elements except the last.
- `x[1::2]` every other element starting with `x[1]`.
In [10]:
print(x[2:])
print(x[-1])
print(x[2:-2])
print(x[1::2])
[2. 3. 4. 5. 6.]
6.0
[2. 3. 4.]
[1. 3. 5.]

Modifying arrays as Python objects

In most computer languages if you do

a = 1
b = a
b = 2

The value of b will be changed but a will still be 1. This is also true in Python since a and b are integers here (also if they were floats). A very good thing or it would be very hard to write correct programs.

However, if A is a numpy array and you set B = A and then modify some element of B, the corresponding element of A also changes!

That's because Python is an object-oriented language and a numpy array is what's called a mutable object, meaning you can change certain attributes of it without changing the basic object or where it's stored in memory. If A is a mutable object and you do B = A then you are simply creating a new pointer to the same object.

Integers and floats are immutable objects and doing b = a above implicitly forces Python to create a new object and initialize it with the value a has.

You can do the same thing for a numpy array by setting B = A.copy(), and then B is a new object initialized with the attributes of A but it can be changed independently.

Note that the main difference between a tuple such as (1,2,"A") and a list [1,2,"A"] is that a list is mutable (e.g. you can change an element or use the append method) while a tuple is always immutable. Sometimes it's important which you use, sometimes not. Google "Python mutable objects" for many tutorials.

Some examples of modifying or slicing arrays follow:

In [11]:
A = eye(2)
B = A  # points to same object as A
B[0,1] = 5.
print('A = \n', A, '\nB = \n', B)
print('Note A and B both point to the same object:')
print('id(A) = %s, id(B) = %s' % (id(A), id(B)))
A = 
 [[1. 5.]
 [0. 1.]] 
B = 
 [[1. 5.]
 [0. 1.]]
Note A and B both point to the same object:
id(A) = 4735809536, id(B) = 4735809536
In [12]:
A = eye(2)
B = A.copy()  # makes new object
B[0,1] = 5.
print('A = \n', A, '\nB = \n', B)
print('Note A and B both point to the different objects:')
print('id(A) = %s, id(B) = %s' % (id(A), id(B)))
A = 
 [[1. 0.]
 [0. 1.]] 
B = 
 [[1. 5.]
 [0. 1.]]
Note A and B both point to the different objects:
id(A) = 4735810416, id(B) = 4738359504

If you define an array as a combintaion of other arrays then of course numpy is forced to create a new object to hold the result, so you don't need to do anything special in this case:

In [13]:
A = eye(2)
B = A + 3*A  # A must be a new object
B[0,1] = 5.
print('A = \n', A, '\nB = \n', B)
A = 
 [[1. 0.]
 [0. 1.]] 
B = 
 [[4. 5.]
 [0. 4.]]

A slice or subset of an array sometimes only defines a view into an existing object, for example:

In [14]:
U = linspace(1,3,3)
print('U = ', U)
V = U[0:2]
print('V = ',V)
V[0] = 5.
print('after changing V, U also changed:')
print('V = ',V)
print('U = ', U)
U =  [1. 2. 3.]
V =  [1. 2.]
after changing V, U also changed:
V =  [5. 2.]
U =  [5. 2. 3.]

Again you might want to make a copy if you know you are going to modify the slice:

In [15]:
U = linspace(1,3,3)
print('U = ', U)
V = U.copy()[0:2]
print('V = ',V)
V[0] = 5.
print('after changing V, U is unchanged:')
print('V = ',V)
print('U = ', U)
U =  [1. 2. 3.]
V =  [1. 2.]
after changing V, U is unchanged:
V =  [5. 2.]
U =  [1. 2. 3.]