Lab 03 - Lists, Dictionaries, & File I/O

Due by 11:59pm on 2024-01-18.

Starter Files

Download lab03.zip. Inside the archive, you will find starter files for the questions in this lab.

Topics

Lists

A list is a data structure that can store multiple elements. Each element can be any type, even a list itself. We write a list as a comma-separated list of expressions in square brackets:

>>> list_of_ints = [1, 2, 3, 4]
>>> list_of_bools = [True, True, False, False]
>>> nested_lists = [1, [2, 3], [4, [5]]]

Each element in the list has an index, with the index of the first element starting at 0. We say that lists are therefore “zero-indexed.”

With list indexing, we can specify the index of the element we want to retrive. A negative index represents starting from the end of the list, where the negative index -i is equivalent to the positive index len(lst)-i.

>>> lst = [6, 5, 4, 3, 2, 1, 0]
>>> lst[0]
6
>>> lst[3]
3
>>> lst[-1] # Same as lst[6]
0
>>> lst[-2]
1

To check if an element is within a list, use Python's in keyword. For example,

>>> numbers = [2, 3, 4, 5]
>>> 1 in numbers
False
>>> n = 2
>>> n in numbers
True

To find the number of elements in a list, Python comes with a prebuilt function len(<list>).

>>> numbers = [0, 1, 2, 3, 4]
>>> len(numbers)
5

With the len() function, we can iterate through each elements using indicies.

>>> numbers = [10, 9, 8, 7, 6]
>>> for i in range(len(numbers)):
... print(numbers[i])
...
10
9
8
7
6

When accessing a element through a list using an index, we are given a reference to the element and are allowed to manipulate it within the list.

>>> numbers = [10, 9, 8]
>>> numbers[0] = 5
>>> numbers
[5, 9, 8]

List Slicing

To create a copy of part of or all of a list, we can use list slicing. The syntax to slice a list lst is:

lst[<start index>:<end index>:<step size>]

This expression evaluates to a new list containing the elements of lst:

  • Starting at and including the element at <start index>.
  • Up to but not including the element at <end index>.
  • With <step size> as the difference between indices of elements to include.

If the start, end, or step size are not explicitly specified, Python has default values for them. A negative step size indicates that we are stepping backwards through a list when including elements.

>>> lst = [6, 5, 4, 3, 2, 1, 0]
>>> lst[0:3]
[6, 5, 4]
>>> lst[:3] # Start index defaults to 0
[6, 5, 4]
>>> lst[3:7]
[3, 2, 1, 0]
>>> lst[3:] # End index defaults to len(lst)
[3, 2, 1, 0]
>>> lst[::2] # Skip every other; step size defaults to 1 otherwise
[6, 4, 2, 0]
>>> lst[::-1] # Make a reversed copy of the entire list
[0, 1, 2, 3, 4, 5, 6]

Try doing Q1: WWPD: Lists

You can also index, use in, use len(), and slice with strings.

>>> s = "abcde"
>>> s[0] # retrieves the character at index 0
'a'
>>> 'b' in s
True
>>> len(s)
5
>>> s[2:]
'cde'

List Comprehensions

List comprehensions are a compact and powerful way of creating new lists out of sequences.

Let's say we wanted to create a list containing the numbers between 0 and 4, this is one way to do it:

>>> numbers = []
>>> for i in range(5):
... numbers += [i] # or `numbers.append(i)`
>>> numbers
[0, 1, 2, 3, 4]

Using list comprehensions, we can create the same list but in more compact way.

The simplest syntax for a list comprehension is the following:

[<expression_to_add> for <element> in <sequence>]

The syntax is designed to read like English: “Add and compute the expression for each element in the sequence."

Trying to create the same numbers list above, we can do it using a list comprehension as so:

>>> numbers = [i for i in range(5)]
>>> numbers
[0, 1, 2, 3, 4]

Compare the similarities and difference of creating the numbers list. What syntax is missing in the list comprehension version? What is the same?

In some cases, we might want to add a condition on to which elements we adding:

>>> lst = []
>>> for i in [1, 2, 3, 4]:
... if i % 2 == 0: # if `i` is an even number
... lst = lst + [i**2]
>>> lst
[4, 16]

