Introduction#

\(\newcommand{\re}{\mathbb{R}}\) Python comes in two versions, 2 and 3. Version 2 is still available on some old computers but it is deprecated and should not be used. Hence we will use version 3 throughout these lectures. To see if Python is already installed

which python

You can check the version like this on the terminal

$ python --version

On some systems, you may have to use python3 to get version 3 python.

Usually Python is available on Linux/Unix computers, though it may not provide all the required libraries. You can install them using your system package manager (apt on Debian/Ubuntu) or by installing a full Python distribution like Miniforge. It is highly recommended to use Conda to install Python, rather than using your system package manager. If you use Conda, then make sure to set your PATH variable so that it finds Anaconda python rather than your system python.

export PATH=/path/to/miniforge/bin:$PATH

Then test that the correct python is in your PATH

$ which python

Python is organized into modules and some of the useful Python modules we will use in this tutorial are

  • numpy: provides arrays, linear algebra, random numbers, etc.

  • scipy: integration, optimization, linear algebra

  • matplotlib: for making graphs

  • sympy: symbolic math

There are several ways to use Python.

  • python: type this in terminal, very basic, does not have interactivity or help

$ python
Python 3.10.8 | packaged by conda-forge | (main, Nov 22 2022, 08:25:13) [Clang 14.0.6 ] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>
  • ipython: better python terminal

Python 3.10.8 | packaged by conda-forge | (main, Nov 22 2022, 08:25:13) [Clang 14.0.6 ]
Type 'copyright', 'credits' or 'license' for more information
IPython 8.7.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]:
  • pylab: imports some modules like numpy; define an alias

alias pylab="ipython --pylab --matplotlib --nosep --pprint"
  • jupyter notebook: This is what you are reading now. On some computers you can start a notebook using

$ ipython-notebook (conda install notebook)
$ jupyter-lab      (conda install jupyterlab)

I prefer using jupyterlab since it seems more modern.

Basic math#

The basic number types we commonly use are integers, reals and complex numbers. In Python, we can directly use variables without declaring their type. This is in contrast to most other programming languages like Fortran/C/C++ where the type of variable has to be declared before using it.

x = 1
y = 2
z = x + y
print(z)
3

The type of the variable is automatically determined by what we assign to it.

print(type(x),type(y),type(z))
<class 'int'> <class 'int'> <class 'int'>
x = 1.0
y = 2.0
z = x + y
print(z)
print(type(z))
3.0
<class 'float'>

The float type corresponds to double precision in C/C++ and has about 16 decimal places of accuracy. It is good programming practice to assign numerical values based on their intended type. If you want x to be a float then assign its value as x = 1.0 and not as x = 1.

Python will automatically determine the type when you have mixed types in some expression.

a = 1
b = 2.345
c = a + b
print(c)
print(type(a),type(b),type(c))
3.345
<class 'int'> <class 'float'> <class 'float'>

Implicit typing reduces code clutter, but there is potential for making mistakes, so be careful to set correct values to reflect the intended type.

Variable names are case sensitive; below a and A are different variables.

a = 1
A = 2
print('a = ',a,', A = ',A)
a =  1 , A =  2

Division is performed using backslash symbol

x = 1.0
y = 3.0
z = x/y
print(z)
0.3333333333333333

Division of integers still returns a float (This behaviour was different in Version 2 and earlier).

print(1/2)
0.5

Integer division can be performed with double backslash operator

