Iterable Objects (part II)

Try me

Open In ColabBinder

Tuples

Tuples are very similar to lists, except that they are inmutable. This means that, once you declare a tuple variable, you cannot make any changes to it. Tuples are created using parenthesis instead of brackets, like in the example below. Since they are inmutable, we can use tuples to store values that do not change in our program. Tuples are iterable. We can access the members of a tuple using indexing and slicing and use them in control structures.

[ ]:
unknowns = ("x", "y", "z")
# Let's print the first unknown
print("The first unknown is: " + unknowns[0])

Dictionaries

A Dictionary is another data type to store a collection of data (like arrays).

A dictionary works with keys and values. Values are accessed using keys (instead of indexes). In fact, we can think of lists as dictionaries that use integers as keys. Dictionaries can be initialized as empty dictionaries, using curved brackets ({}), and then using brackets to add key, value pairs as in the following example:

[4]:
contacts = {}
contacts["Paco"] = 655555555
contacts["Pepe"] = 666555111
contacts["Pili"] = 677777555
print(contacts)
{'Paco': 655555555, 'Pepe': 666555111, 'Pili': 677777555}

Dictionaries can also be initialized using JSON notation:

[1]:
contacts = {
    "Paco": 655555555,
    "Pepe": 666555111,
    "Pili": 677777555
}
print(contacts)
{'Paco': 655555555, 'Pepe': 666555111, 'Pili': 677777555}

Removing elements

Elements can be removed using the keyword del or the function pop of a dictionary.

[8]:
contacts = {
    "Paco": 655555555,
    "Pepe": 666555111,
    "Pili": 677777555
}
del contacts["Pepe"]
print(contacts)

contacts.pop("Pili")
print(contacts)
{'Paco': 655555555, 'Pili': 677777555}
{'Paco': 655555555}

Search in a dictionary

Use keyword in to find a key in a dictionary:

[10]:
contacts = {
    "Paco": 655555555,
    "Pepe": 666555111,
    "Pili": 677777555
}
if "Paco" in contacts:
    print("Paco has been found within your contacts")
Paco has been found within your contacts

The method values() returns the values of the dictionary, so we can use it to search a specific value in the dictionary.

[3]:
if 655555555 in contacts.values():
    print("the number has been found in the contact list")
the number has been found in the contact list

For loops with dictionaries

Dictionaries allow for different ways to iterate. By default, if we pass the dictionary to the for clause, the variable will take the value of each key member. You can then use the keys to access the values in the dict:

[9]:
contacts = {
    "Paco": 655555555,
    "Pepe": 666555111,
    "Pili": 677777555
}

for contact in contacts:
    print(contact)
    print("The contact name is ", contact, "and the number", contacts[contact])
Paco
The contact name is  Paco and the number 655555555
Pepe
The contact name is  Pepe and the number 666555111
Pili
The contact name is  Pili and the number 677777555

You can use the dictionary method values() to get a list of the values and iterate only over the values (if this is what you are into).

[10]:
contacts = {
    "Paco": 655555555,
    "Pepe": 666555111,
    "Pili": 677777555
}

for contact in contacts.values():
    print("The phone number", contact, "is in the contact list")

The phone number 655555555 is in the contact list
The phone number 666555111 is in the contact list
The phone number 677777555 is in the contact list

We can iterate over keys and values defining two variables in the for clause and using the function items() which returns a list of tuples for every key value pair of the dictionary:

[7]:
contacts = {
    "Paco": 655555555,
    "Pepe": 666555111,
    "Pili": 677777555
}
for k, v in contacts.items():
    print("The name of the contact is", k, "and the phone number", v)
The name of the contact is Paco and the phone number 655555555
The name of the contact is Pepe and the phone number 666555111
The name of the contact is Pili and the phone number 677777555

Nested iterables

We can nest iterables, one inside the other, for instance, to build a list of lists, or a list of dictionaries:

[13]:
# my_nested_list_1 is a list of lists
my_nested_list_1 = [[1, 2],
             [3, 4]]