You can add a condition to list comprehensions. The general syntax for a list comprehension is the following:

[<expression_to_add> for <element> in <sequence> if <conditional>]

where the if <conditional> section is optional.

The syntax is designed to read like English: “Add and compute the expression for each element in the sequence (if the conditional is true for that element).” For example, using the same example above:

>>> [i**2 for i in [1, 2, 3, 4] if i % 2 == 0]
[4, 16]

This list comprehension will:

  • Add and compute the expression i**2
  • For each element i in the sequence [1, 2, 3, 4]
  • Where i % 2 == 0 (i is an even number),

and then put the resulting values of the expressions into a new list.

In other words, this list comprehension will create a new list that contains the square of every even element of the original list [1, 2, 3, 4].

Both are completely valid, but the list comprehension is more compact and the more "Pythonic" way to do that operation. List comprehensions often seem odd when you first see them, but as you use them more, they become easier to understand and create.

Try doing Q2: Even Weighted and Q3: Couple

It will be very helpful look at Python's builtin list methods for adding and removing elements to a list if you are unfamiliar with Python lists. (It is described at the very bottom of this lab in the 'Additional List Methods' section.)

Dictionaries

A dictionary is a data structure that holds pairs of items that go together. One is called the key and it directs you to the value. This is known as mapping.

{} are used to create a dictionary then each pair is created like this <key> : <value>. Each pair is seperated by a comma.

my_dictionary = {"a": 1, "b": 2, "c": 3, "d": 4}

The example above shows single character strings as keys to a number. Each key must be unique, but values can repeat.

Adding Elements

To add add a key-value pair to a dictionary you'll use the following template <name_of_dictionary>[<key>] = <value>. Using the example from above, I can add the pair "e" : 5 like this:

my_dictionary["e"] = 5

Consider this: If each key must unqiue, what if you try adding a pair with a key that already exists such as my_dictionary["a"] = 8? Try it in the Python interpreter.

Accessing Elements

To access a value you must provide the key. You use a similar syntax to lists, but instead of an index, you provide a key. There is no way to go the opposite direction and provide a value for a key.

To get the value in our dictionary associated with the key "b" we can do the following:

my_value = my_dictionary["b"]

Check For a Key

If we try to access a key that doesn't exist, Python will throw an error. We can use the in keyword with an if-statement to check if a key is in our dictionary before trying to access it.

key = "a"
if key in my_dictionary:
value = my_dictionary[key]
else:
value = 0

This, however, does not work when checking if a certain value exists within a dictionary.

for Loops and Dictionaries

When you use a for loop on a dictionary, you will go through each key.

for someKey in myDictionary:
<SOME_CODE>

Try doing Q4: Count Appearances

File I/O

Whenever we shut down our electronic device, we lose information unless it is saved in a file. In this class, we will focus on writing to and reading from files.

Python already has a built-in way to create, write to, and read files.

To work with a file, we need to open it by using the open function and provide as arguments the filename and the mode we want to use. There are several different mode for opening a file such as write mode, read mode, or append mode.

Opening and Closing a File

There are three steps when working with files:

  1. Open the file in a mode.
  2. Do stuff with the file.
  3. Close the file.

To open and close a file, you could use this syntax:

file = open(<file_name>, <mode>)

# code that does stuff with the file

file.close()

Please don't do it that way in your code. It's very common to forget to close a file, the problems it causes are not obvious, and it will break the autograder. Instead, make the computer do that work for you.

Do this instead:

with open(<file_name>, <mode>) as file:

# code that does stuff with the file

# the file is closed out here

All the code that is indented after the with open, will have access to the file for writing.

Once Python is finished executing the indented code, then the file automatically closes.

file_name and mode have to be strings. Mode specifically has to be either

  • "w" - write mode
  • "r" - read mode
  • "a" - append mode
  • or other modes supported by python

Important: Make sure to close your file. Not doing so will waste space and break the autograder.

Reading From a File

If you do not provide a mode when opening a file, python will automatically open it in read mode.

