= [1, "apple"]
xs type(xs)
list
Introduction to Software Engineering (CSSE 1001)
A list is a mutable ordered collection of elements. These elements are not necessarily the same type and are not necessarily distinct.
Square brackets []
are used to create lists in Python.
= [1, "apple"]
xs type(xs)
list
And are indexable like strings.
0] xs[
1
Again (and importantly) that lists are mutable. We can modify a list once it is created, something we could not do with strings and tuples.
0] xs[
1
0] = 2
xs[0] xs[
2
0] = 2*xs[1]
xs[0] xs[
'appleapple'
1,2,3] < [4,5,6] [
True
Point-wise comparisons from position zero.
7,2,3] < [4,5,6] [
False
< [1] []
True
1 in [1, 2, 3]
True
0 in [1, 2, 3]
False
123 in [1, 2, 3]
False
1] in [1, 2, 3] [
False
This is the original list updated.
= [0, 1, 2]
xs = xs + [3]
xs xs
[0, 1, 2, 3]
Alternatively, using a method that list provides…
= [0, 1, 2]
xs 3)
xs.append( xs
[0, 1, 2, 3]
= [1, 2, 3]
xs 4,5])
xs.append([ xs
[1, 2, 3, [4, 5]]
= [1, 2, 3]
xs 4,5]) xs.extend([
Is code similar to.
= xs + [4, 5] xs
Take care when (trying to) copy lists.
= [1, 2, 3]
xs = xs
ys -1] = 9 ys[
ys
[1, 2, 9]
xs
[1, 2, 9]
= [1, 2, 3]
xs = xs.copy()
ys -1] = 9 ys[
xs
[1, 2, 3]
ys
[1, 2, 9]
A more nuanced case is when passing lists (but really list references) to functions.
def foo(ys):
0] = -100 ys[
= [1, 2, 3]
xs
foo(xs) xs
[-100, 2, 3]
Notice here that a reference to the list is passed to foo
and consequently changes made inside function have effect outside the function.
Often this is something we wish to exploit. We can have functions that sort a list “in place” (without duplicating the list) or have utility functions that zero out the contents of a list for example.
Notice in ths visualization that a reference (arrow) is passed to foo
.
There are some minor nuances with list appending. We enumerate them here so they can be identified in debugging.
The following have typical behavior on aliases. Appending to xs
changes the list ys
is referencing.
= [1, 2, 3]
xs = xs
ys 4)
xs.append(# ys has also changed ys
[1, 2, 3, 4]
= [1, 2, 3]
xs = xs
ys += [4]
xs # ys has also changed ys
[1, 2, 3, 4]
Concatenating like the following, however, breaks the aliasing relationship because xs
creates a new list in memory.
= [1, 2, 3]
xs = xs
ys = xs + [3]
xs # ys has NOT changed! ys
[1, 2, 3]
Here the aliasing relationship broken! This operation is concatenating two lists (which produces a new list) and not appending xs
.
= [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
xs 3:6]
xs[3, 4, 5] [
[3, 4, 5]
2] xs[::
[0, 2, 4, 6, 8]
7:2:-2] xs[
[7, 5, 3]
Exercise 1 We say the list
[0, 1, 2, 3, 4, 5]
rotated by 2 is
[2, 3, 4, 5, 0, 1].
Write a function
def rotate(xs: list, k: int) -> list
that rotates a list by k
.
A list can have another list as an element.
= [ [1,2], [5,6,7] ] xs
len(xs)
2
len(xs[-1])
3
It is more accurate to say we a list of references to lists, and not the lists themselves. As illustrated in the following.
Notice that a matrix can be represented by a list-of-lists in Python.
\[ \mathbb{A} = \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix} \equiv [[1, 2, 3], [4, 5, 6], [7, 8, 9]] \]
= [[1, 2, 3], [4, 5, 6], [7, 8, 9]] ass
-1] ass[
[7, 8, 9]
1:3] ass[
[[4, 5, 6], [7, 8, 9]]
1] ass[
[4, 5, 6]
1][-1] ass[
6
1,-1] ass[
TypeError: list indices must be integers or slices, not tuple
= [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
ass = ass.copy()
bss 0][0] = 0 bss[
bss
[[0, 2, 3], [4, 5, 6], [7, 8, 9]]
ass
[[0, 2, 3], [4, 5, 6], [7, 8, 9]]
This is because Python did a shallow-copy (it copied the references, not the lists themselves). For this to work we need a deep-copy, which is not built-in — see copy.deepcopy
.
It is possible to “unbracket” a list which is useful for passing all the arguments in on variable
def f(x, y, z):
return x + y + z
= [1, 2, 3] t
f(t)
TypeError: f() missing 2 required positional arguments: 'y' and 'z'
*t) f(
6
Exercise 2 Write a function
def transpose(ass: List[List[int]]) -> List[List[int]]:
that returns the matrix transpose of \(\mathbb{A}\). Recall \[ \begin{bmatrix} 1 & 2 & 3\\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix}^T = \begin{bmatrix} 1 & 4 & 7 \\ 2 & 5 & 8 \\ 3 & 6 & 9 \end{bmatrix} \] That is the columns and rows are swapped.
Here is a deliberately vague task: Write a function that returns the character frequency of a string comprised only of lower case characters.
For example, the character frequency of “hello world” would encode that there is one “h”, three “l”s and so on.
What are some viable representations?
Two Lists
'h', 'e', 'l', 'o', ' ', 'w', 'r', 'd'],
[ [1, 1, 3, 2, 1, 1, 1, 1] ] [
One-to-one Encoding
a b c d e f g h i j k l m n o p q r s t u v w x y z0,0,0,1,1,0,0,1,0,0,0,3,0,0,2,0,0,1,0,0,0,0,1,0,0,0] [
In both cases we provided a way to map a key (character) to a value (number). \[ \begin{align*} \text{str} &\to \text{int} \\ \text{h} &\mapsto 1 \\ \text{e} &\mapsto 1 \\ \text{k} &\mapsto 3 \\ &\;\; \vdots \end{align*} \] This mapping is called a hash function.
This is the hash-function for the two list encoding.
def hash_1(key: str) -> int:
"""Two list encoding."""
= ['h', 'e', 'l', 'o', ' ', 'w', 'r', 'd']
xs = [1, 1, 3, 2, 1, 1, 1, 1]
ys = xs.index(key) # position of key in xs
k return ys[k]
>>> hash_1('o')
2
>>> hash_1('r')
1
This is the hash function for the single list encoding.
def hash_2(key: str) -> int:
"""One-to-one encoding."""
= [0,0,0,1,1,0,0,1,0,0,0,3,0,0,2,0,0,1,0,0,0,0,1,0,0,0]
xs = ord(key)-ord('a') # position of key in alphabet
pos_in_alpha return xs[pos_in_alpha]
>>> hash_2('o')
2
>>> hash_2('r')
1
Generally speaking we can index a list by any type of key given some hash-function which takes keys to values.
Python has a “magic” hash function that will index anything appropriately. The result is implemented as the dict
data-type.
A dictionary in Python is a data-structure that stores tuple(key, value)
pairings. Python has a “magic” hash function to find a key among the tuples fast. They are like lists that can be indexed by more than just index positions.
The “hello world” example encoded using a dictionary is
>>> char_to_freq = {
'd': 1,
'o': 1,
' ': 1,
'r': 1,
'w': 1,
'e': 1,
'l': 3,
'h': 0
}
Dictionaries have no ordering because the keys can be of mixed type but are mutable.
Hash tables are a candidate for the most important/useful data-structure in computer science.
items
The items of a dictionary are its key/value pairings written as key: value
.
= {
h "red": ["apple", "firetrucks", "cars"],
"yellow": ["banana", "cars"],
"blue": ["sky", "cars"]
}
h.items()
dict_items([('red', ['apple', 'firetrucks', 'cars']), ('yellow', ['banana', 'cars']), ('blue', ['sky', 'cars'])])
values
The values of a dictionary are what keys point to. The value
part of key: value
.
= {
h "red": ["apple", "firetrucks", "cars"],
"yellow": ["banana", "cars"],
"blue": ["sky", "cars"]
}
h.values()
dict_values([['apple', 'firetrucks', 'cars'], ['banana', 'cars'], ['sky', 'cars']])
keys
The keys of the dictionary point to values. The key
part of key: value
.
The keys of a dictionary must be an immutable type.
= dict() d
1,2,3] ] = 1 d[ [
TypeError: unhashable type: 'list'
Tuples are immutable
1,2,3) ] = 1 d[ (
Dictionaries are mutable.
= dict()
h = 1 d[ h ]
TypeError: unhashable type: 'dict'
= {
h "red": ["apple", "firetrucks", "cars"],
"yellow": ["banana", "cars"],
"blue": ["sky", "cars"]
}
h.keys()
dict_keys(['red', 'yellow', 'blue'])
"blue"] h[
['sky', 'cars']
"green"] h[
KeyError: 'green'
The following is fine because we are assigning not retrieving.
"green"] = ["leaves"] h[
clear
We can empty a dictionary of its items.
= {
h "red": ["apple", "firetrucks", "cars"],
"yellow": ["banana", "cars"],
"blue": ["sky", "cars"]
}
= h.copy()
f h.clear()
"blue"] h[
KeyError: 'blue'
"blue"] f[
['sky', 'cars']
copy
Recalling that copying is required because a dictionary is a mutable type. There is a method for copying a dictionary.
= {
h "red": ["apple", "firetrucks", "cars"],
"yellow": ["banana", "cars"],
"blue": ["sky", "cars"]
}
= h.copy() f
"blue"].append("windex")
h["blue"] h[
['sky', 'cars', 'windex']
Shallow copy.
"blue"] f[
['sky', 'cars', 'windex']
get
The method get
is preferred by some because it returns None
rather than KeyError
.
= {
h "red": ["apple", "firetrucks", "cars"],
"yellow": ["banana", "cars"],
"blue": ["sky", "cars"]
}
"red") h.get(
['apple', 'firetrucks', 'cars']
"green"] h[
KeyError: 'green'
"green") # output is invisible because it is None h.get(
type(h.get("green"))
NoneType
Lists are a mutable type that collects another type into a indexed sequence string from index 0.
Dictionaries are for storing key-value relationships. A key must be an immutable type and gets mapped to a value using a hash-function that is magically fast.