print('1/2, 1//2 =', 1/2, 1//2)
print('1/3, 1//3 =', 1/3, 1//3)
print('2/3, 2//3 =', 2/3, 2//3)
print('3/2, 3//2 =', 3/2, 3//2)
print('5/3, 5//3 =', 5/3, 5//3)
1/2, 1//2 = 0.5 0
1/3, 1//3 = 0.3333333333333333 0
2/3, 2//3 = 0.6666666666666666 0
3/2, 3//2 = 1.5 1
5/3, 5//3 = 1.6666666666666667 1

The result is rounded down to the integer value.

Raising to some power

\[ c = a^b \]

is done like this

a, b = 3, 2
c = a**b
print(c)
9

If the power \(n\) in

\[ y = x^n \]

is an integer, declare \(n\) as integer 2 which will be faster than if you declare it as 2.0.

x, n = 1.234, 2
y = x**n
print(x,n,y)
1.234 2 1.522756

Formatted print#

We can use C-style formatting

i = 10
x = 1.23456789
print('i, x = %5d %12.6f %14.6e' % (i,x,x))
i, x =    10     1.234568   1.234568e+00

Since we used 6 decimal places, the floating point numbers are rounded. The exponential form is preferred if we have very small or very large numbers.

x = 0.0000000123456
print('%16.6f %16.6e' % (x,x))
        0.000000     1.234560e-08

Math functions#

The basic Python language does not have mathematical functions like sin, cos, etc. These are implemented in additional modules. The math module provides many of these standard functions. We have to first import the module like this

import math

Now we can use the functions available in this module.

x = 10.0
z = math.sin(x)
print(z)
-0.5440211108893698

Value of \(\pi\)

print(math.pi)
3.141592653589793

On import#

We can also import everything into the current workspace and then we can use it without prepending math

>> from math import *
>> x = sin(1.5*pi)

but this is not recommended usage since there may exist functions with same name in different modules. We can also import only what we need, e.g.,

from math import sin,cos,pi

Strings#

Strings can be created by enclosing characters between single quote or double quotes.

a, b, c, d = 'Hello', ',', ' ', 'World!'
print(a+b+c+d)
Hello, World!

Addition operator on strings concatenates them.

If we want to create a string of the form sol_100.txt

base = 'sol_'
it   = 100
ext  = '.txt'
filename = base + str(it) + ext
print(filename)
sol_100.txt

Lists#

Lists allow us to store a collection of objects. Here is a list of integers

a = [1, 2, 3, 4, 5]
print(a)
[1, 2, 3, 4, 5]

But lists can be made up of different types of elements.

b = [1, 2.0, 3.0, 4, 'x']
print(b)
[1, 2.0, 3.0, 4, 'x']

Lists look like vectors but they do not obey rules of algebra.

x = [1, 2, 3]
y = [5, 6, 1]
print(x + y)
[1, 2, 3, 5, 6, 1]

Note that x and y have been concatenated. So a list behaves more like a set, but they are not sets since elements in a list can be repeated. To get the behaviour of vector addition, use Numpy arrays which we discuss later.

We can access an element of a list using its index

print('x =',x)
print('x[0] =',x[0])
print('x[1] =',x[1])
print('x[2] =',x[2])
x = [1, 2, 3]
x[0] = 1
x[1] = 2
x[2] = 3

Indices in Python start from 0.

You can get the length of a list using len.

print(x)
print(len(x))
[1, 2, 3]
3

We can modify the elements of a list

x[1] = 0
print(x)
[1, 0, 3]

We can incrementally build a list

a = [] # empty list
a.append(1)
a.append(2)
a.append(3)
print(a)
[1, 2, 3]

Tuples#

Tuples are similar to lists but they cannot be modified, they are immutable.

x = (1,2,3)
print(x)
print(x[0])
print(x[1])
print(x[2])
(1, 2, 3)
1
2
3

Try to modify some element of a tuple, for example

x[1] = 0

NOTE: Lists and tuples can contain elements of different types

a = [1,2,'a']
b = (1,2,'a')
print("a = ", a)
print("b = ", b)
a =  [1, 2, 'a']
b =  (1, 2, 'a')

Sets#

A set is a collection of some objects which can be of any and different types.

a = {1, 2, 3, 'a', 'b', 'c'}
print(a)
{'b', 1, 2, 3, 'a', 'c'}

Elements of a set must be unique.

b = {1, 2, 3, 3}
print(b)
{1, 2, 3}

Minus performs set minus.

a = {1,2,3,4}
b = {1,2,3}
print(a-b)
print(b-a)
{4}
set()

Union of sets

c = {1,2,3,4}
d = {3,4,5,6}
print(c.union(d))
print(c|d)
{1, 2, 3, 4, 5, 6}
{1, 2, 3, 4, 5, 6}

For loops#

for i in range(5):
    print(i)
0
1
2
3
4

range(n) produces the integers: \(0,1,2,\ldots,n-1\). Note: \(n\) is not included.

print(range(5))
print(type(range(5)))
range(0, 5)
<class 'range'>

Specify both start and end

for i in range(5,10):
    print(i)
5
6
7
8
9

range(m,n) produces the integers: \(m,m+1,\ldots,n-1\) provided \(m < n\).

for i in range(0,10,2):
    print(i)
0
2
4
6
8

range(m,n,s) produces the integers: \(m,m+s,m+2s,\ldots\) until \(n\), but excluding \(n\).

We can have a negative step size.

for i in range(10,0,-2):
    print(i)
10
8
6
4
2

Again, the last element, here 0, is not included.

In built manual/help#

To see the inbuilt manual in terminal

>>> range?

This works for all functions, etc.

>>> import math
>>> math.sin?

In a notebook, type this in a code cell

range?

get the help documentation.

Example: Sum a list of integers#

Given a vector \(a \in \re^n\), compute the sum of its element

\[ s = \sum_{i=0}^{n-1} a_i \]

We will need some mechanism to iterate over the elements to compute the sum, and we can use a for loop.

a = [1,2,3,4,5,6,7,8,9,10]
s = 0
for x in a:
    s += x
print('Sum = ',s)
Sum =  55

Note that s += x is shorthand for s = s + x. Since we accumulate the sum into s, we have to first initialize it to zero.

Another way is to use indices

s = 0
for i in range(len(a)):
    s += a[i]
print('Sum = ',s)
Sum =  55

while loop#

A for loop is useful when we know a-priori how many steps we have to do. When we dont know in advance how many steps are needed, a while loop may be more useful.

Example: Generate independent uniform random numbers \(x_j\) such that

\[ \sum_{j=0}^{n-1} x_j < 10, \qquad \sum_{j=0}^{n} x_j > 10 \]

We dont know the value of n a-priori.

import random
s, i = 0.0, 0
while s < 10.0:
    s += random.random()
    i += 1
    print("%5d %20.10f" % (i,s))
    1         0.5940166748
    2         1.2818454959
    3         1.5974936223
    4         2.1392944746
    5         2.9149129007
    6         3.0732040697
    7         3.0880344636
    8         3.3358875641
    9         3.3415009088
   10         3.3792213245
   11         4.1367973168
   12         4.8236956917
   13         5.2520791720
   14         6.1970159750
   15         6.7005327811
   16         6.7540986939
   17         6.9850782977
   18         7.3449310783
   19         7.7279754536
   20         8.4521317429
   21         8.4684857437
   22         8.6397382858
   23         9.1490734198
   24         9.4050665941
   25         9.6616819777
   26        10.1777617676

Note how we controlled the printing of numbers: d is for integers, f is for floating point numbers. You can also use e for scientific notation.

enumerate#

Another way to iterate over an array-type object where we get both index and element value is using enumerate.

values = [1.2, 2.3, 3.4, 4.5, 5.6]
for i,val in enumerate(values):
    print(i,"   ",val)
0     1.2
1     2.3
2     3.4
3     4.5
4     5.6

Swapping#

a, b = 1, 2
print('Before: a = ',a,', b = ',b)
tmp = a
a   = b
b   = tmp
print('After : a = ',a,', b = ',b)
Before: a =  1 , b =  2
After : a =  2 , b =  1

In Python, swapping can be done as

a, b = 1, 2
print('Before: a = ',a,', b = ',b)
a, b = b, a
print('After : a = ',a,', b = ',b)
Before: a =  1 , b =  2
After : a =  2 , b =  1