There are multiple ways to read from a file. The following are a few methods to do so:

  • .read() - returns a string containing all the contents of the file including newlines \n and tabs \t.
  • .readline() - returns a string with the contents of the first line including newlines \n and tabs \t. Once it returns the contents of the first line, another call to readline() will return the contents of the second line, and so on and so forth until the final line.
  • .readlines() - returns a list of strings where each string is a line. Newlines and tabs are explicitly shown in each string.

Remember, since these are methods, these follow the format of <file>.read(). The differences between methods and functions will become more apparent in the classes lecture.

Create a file called cs_is_cool.txt and type something into it that spans a few lines. Make sure you are in the same directory as the file and demo the following code in Python file!

# if a mode is not provided, open will automatically default to read mode
with open("cs_is_cool.txt") as input_file:

file_lines = input_file.readlines()

print(file_lines)

What shows up? Try using .readline() and .read() instead.

Writing to a File

If the file does not exist and you are opening the file in write mode, then python will automatically create the file. If the file already has content, opening it in write mode will erase everything and override it with what you choose to write to it.

There are two ways to write to a file in python:

  1. .write(<line_content>) - writes the exact contents given in a string.
  2. .writelines(<list_of_line_content) - writes each string in the list into the file.

.write() may not work as you might think. If we were to use the following code:

with open("cs_is_cool.txt", 'w') as file:
file.write("Hello World")
file.write(":D")

Right now, our cs_is_cool.txt looks like this:

Hello World:D

By default, the write() method does not put a new line at the end of the string; we have to do that ourselves by putting "\n" (backslash-n) at the end of the string.

file.write("Hello World\n")
file.write(":D")

Now, with the addition of the "\n", our cs_is_cool.txt looks like this:

Hello World
:D

Additionally, we can use the writelines() method which takes in a list of strings and writes each of them into the file:

lines = ["Hello World\n", ":D"]
file.writelines(lines)

Try doing Q5: Copy File

Required Questions

Q1: WWPD: Lists

What would Python display for each of these? (If you are completely stumped on what the code is doing, type the code in to the interpreter and see what happens and if you were correct. If not, spend some time figuring out what was wrong.)

>>> a = [1, 5, 4, [2, 3], 3]
>>> print(a[0], a[-1])
______
>>> len(a)
______
>>> 2 in a
______
>>> 2 in a[3]
______
>>> a[3][0]
______
>>> a[3][0] += 1
>>> a
______
>>> a[3][2]
______

Q2: Even Weighted

Implement the function even_weighted that takes a list s and returns a new list that keeps only the even-indexed elements of s and multiplies them by their corresponding index.

def even_weighted(s):
"""
>>> x = [1, 2, 3, 4, 5, 6]
>>> even_weighted(x)
[0, 6, 20]
"""

return [_________________________________________________]

Hint: Recall that a even number is a number that is evenly divisble by 2 and therefore has 0 remainder. We can use the modulo operator % to find the remainder and ensure it is 0: number % 2 == 0

Test your code:

python3 -m doctest lab03.py

You should see zero errors for even_weighted.

Q3: Couple

Implement the function couple(), which takes in two lists and returns a list that contains lists with i-th elements of two sequences coupled together. You can assume the lengths of two sequences are the same. Try using a list comprehension.

def couple(s, t):
"""Return a list of two-element lists in which the i-th element is [s[i], t[i]].

>>> a = [1, 2, 3]
>>> b = [4, 5, 6]
>>> couple(a, b)
[[1, 4], [2, 5], [3, 6]]
>>> c = ['c', 6]
>>> d = ['s', '1']
>>> couple(c, d)
[['c', 's'], [6, '1']]
"""

assert len(s) == len(t)
"*** YOUR CODE HERE ***"

Test your code:

python3 -m doctest lab03.py

Q4: Count Appearances

Implement a function called count_appearances that has a parameter lst that contains a list of integers. The function should return a dictionary where its keys are each individual integer and the associated value is the number of times the integer has been seen.

def count_appearances(lst):
"""Returns a dictionary containing each integer's appearance count
>>> lst = [0]
>>> count_appearances(lst)
{0 : 1}
>>> lst = [0, 0, 1, 2, 1, 1]
>>> count_appearances(lst)
{0 : 2, 1 : 3, 2 : 1}
>>> lst = [0, 0, 0, 0, 0, 3, 0, 0]
>>> count_appearances(lst)
{0 : 7, 3 : 1}
"""

