# Python Crash Course¶

Hello! This is a quick intro to numerical programming in Python to help you hit the ground running with the AeroPython set of notebooks. (This intro is a modified version of the first notebook of the CFD Python series by Prof. Lorena A. Barba, July 2013.)

Python may already be installed on your computer (especially if you use OSX or a flavor of Linux). Even so, we recommend that you download and install the free Anaconda Scientific Python distribution. It makes it much easier to hit the ground running.

You will probably want to download a copy of this notebook, or the whole AeroPython collection. We recommend that you then follow along each lesson, experimenting with the code in the notebooks, or typing the code into a separate Python interactive session.

If you decided to work on your local Python installation, you will have to navigate in the terminal to the folder that contains the .ipynb files. Then, to launch the notebook server, just type:

jupyter notebook

You will get a new browser window or tab with a list of the notebooks available in that folder. Click on one and start working!

## Libraries¶

Python is a high-level open-source language. But the Python world is inhabited by many packages or libraries that provide useful things like array operations, plotting functions, and much more. We can import libraries of functions to expand the capabilities of Python in our programs.

OK! We'll start by importing a few libraries to help us out. First: our favorite library is NumPy, providing a bunch of useful array operations (similar to MATLAB). We will use it a lot! The second library we need is Matplotlib, a 2D plotting library which we will use to plot our results.

The following code will be at the top of most of your programs, so execute this cell first:

In [1]:
# <-- comments in python are denoted by the pound sign, like this one

import numpy                 # we import the array library
from matplotlib import pyplot    # import plotting library


We are importing one library named numpy and we are importing a module called pyplot of a big library called matplotlib.

To use a function belonging to one of these libraries, we have to tell Python where to look for it. For that, each function name is written following the library name, with a dot in between.

So if we want to use the NumPy function linspace(), which creates an array with equally spaced numbers between a start and end, we call it by writing:

In [2]:
myarray = numpy.linspace(0, 5, 10)
print(myarray)

[ 0.          0.55555556  1.11111111  1.66666667  2.22222222  2.77777778
3.33333333  3.88888889  4.44444444  5.        ]


If we don't preface the linspace() function with numpy, Python will throw an error, because it doesn't know where to find this function. Try it:

In [3]:
myarray = linspace(0, 5, 10)

---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-3-ed3ba806937a> in <module>()
----> 1 myarray = linspace(0, 5, 10)

NameError: name 'linspace' is not defined

The function linspace() is very useful. Try it changing the input parameters!

##### Import style:¶

You will often see code snippets that use the following lines

import numpy as np
import matplotlib.pyplot as plt


What's all of this import-as business? It's a way of creating a 'shortcut' to the NumPy library and the pyplot module. You will see it frequently as it is in common usage, but we prefer to keep out imports explicit. We think it helps with code readability.

##### Pro tip:¶

Sometimes, you'll see people importing a whole library without assigning a shortcut for it (like from numpy import *). This saves typing but is sloppy and can get you in trouble. Best to get into good habits from the beginning!

To learn new functions available to you, visit the NumPy Reference page. If you are a proficient Matlab user, there is a wiki page that should prove helpful to you: NumPy for Matlab Users

## Variables¶

Python doesn't require explicitly declared variable types, like C and other languages do. Just assign a variable and Python understands what you want:

In [4]:
a = 5      # a is an integer 5
b = 'five' # b is a string of the word 'five'
c = 5.0    # c is a floating point 5


Ask Python to tell you what type it has assigned to a given variable name like this:

In [5]:
type(a)

Out[5]:
int
In [6]:
type(b)

Out[6]:
str
In [7]:
type(c)

Out[7]:
float

In Python 3, division between integers, floats, or a combination thereof produces a float with the correct value, to machine precision. For example,

In [8]:
14/a

Out[8]:
2.8
In [9]:
14/c

Out[9]:
2.8

both produce the same floating point number.

## Whitespace in Python¶

Python uses indents and whitespace to group statements together. For contrast, if you were to write a short loop in the C language, you might use:

