# Unit 2: Python Basics¶

- Overview and Learning Objectives
- Basic Facts
- Variables and Basic Operators
- Lists and Tuples
- Boolean Values
- Sets
- Dictionaries
- Python Type Conversion
- Exercise 1: Basic List Manipulations
- Exercise 2: Basic Dictionary Manipulations

## Overview and Learning Objectives

This unit introduces basic concepts used in Python programming while we assume that you are familiar with general programming and have some experience with other programming languages such as MATLAB, C/C++, or Java. We start with basic Python variables and operators and then introduce compound data types such as lists, tuples, sets, and dictionaries. It is important that you understand the conceptual difference between these constructs and the Python syntax to encode them. Using variables and data types in Python can differ significantly from the other programming languages, often resulting in seemingly surprising programming errors. Therefore, we will briefly discuss how to convert data types in Python. Furthermore, we point out the difference between deep and shallow copies, a delicate and important topic that is a common cause of errors, especially when passing variables to functions. As said, we only touch on these topics by giving concrete examples. To get familiar with the Python syntax, we recommend that you do the small exercises on list (Exercise 1) and dictionary manipulations (Exercise 2). For more comprehensive tutorials, we refer to the following sources:

- The Python Tutorial introduces basic concepts and features of the Python language and system.
- The Scipy Lecture Notes introduce the scientific Python ecosystem including libraries such as NumPy, Scipy, and Matplotlib.
- The FMP Nootbooks are a collection of Python notebooks on Fundamentals of Music Processing (FMP).

## Basic Facts¶

