<jupyter_start><jupyter_text>Python's Advanced Data Types and the Concept of "Call by Reference"  list In Python, list is an ordered conglomerate of data. The data can be of different data types:<jupyter_code># Syntax: [element_1, element_2, ...]
list_variable = [1, "a", None, True]
list_variable<jupyter_output><empty_output><jupyter_text>IndexingImportant difference to MATLAB: The first element has the index zero! Using brackets after the list, one can access the nth element using a positive number or 0 inside the brackets:<jupyter_code># Let us get the first element :D
list_variable = [1, "a", None, True]
list_variable[0]  # Indexing starts with zero :O
# Let us get the second element :-)
list_variable = [1, "a", None, True]
list_variable[1]<jupyter_output><empty_output><jupyter_text>Using a negative number, the nth-last (where n is the absulute value of the negative number)  element of the list can be accessed:<jupyter_code># Let us get the last element
list_variable = [1, "a", None, True]
list_variable[-1]
# Let us get the 3rd-last element
list_variable = [1, "a", None, True]
list_variable[-3]<jupyter_output><empty_output><jupyter_text>Slicing (yes the indexing is confusing 😐) With slicing, you can access a given part of the list. Just like indexing, it works by using brackets, this time with a ":" (colon) inside and an indexing number before the ":" (this is the first sliced element, included) and an indexing number afterwards (this is the last selected element, *not* included):<jupyter_code># Let us get the 2nd and the 3rd element \o/
list_variable = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
list_variable[1:3]  # The first number is the first selected element (included), the 2nd number the last selected element (not included)
# Let us get the 2nd up to the 2nd-last element
list_variable = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
list_variable[1:-1]  # The first number is the first selected element (included), the 2nd number the last selected element (not included)<jupyter_output><empty_output><jupyter_text>By leaving out a number *before* the ":", all elements up to the element specified in the number after the ":" are selected:<jupyter_code># Let us get the all elements up to the 3rd-last one
list_variable = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
list_variable[:-2]  # No first number -> start from the first element; the 2nd number the last selected element (not included)<jupyter_output><empty_output><jupyter_text>By leaving out a number *after* the ":", all elements from to the element specified in the number before the ":" are selected:<jupyter_code># Let us get the all elements starting from the 4th one
list_variable = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
list_variable[3:]  # The 1st number is the first selected element (included); No 2nd number -> end with the last element<jupyter_output><empty_output><jupyter_text>The "in" logic operator With the "in" logic operator, one can check if a given element is included in a list:<jupyter_code># If the element is in the list, "in" returns True
1 in [1, 2, 3]
# If the element is *not* in the list, "in" returns False
0 in [1, 2, 3]<jupyter_output><empty_output><jupyter_text>Changing elements in a list Changing lists simply works by selecting an element (or a slice) using brackets and redefining its value:<jupyter_code># Let us change the 2nd element of the list :D
list_variable = ["A", "X", "C", "D"]
list_variable[1] = "B"
list_variable<jupyter_output><empty_output><jupyter_text>Removing or appending elements from a list Using the list-internal functions .append() and the Python base function del(), one can easily add or delete elements of a list:<jupyter_code># Let us append something to the list :-)
list_variable = [1, 2, 3]
list_variable.append(4)
list_variable
# Let us remove the 2nd element of the list :O
list_variable = [1, 2, 3, 4, 5]
del(list_variable[1])
list_variable<jupyter_output><empty_output><jupyter_text>tuple Tuples are like lists, with two differences:1. They are defined using "(" instead of "["2. They are immutable, i.e., they cannot be changed<jupyter_code>tuple_variable = (1, "a", None, True)
tuple_variable
# Trying to change the tuple won't work D:
tuple_variable = (1, "a", None, True)
tuple_variable[1] = 2<jupyter_output><empty_output><jupyter_text>dict (dictionary/hash map) A dictionary associates a value (the key) with another value (confusingly called the "value" again xD):<jupyter_code># Let us define a dictionary where we associate cities with their population
# Syntax:
# {
#   key: value,
#   ...
#   key: value,
# }
# So in our case, the city names are the *key*, the population numbers the *value*
city_to_population_dictionary = {
    "Berlin": 3e6,
    "Athens": 5e6,
    "Paris": 12e6, # A comma at the end is ok in newer Python versions :-)
}

# Let us print the population of... Athens
print("Population of Athens:")
print(city_to_population_dictionary["Athens"])

# And now the population of... Paris
print("Population of Paris:")
print(city_to_population_dictionary["Paris"])<jupyter_output>Population of Athens:
5000000.0
Population of Paris:
12000000.0<jupyter_text>values can be of any type, however, keys can only be of any type that is *immutable* (i.e., values that are not made to be changed):<jupyter_code># A small collection of hashable types, including tuples :D
hashable_keys_dict = {
    "A": "A",
    ("A","B"): True,
    None: [2,3,4],
    1: 2,
}

print(hashable_keys_dict["A"])
print(hashable_keys_dict[("A","B")])
print(hashable_keys_dict[None])
print(hashable_keys_dict[1])<jupyter_output>A
True
[2, 3, 4]
2<jupyter_text>Lists are *not* immutable, because their value can be changed. Tehrefore, they cannot be keys:<jupyter_code>list_as_key_test_dictionary = {
    [1,2,3]: 1
}<jupyter_output><empty_output><jupyter_text>Get keys or values of a dictionary...using their built-in functon .keys() or .values():<jupyter_code>city_to_population_dictionary = {
    "Berlin": 3e6,
    "Athens": 5e6,
    "Paris": 12e6,
}
print("Keys:")
keys_as_list = list(city_to_population_dictionary.keys())
print(keys_as_list)  # keys are ordered :D (or D: if you are a computer scientist)
print("Values:")
values_as_list = list(city_to_population_dictionary.values())
print(values_as_list)<jupyter_output>Keys:
['Berlin', 'Athens', 'Paris']
Values:
[3000000.0, 5000000.0, 12000000.0]<jupyter_text>Attention: In older Python versions, dict keys are not ordered! You have to use collections.orderedDict instead (more on modules on the next course day :-)  The Concept of "Call by Reference" Ok this is actually one the most unintuitive concept of Python D:, even though there are good reasons for it :-) If we assign a variable A to another variable B which is a...1. list2. dict3. or any other "complex" type of variable...both of these variables are linked to the *same* data, i.e. if I change variable B, the data to which variable A is linked will be changed too :O<jupyter_code># Let us see this in practice :-)
# So we have a variable A (in this case a list)...
list_a = [1, 2, 3]
# ...and intend to "copy" A as variable B
list_b = list_a
# Now, if we change B...
list_b[0] = 0
# ...A is changed, too :O
print(list_b)
print(list_a)<jupyter_output>[0, 2, 3]
[0, 2, 3]
{'fraction_non_alphanumeric': 0.1081680765497157, 'fraction_numerical': 0.025655248925253085, 'mean_word_length': 5.169375534644995, 'pattern_counts': {'":': 13, '<': 57, '<?xml version=': 0, '>': 59, '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': '15', 'dir': 'jupyter-structured-clean-dedup', 'id': '270582', 'license': 'MIT', 'n_tokens_mistral': 2516, 'n_tokens_neox': 2326, 'n_words': 952, 'path': '6_Advanced_Data_Types_and_Call_by_Reference.ipynb', 'repo_name': 'Paulocracy/python_tutorials', 'size': '7211'}