for (i = 0, i < 5, i++){
printf("Hi! \n");
}

Python does not use curly braces like C, it uses indentation instead; so the same program as above is written in Python as follows:

In [10]:
for i in range(5):
print("Hi \n")

Hi

Hi

Hi

Hi

Hi



Did you notice the range() function? It is a neat built-in function of Python that gives you a list from an arithmetic progression.

If you have nested for loops, there is a further indent for the inner loop, like this:

In [11]:
for i in range(3):
for j in range(3):
print(i, j)

print("This statement is within the i-loop, but not the j-loop")

0 0
0 1
0 2
This statement is within the i-loop, but not the j-loop
1 0
1 1
1 2
This statement is within the i-loop, but not the j-loop
2 0
2 1
2 2
This statement is within the i-loop, but not the j-loop


## Slicing arrays¶

In NumPy, you can look at portions of arrays in the same way as in MATLAB, with a few extra tricks thrown in. Let's take an array of values from 1 to 5:

In [12]:
myvals = numpy.array([1, 2, 3, 4, 5])
myvals

Out[12]:
array([1, 2, 3, 4, 5])

Python uses a zero-based index (like C), which is a good thing. Knowing this, let's look at the first and last element in the array we have created above,

In [13]:
myvals[0], myvals[4]

Out[13]:
(1, 5)

There are 5 elements in the array myvals, but if we try to look at myvals[5], Python will be unhappy and throw an error, as myvals[5] is actually calling the non-existent 6th element of that array.

In [14]:
myvals[5]

---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-14-6cc4d3ae83cd> in <module>()
----> 1 myvals[5]

IndexError: index 5 is out of bounds for axis 0 with size 5

Arrays can also be sliced, grabbing a range of values. Let's look at the first three elements,

In [15]:
myvals[0:3]

Out[15]:
array([1, 2, 3])

Note here, the slice is inclusive on the front end and exclusive on the back, so the above command gives us the values of myvals[0], myvals[1] and myvals[2], but not myvals[3].

## Assigning array variables¶

One of the strange little quirks/features in Python that often confuses people comes up when assigning and comparing arrays of values. Here is a quick example. Let's start by defining a 1-D array called $a$:

In [16]:
a = numpy.linspace(1,5,5)

In [17]:
a

Out[17]:
array([ 1.,  2.,  3.,  4.,  5.])

OK, so we have an array $a$, with the values 1 through 5. I want to make a copy of that array, called $b$, so I'll try the following:

In [18]:
b = a

In [19]:
b

Out[19]:
array([ 1.,  2.,  3.,  4.,  5.])

Great. So $a$ has the values 1 through 5 and now so does $b$. Now that I have a backup of $a$, I can change its values without worrying about losing data (or so I may think!).

In [20]:
a[2] = 17

In [21]:
a

Out[21]:
array([  1.,   2.,  17.,   4.,   5.])

Here, the 3rd element of $a$ has been changed to 17. Now let's check on $b$.

In [22]:
b

Out[22]:
array([  1.,   2.,  17.,   4.,   5.])

And that's how things go wrong! When you use a statement like a = b, rather than copying all the values of a into a new array called b, Python just creates an alias called b and tells it to route us to a. So if we change a value in a, then b will reflect that change (technically, this is called assignment by reference). If you want to make a true copy of the array, you have to tell Python to create a copy of a.

In [23]:
c = a.copy()


Now, we can try again to change a value in $a$ and see if the changes are also seen in $c$.

In [24]:
a[2] = 3

In [25]:
a

Out[25]:
array([ 1.,  2.,  3.,  4.,  5.])
In [26]:
c

Out[26]:
array([  1.,   2.,  17.,   4.,   5.])

OK, it worked! If the difference between a = b and a = b.copy() is unclear, you should read through this again. This issue will come back to haunt you otherwise.

There are a lot of resources online to learn more about using NumPy and other libraries. Just for kicks, here we use IPython's feature for embedding videos to point you to a short video on YouTube on using NumPy arrays.

In [27]:
from IPython.display import YouTubeVideo

from IPython.core.display import HTML