for x in (0, 1, 2, 3, 4):
print(x)
0
1
2
3
4
Introduction to Software Engineering (CSSE 1001)
A tuple is an iterable object which means we can for-loop though them (like strings).
for x in (0, 1, 2, 3, 4):
print(x)
0
1
2
3
4
Comparing Tuples
1, 2, 3) == (1, 2, 3) (
True
1, 2, 3) == (1, 2) (
False
1, 2, 3) < (1, 2, 4) (
True
1, 2, 3) < (1, 2) (
False
1, 2) < (1, 2, 3)
(True
True
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
Note that lists are mutable.
0] = 2*xs[1]
xs[0] xs[
'appleapple'
= ['a', 2, 'c', 4, 'e']
xs for x in xs:
print(x)
a
2
c
4
e
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]
= [1, 2, 3]
xs = xs # ys is an alias of xs
ys 4) # Appending to a list xs.append(
ys
[1, 2, 3, 4]
+= [5] # Appending to a list
xs ys
[1, 2, 3, 4, 5]
= xs + [6] # Concatenating two lists
xs ys
[1, 2, 3, 4, 5]
Here the aliasing relationship broken!
Type-hinting lists is a new feature in Python 3.9 and greater.
def count(xs: list[int], ys: list[int]) -> int:
""" Return the number of times members of xs are in ys.
>>> count([1, 2], [1, 2, 3, 4])
2
>>> count([1, 2], [1, 2, 2, 2, 3, 1, 4])
5
"""
def count(xs: list[int], ys: list[int]) -> int:
""" Count the number of times members of xs are in ys.
"""
= 0
ans for y in ys:
if y in xs:
+= 1
ans return ans
# one-line (FP) solution
return sum(y in xs for y in ys)
def make_unique(xs: list[int]) -> list[int]:
""" Returns the UNIQUE elements of xs.
>>> make_unique([1, 2, 3, 4])
[1, 2, 3, 4]
>>> make_unique([1, 2, 2, 2, 3, 1, 4, -3])
[1, 2, 3, 4, -3]
"""
def make_unique(xs: list[int]) -> list[int]:
""" Return the list of UNIQUE elements of xs.
"""
= []
ans for x in xs:
if x not in ans:
ans.append(x)return ans
= [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
xs
3:6]
xs[3, 4, 5]
[
2]
xs[::0, 2, 4, 6, 8]
[
7:2:-2]
xs[7, 5, 3] [
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
.
Python’s range
keyword allows us to quickly build an iterator for use by for-loops. It has the general form:
range([start], stop[, step])
which is similar to list slicing. Note that square braces denote optional arguments.
range(10)
range(0, 10)
range(0, 10)
range(0, 10)
type(range(10))
range
list(range(10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
list(range(2, 7))
[2, 3, 4, 5, 6]
list(range(2, 7, 3))
[2, 5]
list(range(2, 7, -1))
[]
list(range(7, 2, -1))
[7, 6, 5, 4, 3]
range
can be used to walk through lists using an index.
= [11, 22, 33, 44]
xs for k in range(len(xs)):
#Code involving xs[k]
Python’s enumerate
simplifies the pattern on the previous slide. Basically:
enumerate([x0, ..., xn]) = [(0, x0), ..., (n, xn)]
However, this is not the full story — enumerate
actually returns an iterable.
= [1, 2, 3, 4]
xs for k, xk in enumerate(xs):
#Code involving k and xk == xs[k]
Note (0, x0)
are immutable lists called tuples.
for k, x in enumerate(["Scientia", "ac", "Labore"]):
print(k,x)
UQ’s motto
Scientia ac Labore
translates to by means of knowledge and hard work.
= ["0", "1", "2"], ["x", "y"]
(digits, alphas) for d in digits:
for a in alphas:
print(d + a)
0x
0y
1x
1y
2x
2y
A list can have another list as an element
= [ [1,2], [5,6,7] ] xs
len(xs)
2
len(xs[-1])
3
Recall that loops can be nested.
= [ [1, 2], ["a", "b", "c"], "hello" ]
xss for xs in xss:
for x in xs:
print(x, end=' ') # Replaces newline with space.
1 2 a b c h e l l o
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. For this to work we need a deep-copy, which is not built-in — see copy.deepcopy
.
It is possible to “unbracket” a list when passing to functions.
def f(x, y, z):
return x + y + z
= [1, 2, 3] x
f(x)
TypeError: f() missing 2 required positional arguments: 'y' and 'z'
*x) f(
6
Exercise 1 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.
Exercise 2 (Caesar Cipher) A Caesar cipher is a type of encryption where each letter in the plaintext is ‘shifted’ a certain number of places down the alphabet to obtain the ciphertext.
For instance, with a shift of -2 we do \[ \begin{align*} A &\to Y \\ B &\to Z \\ C &\to A \\ &\; \vdots \\ Z &\to X \end{align*} \]
Write code for encrypting and decrypting messages using the Caesar cipher.
In particular, write functions with the headers
def encrypt_caeser(plaintext: str, shift: int) -> str:
def decrypt_caeser(ciphertext: str, shift: int) -> str:
Tuples are immutable ordered collections whereas lists are mutable ordered collections.
Both can be sliced and indexed.
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, xs: list[str], ys: list[str]) -> int:
"""Two list encoding."""
= xs.index(key) # position of key in xs
k return ys[k]
This is the hash function for the single list encoding.
def hash_2(key: str, xs: list[int]) -> int:
"""One-to-one encoding."""
= ord(key)-ord('a') # position of key in alphabet
pos_in_alpha return xs[pos_in_alpha]
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.
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
= {
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
= {
h "red": ["apple", "firetrucks", "cars"],
"yellow": ["banana", "cars"],
"blue": ["sky", "cars"]
}
h.values()
dict_values([['apple', 'firetrucks', 'cars'], ['banana', 'cars'], ['sky', 'cars']])
keys
= {
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 & copy
= {
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
= {
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
= {
h "red": ["apple", "firetrucks", "cars"],
"yellow": ["banana", "cars"],
"blue": ["sky", "cars"]
}
"red") h.get(
['apple', 'firetrucks', 'cars']
"green"] h[
KeyError: 'green'
Safer because no error is thrown here
"green") h.get(
A dictionary is an iterable. When used in a for loop the loop will walk through the keys of the dictionary.
= {'a': 1, 'b': 2, 'c': 3} table
for key in table:
= table[key]
value print(key, value)
a 1
b 2
c 3
One may also avoid key errors using an if-statement.
if key in table:
+= 1
table[key] else:
= 0 table[key]
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'
Exercise 3 Write a function that when given a string, returns a dictionary whose keys are characters and values are the positions of these characters in the string.
def char_index(xs: str) -> dict[str, list[int]]:
"""
>>> char_index("")
{}
>>> char_index("aaAAbbBB")
{'a': [0, 1], 'b': [4, 5], 'A': [2, 3], 'B': [6, 7]}
"""
Exercise 4 Complete the following function.
def combine( d1: dict[int, list[int]],
dict[int, list[int]] ) -> dict[int, int]:
d2: """Return the dictionary where each key is a key that is in both d1
and d2.
The value associated with each key in the new dictionary is the sum
of all the integers associated with that key in d1 and d2.
>>> combine({1: [2], 4: [5, 6]}, {4: [8]})
{4: 19}
"""
Exercise 5 Write a function with the following contract that does a reverse lookup.
def reverse_lookup(d: dict, item) -> list:
"""Returns all keys such that d[keys] == item
Precondition:
item is immutable.
>>> reverse_lookup({1:'bluey', 19:'chilli', -31:'bluey'}, 'bluey')
[1, -31]
>>> reverse_lookup({1:'bluey', 19:'chilli', -31:'bluey'}, 'muffin')
[]
"""
Exercise 6 Write a function which inverts the keys and items of a dictionary.
Note: Since there may be many identical values, we will have to map those to lists of keys.
def invert(d: dict) -> dict:
"""Return the inverted version of d.
>>> invert({1: 10, 2: 10})
{10: [1, 2]}
"""
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.