<jupyter_start><jupyter_text>ListsUp until now we have dealt with individual pieces of data, such **integer** or **floating point** data types to store numerical data, or **strings** to represent textual data.Often we want to work with **collections** of data, such as a time-series of concentrations from an experiment, or a set of yields obtained from different experimental protocols, or the list of students enrolled in **CH40208 Topics in Computational Chemistry**.A **list** is an **ordered** collection of values. The values that make up a **list** are called **elements** or **items**, so we might talk of a list that contains 4 **items**, or the 2nd **element** in a list. Creating listsA list in Python is written as a sequence of values separated by commas and enclosed by square brackets.<jupyter_code>[1, 1, 2, 3, 5, 8, 13]<jupyter_output><empty_output><jupyter_text>We can assign a list to a variable the same way as with single-value datatypes:<jupyter_code>fibonnaci = [1, 1, 3, 4, 5, 8, 13]
print(fibonnaci)
type(fibonnaci)<jupyter_output><empty_output><jupyter_text>A list can contain any datatype:<jupyter_code>nobel_gases = ['helium', 'Neon', 'Argon']
print(nobel_gases)
type(nobel_gases)<jupyter_output><empty_output><jupyter_text>Even a mixture of different datatypes:<jupyter_code>miscellaneous = [42, "mushroom", 3.145, 2+3j]
print(miscellaneous)
type(miscellaneous)<jupyter_output><empty_output><jupyter_text>Or other lists:<jupyter_code>inner_list = ['a', 'b', 'c']
outer_list = ['e', inner_list, 'f']
print(outer_list)<jupyter_output>['e', ['a', 'b', 'c'], 'f']<jupyter_text>This last example would be described as a **nested** list (a list inside another list). List indexingIndividual elements of a list can be referred to by using **list indexing**. For example, the first item in our `nobel_gases` list can be accessed using<jupyter_code>nobel_gases[0]<jupyter_output><empty_output><jupyter_text>The second item using:<jupyter_code>nobel_gases[1]<jupyter_output><empty_output><jupyter_text>And the third item using:<jupyter_code>nobel_gases[2]<jupyter_output><empty_output><jupyter_text>Note that to reference the 1st element we use `[0]` after the list variable name, to reference the 2nd element we use `[1]`, and to reference the 3rd element we use `[2]`. This convention is because the list index describes the number of entries **from the start of the list**. i.e. the first entry is **zero** entries from the start.Trying to access a list element that does not exist raises an `IndexError`:<jupyter_code>nobel_gases[3]<jupyter_output><empty_output><jupyter_text>We can also refer to individual elements counting backwards from the end of the list, using **negative indexing**:<jupyter_code>nobel_gases[-1]
nobel_gases[-2]
nobel_gases[-3]<jupyter_output><empty_output><jupyter_text>Using positive and negative indexing to refer to individual elements of the `nobel_gases` list. Modifying listsAn important feature of **lists** is that they are **mutable**—values in the list can be changed; new elements can be added; existing elements can be deleted.You might have noticed that the first of our noble gases is `"helium"`, which is missing a capital letter. We can fix that by assigning a new value to `nobel_gases[0]`<jupyter_code>nobel_gases[0] = "Helium"
print(nobel_gases)<jupyter_output>['Helium', 'Neon', 'Argon']<jupyter_text>Adding elements to listsLists have a number of special functions (called **methods**) that we can use to add elements to a pre-existing list. A **method** is somewhat similar to a **function**, except it is associated with a particular object. Using a method lets us implicitly refer to the associated object. Calling a method of a list implicitly refers to that list. For example, to append another nobel gas string to our `nobel_gases` list we can use the `append()` method:<jupyter_code>nobel_gases = ['Helium', 'Neon', 'Argon']
nobel_gases.append('Krypton')
print(nobel_gases)<jupyter_output>['Helium', 'Neon', 'Argon', 'Krypton']<jupyter_text>This is equivalent to a function that takes **two** arguments: the list we want to append to, and the object to be appended. For example, we might imagine an `append_to_list()` function that works as follows (note that this `append_to_list()` function is purely imagined and doesn't exist in native Python):<jupyter_code>def append_to_list(the_list, object):
    return the_list.append(object)
nobel_gases = ['Helium', 'Neon', 'Argon']
append_to_list(nobel_gases, 'Krypton')
print(nobel_gases)<jupyter_output>['Helium', 'Neon', 'Argon', 'Krypton']<jupyter_text>Because the `append()` method belongs to the `nobel_gases` list, when we use `nobel_gases.append()` we don't need to specify the list that we are appending to.Another list method that can be used to add elements is `insert()`, which inserts an object at a given index:<jupyter_code>nobel_gases = ['Helium', 'Neon', 'Krypton']
nobel_gases.insert(2, 'Argon')
print(nobel_gases)<jupyter_output>['Helium', 'Neon', 'Argon', 'Krypton']<jupyter_text>The `insert()` methods takes **two** arguments: the index where the insertion will take place (in this case `2`), and the object to insert (in this case `"Argon"`).You can also **extend** a list with another list, by using the `extend()` method:<jupyter_code>nobel_gases.extend(['Xenon', 'Radon'])
print(nobel_gases)<jupyter_output>['Helium', 'Neon', 'Argon', 'Krypton', 'Xenon', 'Radon']<jupyter_text>Or you can **concatenate** two lists using the `+` operator (this is similar to "adding" strings):<jupyter_code>nobel_gases = ['Helium', 'Neon', 'Argon']
more_nobel_gases = ['Krypton', 'Xenon', 'Radon']
print(nobel_gases + more_nobel_gases)<jupyter_output>['Helium', 'Neon', 'Argon', 'Krypton', 'Xenon', 'Radon']<jupyter_text>Deleting elements from listsLists also have methods for deleting or removing elements. These include `remove()` which removes the first matching object in a list.<jupyter_code>nobel_gases = ['Helium', 'Neon', 'Argon']
nobel_gases.remove('Neon')
print(nobel_gases)<jupyter_output>['Helium', 'Argon']<jupyter_text>List slicesBy referring to a single list index, we can access a single item of a list. Python also allows us to use **slice notation** to access a **range** of items from a list, e.g.<jupyter_code>nobel_gases = ['Helium', 'Neon', 'Argon', 'Krypton', 'Xenon', 'Radon']
print(nobel_gases[1:4])<jupyter_output>['Neon', 'Argon', 'Krypton']<jupyter_text>This gives us just those elements of a list starting from `nobel_gases[1]` up to **but not including** `nobel_gases[4]`.This behaviour, where the first index is included in the slice, but the last index is not included might seem strange at first (you will get used to this with practice and experience). The reasoning behind this behaviour is that a slice `nobel_gases[1:4]` should contain 4-1=3 elements, and should start from the same element as (in this case) `nobel_gases[1]`.Either the first or last index can be left out, in which case you get a slice starting from the first element, or finishing with the last element, respectively.<jupyter_code>nobel_gases = ['Helium', 'Neon', 'Argon', 'Krypton', 'Xenon', 'Radon']
print(nobel_gases[:3])
nobel_gases = ['Helium', 'Neon', 'Argon', 'Krypton', 'Xenon', 'Radon']
print(nobel_gases[3:])<jupyter_output>['Krypton', 'Xenon', 'Radon']<jupyter_text>List slices can also be given a third integer, which describes a **step** size. This is implicitly equal to `1` when it is not specified<jupyter_code>nobel_gases = ['Helium', 'Neon', 'Argon', 'Krypton', 'Xenon', 'Radon']
print(nobel_gases[::2])<jupyter_output>['Helium', 'Argon', 'Xenon']<jupyter_text>In this example. we start from the first element, and go up to the last element, in steps of 2, giving elements 0, 2, and 4.In addition to the `remove()` method above, list slicing gives us another way to "delete" elements from a list:<jupyter_code>nobel_gases = ['Helium', 'Neon', 'Argon', 'Krypton', 'Xenon', 'Radon']
nobel_gases = nobel_gases[1:3]
print(nobel_gases)<jupyter_output>['Neon', 'Argon']<jupyter_text>In the second line here we select elements 1 and 2 from the original `nobel_gases` list, using **slice notation**, which gives us `['Neon', 'Argon']`. We then *assign* this value to the variable name `nobel_gases`, which overwrites the original list. Working with nested listsWe saw above that a list can contain one or more other lists, giving a **nested list**. How then do these work with indexing?Consider the following nested list:<jupyter_code>letters = ['a', 'b', ['c', 'd'], 'e', 'f']
print(letters)<jupyter_output>['a', 'b', ['c', 'd'], 'e', 'f']<jupyter_text>The list `letters` has **five** elements, these are:- `'a'`- `'b'`- the list `['c', 'd']`- `'e'`- `'f'`We can confirm this using the `len()` function. If we pass `len()` a list, it returns the number of elements in that list (i.e. the **length**).<jupyter_code>len(letters)<jupyter_output><empty_output><jupyter_text>The **third** element is the sublist `['c', 'd']`, which we can access using `letters[2]`<jupyter_code>print(letters[2])<jupyter_output>['c', 'd']<jupyter_text>`letters[2]` is a list<jupyter_code>type(letters[2])<jupyter_output><empty_output><jupyter_text>and we can interact with this in the same way as any other list, e.g.<jupyter_code>letters[2][1]
letters[2].append('z')
print(letters)<jupyter_output>['a', 'b', ['c', 'd', 'z'], 'e', 'f']<jupyter_text>Summing over lists using `sum()` (or `.join()`)Python provides a `sum()` function that can be used to &ldquo;sum&rdquo; or add-together all the elements of a list.e.g.<jupyter_code>list_of_numbers = [1, 2, 3, 4, 5]
sum(list_of_numbers)<jupyter_output><empty_output><jupyter_text>This is the same as adding all the elements explicitly:<jupyter_code>list_of_numbers[0] + list_of_numbers[1] + list_of_numbers[2] + list_of_numbers[3] + list_of_numbers[4]<jupyter_output><empty_output><jupyter_text>The `sum()` function only works for lists of **numbers**, so can't be used to (for example) &lqduo;add&rdquo; a list of **strings**. Trying to use `sum()` to add a list of **strings** will raise a `TypeError`:<jupyter_code>message = ["This", "will", "add", "everything", "together"]
sum(message)<jupyter_output><empty_output><jupyter_text>To &ldquo;add&rdquo; lists of strings we can use the `join()` method:<jupyter_code>" ".join(message)<jupyter_output><empty_output><jupyter_text>Note that `join()` is a **string method**, where the string defines what to insert **between** the elements of `message`; in this case we have used a string containing one space, but this could be anything:<jupyter_code>"+".join(message)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.13112283345892992, 'fraction_numerical': 0.00819517709118312, 'mean_word_length': 6.917225950782997, 'pattern_counts': {'":': 0, '<': 110, '<?xml version=': 0, '>': 110, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '31', 'dir': 'jupyter-structured-clean-dedup', 'id': '587663', 'license': 'CC-BY-4.0', 'n_tokens_mistral': 3668, 'n_tokens_neox': 3512, 'n_words': 1239, 'path': 'CH40208/python_basics/lists.ipynb', 'repo_name': 'pythoninchemistry/ch40208', 'size': '10616'}