- Python is an interpreted (not compiled), open-source, multi-platform programming language.
- There exist several 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()`.

## Variables and Basic Operators¶

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_variable1 = 'Welcome to the Python Tutorial'
    print(string_variable1)

    Welcome to the Python Tutorial

This is how you can print some basic math:

In [2]:

    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 [3]:

    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 [4]:

    n = 7
    n += 11
    print('Addition:', n)

    n *= 2
    print('Multiplication:', n)

    n /= 18
    print('Division:', n)

    n **= 0.5
    print('Exponentiation:', n)

    Addition: 18
    Multiplication: 36
    Division: 2.0
    Exponentiation: 1.4142135623730951

If you want more control over the formatting of your printed output, we recommend using the f-String format convention.

In [5]:

    print(f'This is how to print a number: {17}.')
    string_variable2 = 'ABCD'
    print(f'This is how to print a string: {string_variable2}.')
    print(f'This is how to print a number (enforcing six digits): {1234:6d}.')
    print(f'This is how to print a floating point number with three decimals: {3.14159265359:6.3f}.')

    This is how to print a number: 17.
    This is how to print a string: ABCD.
    This is how to print a number (enforcing six digits):   1234.
    This is how to print a floating point number with three decimals:  3.142.

## Lists and Tuples¶

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_list = [1, 2, 3]
    print('Print list:', var_list)
    var_list[0] = -1
    print('Alternate item:', var_list)
    var_list.append(10)
    print('Append item:', var_list)
    var_list = var_list + ['a', '12', [13, 14]]
    print('Concatenate two lists:', var_list)
    print('Last element of list: ', var_list[-1])

    print('Remove an item by index and get its value:', var_list.pop(2))
    var_list.remove([13, 14])
    print('Remove an item by value:', var_list)
    del(var_list[2:4])
    print('Remove items by slice of indices:', var_list)

    Print list: [1, 2, 3]
    Alternate item: [-1, 2, 3]
    Append item: [-1, 2, 3, 10]
    Concatenate two lists: [-1, 2, 3, 10, 'a', '12', [13, 14]]
    Last element of list:  [13, 14]
    Remove an item by index and get its value: 3
    Remove an item by value: [-1, 2, 10, 'a', '12']
    Remove items by slice of indices: [-1, 2, '12']

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_list = [11, 12, 13, 14, 15]
    print('var_list =', var_list)
    print('var_list[0:3] =', var_list[0:3])
    print('var_list[1:3] =', var_list[1:3])
    print('var_list[-1] =', var_list[-1])
    print('var_list[0:4:2] =', var_list[0:4:2])
    print('var_list[0::2] =', var_list[0::2])
    print('var_list[::-1] =', var_list[::-1])

    var_list = [11, 12, 13, 14, 15]
    var_list[0:3] = [11, 12, 13]
    var_list[1:3] = [12, 13]
    var_list[-1] = 15
    var_list[0:4:2] = [11, 13]
    var_list[0::2] = [11, 13, 15]
    var_list[::-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_list = [1, 2]
    [a, b] = var_list
    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
    a, b = t
    print(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). A range can then be converted into a tuple or list.

In [11]:

    print(range(9))
    print(range(1, 9, 2))
    print(list(range(9)))
    print(tuple(range(1, 9, 2)))
    print(list(range(9, 1, -1)))

    range(0, 9)
    range(1, 9, 2)
    [0, 1, 2, 3, 4, 5, 6, 7, 8]
    (1, 3, 5, 7)
    [9, 8, 7, 6, 5, 4, 3, 2]

## Boolean Values¶

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

In [12]:

    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 [13]:

    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

## Sets¶

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 [14]:

    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}

## Dictionaries¶

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 example and introduces some basic operations.

In [15]:

    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 dictionary via a key:', dic['b'])
    print('Print the values of the dictionary:', list(dic.values()))

    Print the dictionary dic: {'a': 1, 'b': 2, 3: 'hello'}
    Print the keys of dic: ['a', 'b', 3]
    Access the dictionary via a key: 2
    Print the values of the dictionary: [1, 2, 'hello']

## Python Type Conversion¶

Python offers many different numerical types and methods for type conversion. The standard numeric types in Python are `int`, `float`, and `complex`. The function `type()`can be used to identify the type of a variable. One can use the methods `int()`, `float()`, and `complex()` to convert from one type to another. This is demonstrated in the following code cell.

Note:

- Type conversions from `float` to `int` may result in some rounding.
- One cannot directly convert from the type `complex` to `int` or `float`.

In [16]:

    a = 1
    print('a =', a, type(a))
    b = float(a)
    print('b =', b, type(b))
    c = 2.2
    print('c =' , c, type(c))
    d = int(c)
    print('d =', d, type(d))
    e = complex(d)
    print('e =', e, type(e))
    f = complex(a, c)
    print('f =', f, type(f))

    a = 1 <class 'int'>
    b = 1.0 <class 'float'>
    c = 2.2 <class 'float'>
    d = 2 <class 'int'>
    e = (2+0j) <class 'complex'>
    f = (1+2.2j) <class 'complex'>

## Shallow and Deep Copy Operations¶

Dealing with objects such as lists, tuples, dictionaries, or sets can be trickier as one may think. In particular, using the assignment operator `=` may only create a pointer from variable to an object. As a result, two variables may point to the same object, which may lead to unexpected modifications. This effect is illustrated by the following example.

In [17]:

    a = [1, 2, 3]
    print('a = ', a, ', id(a) = ', id(a))
    b = a
    b[0] = 0
    b.append(4)
    print('b = ', b, ', id(b) = ', id(b))
    print('a = ', a, ', id(a) = ', id(a))

    a =  [1, 2, 3] , id(a) =  4366409792
    b =  [0, 2, 3, 4] , id(b) =  4366409792
    a =  [0, 2, 3, 4] , id(a) =  4366409792

This example shows that the assignment `b = a` does not create a copy of the list. The variables `a` and `b` point to the same object, whose identifier (unique integer) is revealed by the function `id()`. To create copies of an object, one can use the python module `copy`, which is imported by the statement `import copy`. There are different types of copies called shallow copy and deep copy, which become important when dealing with compound objects (objects whose elements are again objects such as lists of lists).

- A shallow copy creates a new compound object. However, if the entries are again compound objects, only links are created.
- A deep copy creates a new compound object, where new objects are created for all entries (and recursively for their entries).

The two cases are illustrated by the subsequent example.

In [18]:

    import copy

    a = [[1, 2, 3], [4, 5, 6]]
    b = copy.copy(a)
    b[0] = 0
    print('a = ', a)
    print('b = ', b)
    b[1][0] = 0
    print('a = ', a)
    print('b = ', b)

    a = [[1, 2, 3], [4, 5, 6]]
    c = copy.deepcopy(a)
    c[1][0] = 0
    print('a = ', a)
    print('c = ', c)

    a =  [[1, 2, 3], [4, 5, 6]]
    b =  [0, [4, 5, 6]]
    a =  [[1, 2, 3], [0, 5, 6]]
    b =  [0, [0, 5, 6]]
    a =  [[1, 2, 3], [4, 5, 6]]
    c =  [[1, 2, 3], [0, 5, 6]]

## Exercises and Results¶

In [19]:

    import libpcp.python
    show_result = True

Exercise 1: Basic List Manipulations

- In the following, we assume that a student is specified by a list containing a student ID (integer), last name (string), and first name (string). Create a list of students, which contains the following entries: `[123, 'Meier', 'Sebastian']`, `[456, 'Smith', 'Walter']`. (Note that this becomes a list of lists.)
- Add to this list the student `[789, 'Wang', 'Ming']`.
- Print out the student list in reversed order (descending student IDs).
- Print out only the first name of the second student in this list.
- Print the length of the list (using the `len` function).
- Make a deep copy of the list and then remove the first and second student from the copied list (using the `del` statement). Furthermore, change the student ID of the remaining student from `789` to `777`. Check if the original list has been modified.

In [20]:

    #<solution>
    # Your Solution
    #</solution>

In [21]:

    libpcp.python.exercise_list(show_result=show_result)

    [[123, 'Meier', 'Sebastian'], [456, 'Smith', 'Walter']]
    [[123, 'Meier', 'Sebastian'], [456, 'Smith', 'Walter'], [789, 'Wang', 'Ming']]
    [[789, 'Wang', 'Ming'], [456, 'Smith', 'Walter'], [123, 'Meier', 'Sebastian']]
    Walter
    3
    [[777, 'Wang', 'Ming']]
    [[123, 'Meier', 'Sebastian'], [456, 'Smith', 'Walter'], [789, 'Wang', 'Ming']]

Exercise 2: Basic Dictionary Manipulations

- Again, we assume that a student is specified by student ID (integer), last name (string), and first name (string). Create a dictionary, where a key corresponds to the student ID and a value to a list of the last name and first name. Start with a dictionary with key `123` and value `['Meier', 'Sebastian']` as well as key `456` and value `['Smith', 'Walter']`.
- Add the student with key `789` and value `['Wang', 'Ming']`.
- Print out a list of all keys of the dictionary.
- Print out a list of all values of the dictionary.
- Print out the last name of the student with key `456`.
- Remove the student with key `456` (using the `del` statement).
- Print the length of the dictionary (using the `len` function).

In [22]:

    #<solution>
    # Your Solution
    #</solution>

In [23]:

    libpcp.python.exercise_dict(show_result=show_result)

    {123: ['Meier', 'Sebastian'], 456: ['Smith', 'Walter']}
    {123: ['Meier', 'Sebastian'], 456: ['Smith', 'Walter'], 789: ['Wang', 'Ming']}
    [123, 456, 789]
    [['Meier', 'Sebastian'], ['Smith', 'Walter'], ['Wang', 'Ming']]
    Smith
    {123: ['Meier', 'Sebastian'], 789: ['Wang', 'Ming']}
    2