Lab 02 - Lists, Dictionaries, & File I/O
Due by 11:59pm on 2024-01-18.
Starter Files
Download lab02.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:
- Open the file in a mode.
- Do stuff with the file.
- 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 toreadline()
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:
.write(<line_content>)
- writes the exact contents given in a string..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 lab02.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 lab02.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 lab02.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 theprint()
function already appends a newline to the end of the content you provide. To mitigate this, you can use useprint(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 lab02.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. ReturnsNone
.
>>> nums = [0, 1, 2]
>>> nums.append(3)
>>> nums
[0, 1, 2, 3]
.extend(<list>)
- Extends a list given another list. ReturnsNone
.
>>> 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 returnsNone
. 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. ReturnsNone
.
>>> 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()