FMP AudioLabs
B

Python Basics


This notebook introduces some important aspects of the Python programming language. For more comprehensive tutorials, we refer to the following sources:

  • The Python Tutorial introduces the reader informally to the basic concepts and features of the Python language and system.
  • The Scipy Lecture Notes is a tutorial on the scientific Python ecosystem including libraries such as Numpy, Scipy, and Matploblib.

Basic Facts

  • Python is an interpreted (not compiled), open-source, multi-platform programming language.
  • There exist a set of modules for scientific computing (e.g. numpy, scipy, matplotlib, librosa) in Python.
  • Python uses indentation (and not brackets or end-commands) to separate blocks of code.
  • Comment lines start with the character #.
  • Useful functions for help:
    • Invoke the built-in help system: help()
    • List objects in namespace: dir()
    • Show global and local variables: globals(), locals()

Data Types

Let us start with some basic facts on Python variables:

  • Variables do not need to be declared; neither their type.
  • Variables are created automatically when they are first assigned.
  • A variable name may contain letters (a, b, ..., Y, Z) and the underscore (_).
  • Variable names are case sensitive.
  • All but the first character can also be positive integer numbers.
  • Usually one uses lower case letters and underscores to separate words.

A string is given in single ticks (') or double ticks ("). If there is no other reason, we recommend single ticks. The following code assigns a string to a variable and prints it using the print-command.

In [1]:
string_variable = 'Welcome to the Python Tutorial'
print(string_variable)
Welcome to the Python Tutorial

Here some basic string formatting:

In [2]:
print('This is an integer: %d.' % 17)
print('This is string1: %s and this is string2: %6s.' % ('ABCD', '1234'))
print('This is a floating point number: %06.3f.' % 3.14159265359)
This is an integer: 17.
This is string1: ABCD and this is string2:   1234.
This is a floating point number: 03.142.

Some basic math:

In [3]:
n = 3
print('n + 1 =', n + 1)
print('n - 1 =', n - 1)
print('n * 2 =', n * 2)
print('n / 2 =', n / 2)
print('n ^ 2 =', n ** 2)
n + 1 = 4
n - 1 = 2
n * 2 = 6
n / 2 = 1.5
n ^ 2 = 9

Division always results in a floating point number, even if the number is divisible without remainder. (Note that there are differences between Python 2 and Python 3 in using /). If the result should be an integer (e.g., when using it as an index), one may use the // operator. The % yields the remainder.

In [4]:
n = 8
print('Normal division:', n / 2)
print('Integer division:', n // 2)
print('Normal division:', n / 5)
print('Integer division:', n // 5)
print('Remainder of integer division:', n % 5)
Normal division: 4.0
Integer division: 4
Normal division: 1.6
Integer division: 1
Remainder of integer division: 3

For re-assigning a variable, one may use the following conventions:

In [5]:
n = 7
n += 11
print(n)

n *= 2
print(n)

n /= 18
print(n)

n **= 0.5
print(n)
18
36
2.0
1.4142135623730951

The basic compound data types in Python are lists and tuples. A list is enclosed in square brackets and a tuple is enclosed in round brackets. Both are indexed with square brackets (with indexing starting with $0$). The len function gives the length of a tuple or a list.

In [6]:
var_lis = ['I', 'am', 'a', 'list']
var_tup = ('I', 'am', 'a', 'tuple')
print(var_lis)
print(var_tup)
print(var_lis[0], var_tup[1], 'generated from', 
      var_tup[2], var_tup[3], 'and', var_lis[2], var_lis[3])
print(len(var_tup))
print(len(var_lis))

print(type(var_lis))
print(type(var_tup))
['I', 'am', 'a', 'list']
('I', 'am', 'a', 'tuple')
I am generated from a tuple and a list
4
4
<class 'list'>
<class 'tuple'>

What is the difference between a list and a tuple? Tuples are immutable objects (i.e., their state cannot be modified after they are created) and a bit more efficient. Lists are more flexible. Here are some examples for list operations:

In [7]:
var_lis = [1, 2, 3]
var_lis[0] = -1
var_lis.append(10)
var_lis = var_lis + ['a', '12', [13, 14]]
print(var_lis)
print(var_lis[-1])
[-1, 2, 3, 10, 'a', '12', [13, 14]]
[13, 14]

One can index a list with start, stop, and step values ([start:end:step). Note that, in Python, the last index value is end-1. Negative indices are possible with -1 referring to the last index. When not specified, start refers to the first item, end to the last item, and step is set to $1$.

In [8]:
var_lis = [11, 12, 13, 14, 15]
print('var_lis[0:3] =', var_lis[0:3])
print('var_lis[1:3] =', var_lis[1:3])
print('var_lis[-1] =', var_lis[-1])
print('var_lis[0:3:2] =', var_lis[0:4:2])
print('var_lis[0::2] =', var_lis[0::2])
print('var_lis[::-1] =', var_lis[::-1])
var_lis[0:3] = [11, 12, 13]
var_lis[1:3] = [12, 13]
var_lis[-1] = 15
var_lis[0:3:2] = [11, 13]
var_lis[0::2] = [11, 13, 15]
var_lis[::-1] = [15, 14, 13, 12, 11]

The following examples shows how the elements of a list or tuple can be assigned to variables (called unpacking):

In [9]:
var_lis = [1, 2]
[a, b] = var_lis
print(a, b)

var_tup = (3, 4)
[c, d] = var_tup
print(c, d)
1 2
3 4

Leaving out brackets, tuples are generated.

In [10]:
t = 1, 2
print(t)

a, b = t
print(a, b)
(1, 2)
1 2

The range-function can be used to specify a tuple or list of integers (without actually generating these numbers):

In [11]:
print(range(9))
print(range(1, 9, 2))
range(0, 9)
range(1, 9, 2)

A range can then be converted into a tuple or list as follows:

In [12]:
print(list(range(9)))
print(tuple(range(1, 9, 2)))
print(list(range(9, 1, -1)))
[0, 1, 2, 3, 4, 5, 6, 7, 8]
(1, 3, 5, 7)
[9, 8, 7, 6, 5, 4, 3, 2]

Boolean values in Python are True and False. Here are some examples for basic comparisons:

In [13]:
a = 1
b = 2
print(a < b)
print(a <= b)
print(a == b)
print(a != b)
True
True
False
True

The bool function converts an arbitrary value into a boolean value. Here, are some examples:

In [14]:
print(bool('a'))
print(bool(''))
print(bool(1))
print(bool(0))
print(bool(0.0))
print(bool([]))
print(bool([4, 'hello', 1]))
True
False
True
False
False
False
True

There are also other data types in Python, which we want to mention here. In the following, we introduce sets, which are unordered collections of unique elements. Furthermore, we apply some basic set operations.

In [15]:
s = {4, 2, 1, 2, 5, 2}
print('Print the set s:', s)
print('Union of sets:', {1, 2, 3} | {2, 3, 4})
print('Intersection of sets:', {1, 2, 3} & {2, 3, 4})
s.add(7)
print('Adding an element', s)
s.remove(2)
print('Removing an element', s)
Print the set s: {1, 2, 4, 5}
Union of sets: {1, 2, 3, 4}
Intersection of sets: {2, 3}
Adding an element {1, 2, 4, 5, 7}
Removing an element {1, 4, 5, 7}

Another convenient data type are dictionaries, which are indexed by keys (rather than by a range of numbers as is the case for lists or arrays). The following code cell gives an examples and introduce some basic operations.

In [16]:
dic = {'a': 1, 'b': 2, 3: 'hello'}
print('Print the dictionary dic:', dic)
print('Print the keys of dic:', list(dic.keys()))
print('Access the dic via a key:', dic['b'])
print('Print the values of dic:', list(dic.values()))
Print the dictionary dic: {'a': 1, 'b': 2, 3: 'hello'}
Print the keys of dic: ['a', 'b', 3]
Access the dic via a key: 2
Print the values of dic: [1, 2, 'hello']

Basic Control Structures

For control structures such as if, for or while one has to use indentations (as part of the syntax). A typical Python convention is to use four spaces. For example, an if-statement is written as follows:

In [17]:
n = 2
if n == 2:
    print('True')
else:
    print('False')
True

The next example shows how to use a for-loop. Note that an iterable may be specified by a range, a list, a tuple, or even other structures.

In [18]:
for i in range(3):
    print(i, end='-')
print()
    
for i in ['a', 2, 'c', 'def']:
    print(i, end='-')
print()

for i in 'abcd':
    print(i, end='-')
print()
0-1-2-
a-2-c-def-
a-b-c-d-

A while-loop is written as follows:

In [19]:
a = 0
while a < 5:
    print(a, end='-')
    a += 1
0-1-2-3-4-

Functions

One defines functions with the def-keyword. As variable names, function names may contain letters (a, b, ..., Y, Z) and the underscore (_). All but the first character can also be positive integer number. Usually one uses lower case letters and underscores to separate words. The following function is named add. It has three arguments a, b, and c (with b and c having a default value). The return keyword is succeeded by the return value.

In [20]:
def add(a, b=0, c=0):
    """Function to add three numbers

    | Notebook: B/B_libfmp.ipynb and
    | Notebook: B/B_PythonBasics.ipynb

    Args:
        a (float): First number
        b (float): Second number (default: 0)
        c (float): Third number (default: 0)

    Returns:
        d (float): Sum
    """
    d = a + b + c
    print('Addition: ', a, ' + ', b, ' + ', c, ' = ', d)
    return d

print(add(5))
print(add(5, 2, 1))
print(add(5, c=4))
Addition:  5  +  0  +  0  =  5
5
Addition:  5  +  2  +  1  =  8
8
Addition:  5  +  0  +  4  =  9
9

There can also be multiple return values (which are returned as a tuple):

In [21]:
def add_and_diff(a, b=0):
    """Function to add and substract two numbers
    
    Notebook: B/B_PythonBasics.ipynb
    
    Args:
        a: first number
        b: second number (default: 0)
        
    Returns:
        first: a + b
        second: a - b
    """
    return a + b, a - b

x = add_and_diff(3, 5)
print(x)
(8, -2)

NumPy Arrays

Python has several useful built-in packages as well as additional external packages. One such package is NumPy, which adds support for multi-dimensional arrays and matrices, along with a number of mathematical functions to operate on these structures, see NumPy Reference Manual for details. The NumPy package is imported as follows:

In [22]:
import numpy as np

It is convenient to bind a package to a short name (for example np as above). This short name appears as prefix when calling a function from the package. This is illustrated by the following array-function provided by numpy:

In [23]:
x = np.array([1, 2, 3, 3])
print(x)
[1 2 3 3]

Each array has a shape, a type, and a dimension.

In [24]:
print('Shape:', x.shape)
print('Type:', x.dtype)
print('Dimension:', x.ndim)
Shape: (4,)
Type: int64
Dimension: 1

In this example, note that x.shape produces a one-element tuple, which is encoded by (4,) for disambiguation. (The object (4) would be an integer of type int rather than a tuple.) Multi-dimensional arrays are created like follows:

In [25]:
x = np.array([[1, 2, 33], [44, 5, 6]])
print('x = ', x, sep='\n')
print('Shape:', x.shape)
print('Type:', x.dtype)
print('Dimension:', x.ndim)
x = 
[[ 1  2 33]
 [44  5  6]]
Shape: (2, 3)
Type: int64
Dimension: 2

There are a couple of functions for creating arrays:

In [26]:
print('Array of given shape and type, filled with zeros: ', np.zeros(2))
print('Array of given shape and type, filled with integer zeros: ', np.zeros(2, dtype='int'))
print('Array of given shape and type, filled with ones: ', np.ones((2, 3)), sep='\n')
print('Evenly spaced values within a given interval: ', np.arange(2, 8, 2))
print('Random values in a given shape: ', np.random.rand(2, 3),  sep='\n')
print('Identity matrix: ', np.eye(3),  sep='\n')
Array of given shape and type, filled with zeros:  [0. 0.]
Array of given shape and type, filled with integer zeros:  [0 0]
Array of given shape and type, filled with ones: 
[[1. 1. 1.]
 [1. 1. 1.]]
Evenly spaced values within a given interval:  [2 4 6]
Random values in a given shape: 
[[0.64630379 0.81998005 0.28367627]
 [0.75657033 0.72372747 0.85203454]]
Identity matrix: 
[[1. 0. 0.]
 [0. 1. 0.]
 [0. 0. 1.]]

Reshaping of an array is possible like follows:

In [27]:
x = np.arange(2 * 3 * 4)
print('Shape:', x.shape)
print(x)

y = x.reshape((3, 8))
print('Shape:', y.shape)
print(y)

y = x.reshape((3, 2, 4))
print('Shape:', y.shape)
print(y)

print('Element y[0, 1, 2] = ', y[0, 1, 2])
Shape: (24,)
[ 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23]
Shape: (3, 8)
[[ 0  1  2  3  4  5  6  7]
 [ 8  9 10 11 12 13 14 15]
 [16 17 18 19 20 21 22 23]]
Shape: (3, 2, 4)
[[[ 0  1  2  3]
  [ 4  5  6  7]]

 [[ 8  9 10 11]
  [12 13 14 15]]

 [[16 17 18 19]
  [20 21 22 23]]]
Element y[0, 1, 2] =  6

NumPy allows for giving one of the new shape parameter as -1. In this case, NumPy automatically figures out the unknown dimension. Note that in the following example the difference between the shape (6,) and the shape (6, 1).

In [28]:
x = np.arange(6)
print('Shape: %6s; dim: %s' % (x.shape, x.ndim))
x = x.reshape(-1, 2)
print('Shape: %6s; dim: %s' % (x.shape, x.ndim))
x = x.reshape(-1, 1)
print('Shape: %6s; dim: %s' % (x.shape, x.ndim))
Shape:   (6,); dim: 1
Shape: (3, 2); dim: 2
Shape: (6, 1); dim: 2

Applied to arrays, many operations are conducted in an element-wise fashion:

In [29]:
x = np.arange(5)
print('x + 1 =', x + 1)
print('x * 2 =', x * 2)
print('x > 2 =', x > 2)
x + 1 = [1 2 3 4 5]
x * 2 = [0 2 4 6 8]
x > 2 = [False False False  True  True]

Similarly, applied to two arrays of the same shape, these operations are conducted in an element-wise fashion. Matrix multiplication is performed by using the function np.dot:

In [30]:
a = np.arange(0, 4).reshape((2, 2))
b = 2 * np.ones((2, 2))

print(a)
print(b)
print(a + b)
print(a * b)
print(np.dot(a, b))
[[0 1]
 [2 3]]
[[2. 2.]
 [2. 2.]]
[[2. 3.]
 [4. 5.]]
[[0. 2.]
 [4. 6.]]
[[ 2.  2.]
 [10. 10.]]

Note that arrays and lists may behave in a completely different way. For example, addition leads to the following results:

In [31]:
a = np.arange(4)
b = np.arange(4)
print(a + b, type(a + b))

a = list(a)
b = list(b)
print(a + b, type(a + b))
[0 2 4 6] <class 'numpy.ndarray'>
[0, 1, 2, 3, 0, 1, 2, 3] <class 'list'>

The sum of an array's elements can be computed as follows:

In [32]:
x = np.arange(6).reshape((2, 3))
print(x)
print(x.sum())
print(x.sum(axis=0))
print(x.sum(axis=1))
[[0 1 2]
 [3 4 5]]
15
[3 5 7]
[ 3 12]

There are many ways for accessing and manipulating arrays:

In [33]:
x = np.arange(6).reshape((2, 3))
print(x)
print(x[1, 1])
print([x > 1])
print(x[x > 1])
print(x[1, :])
print(x[:, 1])
[[0 1 2]
 [3 4 5]]
4
[array([[False, False,  True],
       [ True,  True,  True]])]
[2 3 4 5]
[3 4 5]
[1 4]

NumPy offers many different numerical types and methods for type conversion. Specifying the exact type is often important when using packages such as numba for optimizing machine code at runtime. In the following example, we give an example where a wrong initialization leads to an error when computing the square root of a negative number.

In [34]:
print('=== Initialization with \'int32\' leading to an error ===', flush=True)
x = np.arange(-2, 2)
print(x, x.dtype)
x = np.sqrt(x)
print(x)     
=== Initialization with 'int32' leading to an error ===
[-2 -1  0  1] int64
[nan nan  0.  1.]
<ipython-input-34-0c48f08d8e15>:4: RuntimeWarning: invalid value encountered in sqrt
  x = np.sqrt(x)
In [35]:
print('=== Initialization with \'complex\' ===', flush=True)
x = np.arange(-3, 3, dtype='complex')
print(x, x.dtype)
x = np.sqrt(x)
print(x)
=== Initialization with 'complex' ===
[-3.+0.j -2.+0.j -1.+0.j  0.+0.j  1.+0.j  2.+0.j] complex128
[0.        +1.73205081j 0.        +1.41421356j 0.        +1.j
 0.        +0.j         1.        +0.j         1.41421356+0.j        ]
Acknowledgment: This notebook was created by Frank Zalkow, Stefan Balke, and Meinard Müller.
C0 C1 C2 C3 C4 C5 C6 C7 C8