# Note that it is very similar to a matrix!

# my_nested_list_2 is a list of dictionary that contains data from different students. Each dictionary contains the data of a student
my_nested_list_2 = [{"name": "Pepe", "age":19},
                    {"name": "Ingrid", "age": 18}]

# my_nested_list_3 is similar to my_list_2, but each dictionary contains a list of favourite colors.
my_nested_list_3 = [{"name": "Pepe", "age":19, "favourite_colors": ["Orange", "Blue"]},
                    {"name": "Ingrid", "age": 18, "favourite_colors": ["Orange", "Blue"]}]

Indexing will work, just the same. Taking the first example, the first member of the list can be accessed with my_nest_list_1[0] and is also a list, so, we can access the first member using ```my_nest_list_1[0][0]````:

[14]:
print(my_nested_list_1[0]) #This returns the first (row) list
print(my_nested_list_1[0][0]) #And from the first list, we can list the first number
[1, 2]
1

Similarly, we can access the value of the ‘name’ key in the second member of my_nest_list_2 using a similar procedure:

[15]:
print(my_nested_list_2[1]["name"])
Ingrid

Can you use the skills that you build to collect the names and grades of a group of students and calculate their average grade? Try to complete the following template to achieve this:

[16]:
students = [] #This list will hold the data
keys = ("name", "grade") # This tuple is used to create the dictionaries with the data of each student
while True:
    response = input("Do you want to enter a new student in the list? (Y/N)")
    if response == "Y":
        student = {}
        for key in keys:
            value = input("Enter the students' " + key)
            student[key] = value
        students.append(student)
    elif response == "N":
        break
    else:
        print("I did not understand your response, please enter Y for yes and N for No")

Comprehension

Comprehension is a nice feature of Python that allows to create iterables in an efficient way. Comprehension uses a for loop in the initialisation of the iterable:

[14]:
keys = ('a', 'b', 'c', 'd')
values = (1, 2, 3, 4)

# Array comprehension
squares = [i**2 for i in values]
print(squares)

## Array comprehension with condition
even_squares = [i**2 for i in values if i%2 == 0]
print(even_squares)

# Dictionary comprehension
new_dict = {keys[i]:squares[i] for i in range(len(keys))}
print(new_dict)

[1, 4, 9, 16]
{'a': 1, 'b': 4, 'c': 9, 'd': 16}

Packing and Unpacking

Tuples and dictionaries are used in Python to pack an arbitrary number of members, with or without keys. Unpacking a tuple or array means accessing their members. To unpack an arbitrary number of members of a tuple we use the * operator

[3]:
t = (1, 2, 3, 4)
a, b, c, d = t #This unpacks each member of the tuple in an integer variable
print(a)
print(b)
print(c)
print(d)

first, *g, last = t #This unpacks the first member of t in an integer f, an arbitrary number of members in list g and the last member in last
print(first)
print(g)
print(last)

1
2
3
4
1
[2, 3]
4
1
3
5
7
9

Unpacking is normally used to pass an arbitrary number of parameters to a function, for instance, let us look again to the range function

[ ]:
range_args = (1,10,2)
for i in range(*range_args): #Unpack the members and pass them as params to the range function
    print(i)

To unpack a dictionary, the operator is ** instead of *. Let us revisit the print function to print two strings instead of one, and overwrite the separator:

[7]:
print("first string", "second_string", sep=', ', end='\n') # Print two strings separated by ', ' and terminated with a new line:
args = ("first string", "second string")
key_args = {"sep":', ', "end": '\n'}
print(*args, **key_args) # This is the same but unpacking a tuple with the parameters and a dictionary with the keyword params sep and end
first string, second_string
first string, second string

Zip function

zip is a handy function that returns an iterator of tuples, where the i-th tuple contains the i-th element from each of the argument sequences or iterables:

[9]:
x = [1, 2, 3]
y = [4, 5, 6]
zipped = zip(x, y)
for a, b in zipped:
    print(a, b, sep=', ')

1, 4
2, 5
3, 6