"*** YOUR CODE HERE ***"

Test your code:

python3 -m doctest lab03.py

Q5: Copy File

Implement the copy_file function. It takes two strings, input_filename and output_filename. It opens the two files, reads the file specified by the input_filename line by line, and for each line it prints and writes to the file specified by the output_filename the line with the line number and colon prepended to it. This function does not return anything.

def copy_file(input_filename, output_filename):
"""Print each line from input with the line number and a colon prepended,
then write that line to the output file.
>>> copy_file('text.txt', 'output.txt')
1: They say you should never eat dirt.
2: It's not nearly as good as an onion.
3: It's not as good as the CS pun on my shirt.
"""

"*** YOUR CODE HERE ***"

Hint: When reading from a file, python will interpret the new lines as a literal "\n". As a result, when printing a line, you will have an extra empty new line because the print() function already appends a newline to the end of the content you provide. To mitigate this, you can use use print(line, end=""). By default, end="\n" and changing this removes the extra empty newline. Another solution is to use <string>.strip() on each of the lines from the file you are reading from. .strip() will removing any leading or trailing whitespace from a string, like newlines.

Submit

Submit your lab03.py file to Gradescope to receive credit. Submissions will be in Canvas.


Extra Practice

Q6: Slice and Multiplice

Write a function slice-and-multiplice that takes in a list of integers called lst and returns a new list where all values past the first value are multiplied by the first value. In addition, the new list should not contain the first value.

Note: Make sure that you do not change the inputted list!

def slice_and_multiplice(lst):
"""Return a new list where all values past the first are
multiplied by the first value.

>>> slice_and_multiplice([1,1,6])
[1, 6]
>>> slice_and_multiplice([9,1,5,2])
[9, 45, 18]
>>> slice_and_multiplice([4])
[]
>>> slice_and_multiplice([0,4,9,18,20])
[0, 0, 0, 0]
"""

"*** YOUR CODE HERE ***"

Q7: Factors List

Write factors_list, which takes a number n and returns a list of its factors in ascending order.

def factors_list(n):
"""Return a list containing all the numbers that divide `n` evenly, except
for the number itself. Make sure the list is in ascending order.

>>> factors_list(6)
[1, 2, 3]
>>> factors_list(8)
[1, 2, 4]
>>> factors_list(28)
[1, 2, 4, 7, 14]
"""

all_factors = []
"*** YOUR CODE HERE ***"

Additional List Methods

Some of these may come in very handy when coding, but they may not be necessarily required in this class.

Adding to a List

  • .append(<elem>) - Adds an element to the back of a list. Returns None.
>>> nums = [0, 1, 2]
>>> nums.append(3)
>>> nums
[0, 1, 2, 3]
  • .extend(<list>) - Extends a list given another list. Returns None.
>>> nums = [0, 1, 2]
>>> new_nums = [3, 4, 5]
>>> nums.extend(new_nums)
>>> nums
[0, 1, 2, 3, 4, 5]
  • .insert(<index>, <elem>) - Inserts an element at a given index and returns None. If there is already an element at the index, it will be pushed back to make space for the new element.
>>> forbidden_toppings = ["pineapple", "tomato"]
>>> forbidden_toppings.insert(1, "mushroom forbidden_toppings")
>>> forbidden_toppings
["pineapple", "mushroom", "tomato"]

Removing from a List

  • .pop(<index>) - Removes the element at the provided index. If an index is not provided, the index will default to the last index in the list (len(<list>) - 1) and remove the last element. .pop() returns the removed element.
>>> nums = [0, 1, 2, 3]
>>> three = nums.pop()
>>> nums
[0, 1, 2]
>>> three
3
>>> nums.pop(1)
1
>>> nums
[0, 2]
  • .remove(<elem>) - Removes the first occurence of the element provided. Returns None.
>>> food = ['potato', 'tomato', 'tomato', 'pineapple']
>>> food.remove('tomato')
>>> food
['potato', 'tomato', 'pineapple']

It is important to notice that none of these methods return a modified list but instead modify the list provided directly.

You can also refer to w3schools for more detailed information and more methods like sort()


© 2023 Brigham Young University, All Rights Reserved