# Lists In our first session, we encountered several distinct data **types**: `int` (integers), `float` (decimal point numbers), `complex` (complex numbers), `str` (strings) and `bool` (booleans). All of these types represent various categories of **individual data**: a single number, a single string of letters etc. Often, we are interested in **collections** of data, for example a set of temperatures at which a given experiment has been performed. Python accommodates this requirement by providing us with various **data structures** that allow us to collate related data into a single object. **Lists** are one of these data structures. ## List basics A **list** is an **ordered** collection of values. The values that comprise a list are usually referred to as the **elements** or **items** in that list. To create a list, we enclose the elements (separated by commas) we wish to include in a set of **square brackets**: ```python [1, 2, 3, 4, 5] ``` ``` [1, 2, 3, 4, 5] ``` Just like the other data types we have encountered so far, we can store lists in **variables** with the **assignment** operator `=` ```python some_numbers = [1, 2, 3, 4, 5] print(some_numbers) print(type(some_numbers)) ``` ``` [1, 2, 3, 4, 5] list ``` where see that there is a `list` `type`. This is an example of a `list` of `int`, but **lists** can contain any type of data ```python some_strings = ['Welcome', 'to', 'CH12002'] print(some_strings) ``` ``` ['Welcome', 'to', 'CH12002'] ``` We can mix **different types** together ```python a_variety = [5, 'five', 5.0, 5.0 + 0j] print(a_variety) ``` ``` [5, 'five', 5.0, (5+0j)] ``` and we can even make a **list of lists**! ```python some_numbers = [1, 2, 3, 4, 5] some_strings = ['Welcome', 'to', 'CH12002'] list_of_lists = [some_numbers, some_strings] print(list_of_lists) ``` ``` [[1, 2, 3, 4, 5], ['Welcome', 'to', 'CH12002']] ``` We refer to a **list of lists** as a **nested** `list`. ## List indexing How can we access a single entry in a list? Consider the following example which lists the names of some first-row transition metals ```python transition_metals = ['vanadium', 'chromium', 'iron'] ``` If we want the first entry in the list we can obtain it using **list indexing** as follows ```python transition_metals[0] ``` ``` 'vanadium' ``` To index a list we use square brackets `[]` immediately after the name of the list - no spaces are allowed! Inside the square brackets we put an integer corresponding to the index of the element we want. In programming, we refer to the "entries" of a data structure as its **elements**. In Python, we start counting the **elements** of a data structure from the number zero. The first element of a data structure has index `0`, the second `1`, and so on. This might be a bit confusing, but you'll get used to it. ::: {.callout-note icon="false" title="Syntax"} The general **syntax** for list indexing is ```python name_of_list_variable[index] ``` where `index` is an `int` $\ge 0$. ::: What happens if we try and get the 11th index of `transition_metals`? There are only three elements, so we get an error! ```python transition_metals[10] ``` ``` --------------------------------------------------------------------------- IndexError Traceback (most recent call last) Cell In[11], line 1 ----> 1 transition_metals[10] IndexError: list index out of range ``` ::: {.callout-tip icon='false' title='Error'} Here we have another new type of **error**: an `IndexError`. This one is relatively straightforward, the index we have given is "out of range" and so refers to an element that does not exist. In this case, the index `10` refers to the **eleventh** element of the list, but `transition_metals` only has `3` items, so our code doesn't make any sense! ::: We can get the length of a `list` using the `len` function ```python len(transition_metals) ``` 3 To finish off this discussion of list indexing, lets see what happens if we use a negative index. ```python transition_metals[-1] ``` ``` 'iron' ``` In real life, the "minus first" element doesnt exist, but in Python this takes on a new meaning - the negative sign tells the computer to start **counting backwards from the end of the list**. So an index of `-1` refers to the **last** element, `-2` refers to the second-to-last, and so on. To index a list of lists we use a new set of brackets `[]` for each list. Look at the following list ```python opposites = [['proton', 'electron'], ['positive', 'negative'], ['oxidise', 'reduce']] ``` this contains three lists, each with two elements. Say I want to get the first element of the second list - this would be ```python opposites[1][0] ``` which tells Python to take the second list (with index `1`) and get its first element (index `0`). ## Modifying lists ### Replacing elements List indexing can be used to modify the **elements** of a list. This is possible because `list` objects are what we call **mutable** - they can change or **mutate**. Let's replace the third element of `transition_metals` with `manganese` ```python transition_metals = ['vanadium', 'chromium', 'iron'] print(transition_metals) transition_metals[2] = 'manganese' print(transition_metals) ``` ``` ['vanadium', 'chromium', 'iron'] ['vanadium', 'chromium', 'manganese'] ``` ::: {.callout-tip} As we've seen, you could use an index of `-1` instead. ::: ### Adding new elements We can also make lists longer by **appending** values. To do this, we use the `append` **method** of the list. A **method** is a **function** which is attached to a particular object. They're used somewhat similarly, but the **syntax** differs mostly in the order in which each command is written. Let's compare `len` with `append` to see the difference ```python transition_metals = [ 'vanadium', 'chromium', 'managanese' ] len(transition_metals) transition_metals.append('iron') print(transition_metals) ``` ``` ['vanadium', 'chromium', 'managanese', 'iron'] ``` We use the method `append` by writing `.append` immediately after the name of the `list`, and we then feed in our inputs to `append` - here we just append the string `'iron'`. ::: {.callout-note icon="false" title="Syntax"} The general **syntax** for a method is ```python name_of_object.name_of_method(input_to_method) ``` ::: As well as appending elements to the end of a list, we can also **insert** elements at **any** given index in the list using the `insert` method ```python transition_metals.insert(0, 'titanium') print(transition_metals) ``` ``` ['titanium', 'vanadium', 'chromium', 'managanese', 'iron'] ``` notice that `insert` has two arguments - the first is the index where the new element will be located, and the second is the value to use for that new element. Both `insert` and `append` allow us to add single elements, whereas the `extend` method allows us to add mutiple elements to the end of a list ```python more_transition_metals = ['cobalt', 'nickel'] transition_metals.extend(more_transition_metals) print(transition_metals) ``` ``` ['titanium', 'vanadium', 'chromium', 'managanese', 'iron', 'cobalt', 'nickel'] ``` where we have providedn a new `list` to stick onto the end of our current `list`. Finally, now that you know about `append`, `insert`, and `extend`, its important to know that you can create an empty list ```python empty_list = [] print(empty_list) ``` ``` [] ``` and can append values to it ```python empty_list.append('absolutely_nothing!') print(empty_list) ``` ``` ['absolutely_nothing!'] ``` ### Exercise That's a lot about lists, but there's more to come! As a "break", play around with the following operations, just like you did for [strings](../session_1/types.qmd#exercise). Create a list containing the letters a, b, and c and carry out the following operations 1. Multiply your list by the integer `3` 2. Multiply your list by the float `3.5` 3. Add the integer `5` to your list using the `+` operator 3. Add the list `['d', 'e']` to your list using the `+` operator 4. Subtract the integer `10` from your list using the `-` operator Some of the above will give an error, but some will give results that look similar to what we've just seen for append and extend. ::: {.callout-tip title="Result" collapse="true" icon="false"} 1. Multiplying a list by an integer `N` makes a new list with `N` copies of the element - in this case `['a', 'b', 'c', 'a', 'b', 'c', 'a', 'b', 'c']`. 2. Multiplying a list by an float gives an error - this operation is not allowed. 3. Adding an integer to a list gives an error - this operation is not allowed. 4. Adding a list to a list using `+` gives the same result as `extend`. 5. Subtracting anything from a list gives an error - this operation is not allowed. ::: ### Removing elements We now have a range of tools at our disposal for **adding** elements to a list, but how can we **remove** them instead? Well, for a start, there's the `remove` method ```python transition_metals.remove('cobalt') print(transition_metals) ``` ``` ['titanium', 'vanadium', 'chromium', 'managanese', 'iron', 'nickel'] ``` Thiss removes the first element which matches the input argument from the list. Alternatively, there's the `pop` method, which allows you to instead specify the **index** of the element you want to remove. ```python pop_example = transition_metals.pop(5) print(pop_example) print(transition_metals) ``` ``` 'cobalt' ['titanium', 'vanadium', 'chromium', 'managanese', 'iron', 'nickel'] ``` Notice that the `pop` method also **returns** the value of the removed element. In the example above, we have **assigned** the output of the `pop` method to a variable `pop_example`, which when printed, shows us the removed element. ## List slicing We are now familiar with accessing individual elements of a list, but what if we want to obtain **multiple** elements at once? Python facilitates this task with a process called **list slicing**. ```python transition_metals = ['titanium', 'vanadium', 'chromium', 'managanese', 'iron', 'cobalt', 'nickel'] shorter_list = transition_metals[1:5] print(shorter_list) ``` ``` ['vanadium', 'chromium', 'manganese', 'iron'] ``` We've specified two indices `1` and `5` separated by a colon `:`, and Python has returned a **slice** containing four elements from our the list - the 2nd, 3rd, 4th, and 5th elements. Interestingly, specifying `5` as the second index doesnt result in the 6th element being included in the slice - to explain why take a look at the box below. ::: {.callout-note icon="false" title="Syntax"} The general **syntax** for slicing is ```python name_of_list[start_index : end_index] ``` where the element at `end_index` is **not** included in the slice that is returned. Think of this as saying "give me all the elements starting at `start_index` up to and **not** including `end_index`". ::: In actual fact, we don't have to include **both** the start and end indices - have a go and see what happens using the following example. ```python transition_metals = ['titanium', 'vanadium', 'chromium', 'managanese', 'iron', 'cobalt', 'nickel'] print(transition_metals[3:]) print(transition_metals[:6]) ``` ::: {.callout-tip title='Result' collapse="true" icon=false} In the first case, we specify only a start index `3` - this tells python to give us every element between index `3` to the **end** of the list. In the second case, we specify only an end index `6` - this tells python to give us every element from the **start** of the list up to, but **not** including, index `6`. ::: Finally, we can also slice a list in **steps** by providing a third index which tells python the **step size**. ```python transition_metals = ['titanium', 'vanadium', 'chromium', 'managanese', 'iron', 'cobalt', 'nickel'] transition_metals[1:5:2] ``` ``` ['vanadium', 'managanese'] ``` which gives us every second element from the 2nd up to but **not** including the 6th - i.e. our slice will contain the 2nd and 4th elements. ::: {.callout-note icon="false" title="Test Yourself" collapse="true"} Use list indexing with steps to return the every fourth element of the following list. ```python d_block = ['Scandium', 'Titanium', 'Vanadium', 'Chromium', 'Manganese', 'Iron', 'Cobalt', 'Nickel', 'Copper', 'Zinc', 'Yttrium', 'Zirconium', 'Niobium', 'Molybdenum', 'Technetium', 'Ruthenium', 'Rhodium', 'Palladium', 'Silver', 'Cadmium'] ``` ::: {.callout-tip title="Answer" collapse="true" icon="false"} ```python print(d_block[::4]) ``` ::: ::: ## Exercise Use your knowledge of lists to solve the following problem Copy the following block of code into a Jupyter notebook: ```python colours = [['Red', 'Magenta'], ['Cyan', 'Green'], [['Yellow', 'Blue'], 'White']] ``` a) Use **list indexing** to print `Magenta` from the `colours` list. b) Again, using **list indexing**, assign the element `Red` to a variable called `colour_1`, the element `Green` to a variable called `colour_2`, and the element `Yellow` to a variable called `colour_3`, so that the following line of code runs without error and displays a true statement: ```python print(f'{colour_1} + {colour_2} -> {colour_3}') ``` ::: {.callout-tip collapse="true"} The solutions are **not** to just type out ```python print('Magenta') print(f'Red + Green -> Yellow') ``` you need to use **list indexing**! :::