• Data structures
    • Sequences
      • Lists
        • Using Lists as Stacks
        • Using Lists as Queues
        • List Comprehensions
        • Nested List Comprehensions
        • Extra
          • List comprehensions vs map()
          • List comprehensions vs for loops
          • List comprehensions vs filter()
          • List comprehensions vs generator expressions
          • List comprehensions vs map()
          • List comprehensions vs filter()
      • Tuples and Ranges
    • Sets
      • Sets
      • Set operations
    • Dictionaries
    • Linked lists
    • Classes * [ ] Class inheritance (SOLID / OOD) * [ ] Class methods * [ ] Class attributes * [ ] Class instances * [ ] Class constructors * [ ] Class destructors * [ ] Class inheritance * [ ] Class polymorphism * [ ] Type hinting
      • Hash tables
      • Trees
        • Binary trees
        • Hash trees
  • Core Patterns & Algorithms (note intuition, time complexity, space complexity)
    • Two pointers
    • Sliding windows
    • Breadth first search (BFS)
    • Depth first search (DFS)
    • Backtracking
    • Binary search
    • Stacks
    • Queues
    • Divide and conquer
    • Trie
    • Union find
    • Greedy
    • Sorts
      • Bubble sort
      • Insertion sort
      • Quick sort
      • Merge sort
      • Heap sort
      • Topological sort
  • Bit manipulation
    • Wtf is this?
  • Recursion
    • Call stack
    • Call stack base case unravel
    • Infinite loop
    • Iterative vs recursive
    • Pile of boxes
    • Recursive and base case
    • Dynamic programming
      • Try solve a few ENGSCI Problems from Tony’s section!!!
      • Ask Kart
# initialise list
fruits = ["orange", "apple", "pear", "banana", "kiwi", "apple", "banana"]
print("'fruits' List:", fruits, "\n")
print("type(fruits):", type(fruits), "\n")
# print('dir(fruits):', *dir(fruits), sep='\n')
 
# object_methods = [method_name for method_name in dir(fruits)
#                   if callable(getattr(fruits, method_name))]
 
# print(type(fruits), '\n\n\n', dir(fruits), '\n\n\n', id(fruits), '\n\n\n', callable(fruits), '\n\n\n', getattr(fruits), '\n\n\n', hasattr(fruits), '\n\n\n', globals(), '\n\n\n', locals(), '\n\n\n')
 
'fruits' List: ['orange', 'apple', 'pear', 'banana', 'kiwi', 'apple', 'banana'] 
 
type(fruits): <class 'list'>

1. Python Lists

# initialise list
fruits = ["orange", "apple", "pear", "banana", "kiwi", "apple", "banana"]
print("'fruits' List:", fruits)
print("type(fruits):", type(fruits), "\n")
 
# count method
print("Count of apples:", fruits.count("apple"), "\n" "Count of tangerines:", fruits.count("tangerine"), "\n")
 
# index method
print(
    "Index of 'banana':",
    fruits.index("banana"),
    "\n" "Index of (next) 'banana' (starting at position 4):",
    fruits.index("banana", 4),
    "\n",
)
 
# reverse method (works in place)
fruits.reverse()
print("'fruits', reverse:", fruits)
 
# append method (insert at end of list) (Returns: None; works in place)
fruits.append("GRAPE")
print("'fruits', appende:", fruits)
 
# insert method (insert at arbitrary index in list) (Returns: None; works in place)
fruits.insert(4, "CHERRY")
print("'fruits', insert: ", fruits)
 
# extend method - append iterable (insert at end of list) (Returns: None; works in place)
# NB: try .append() with the 'new_fruits' list
new_fruits = ["GUAVA", "MELON"]
fruits.extend(new_fruits)
print("'fruits', extend: ", fruits)
 
# remove method (Returns: None; works in place)
fruits.remove("apple")
print("'fruits', remove: ", fruits, "\n")
 
# sort method (Returns: None; works in place); list.sort(*, key=None, reverse=False) - reverse for desc.
fruits.sort()
print("'fruits', sorted (alphanum, asc):", fruits)
fruits.sort(reverse=True)
print("'fruits', sorted (alphanum, desc):", fruits, "\n")
 
# pop method; list.pop([i]) - works in place
print(
    "fruits.pop() with no args --> Returns last item:", fruits.pop(), "\n", "\bresulting 'fruits' List:", fruits, "\n"
)
print("\bfruits.pop(3) --> Returns item @ idx 3:", fruits.pop(3), "\n", "\bresulting 'fruits' List:", fruits, "\n")
 
# copy method
copycat = fruits.copy()
print("'copycat' List:", copycat, "\n")
 
fruits.clear()
print("'fruits' cleared 'List':", fruits)
 
'fruits' List: ['orange', 'apple', 'pear', 'banana', 'kiwi', 'apple', 'banana']
type(fruits): <class 'list'> 
 
Count of apples: 2 
Count of tangerines: 0 
 
Index of 'banana': 3 
Index of (next) 'banana' (starting at position 4): 6 
 
'fruits', reverse: ['banana', 'apple', 'kiwi', 'banana', 'pear', 'apple', 'orange']
'fruits', appende: ['banana', 'apple', 'kiwi', 'banana', 'pear', 'apple', 'orange', 'GRAPE']
'fruits', insert:  ['banana', 'apple', 'kiwi', 'banana', 'CHERRY', 'pear', 'apple', 'orange', 'GRAPE']
'fruits', extend:  ['banana', 'apple', 'kiwi', 'banana', 'CHERRY', 'pear', 'apple', 'orange', 'GRAPE', 'GUAVA', 'MELON']
'fruits', remove:  ['banana', 'kiwi', 'banana', 'CHERRY', 'pear', 'apple', 'orange', 'GRAPE', 'GUAVA', 'MELON'] 
 
'fruits', sorted (alphanum, asc): ['CHERRY', 'GRAPE', 'GUAVA', 'MELON', 'apple', 'banana', 'banana', 'kiwi', 'orange', 'pear']
'fruits', sorted (alphanum, desc): ['pear', 'orange', 'kiwi', 'banana', 'banana', 'apple', 'MELON', 'GUAVA', 'GRAPE', 'CHERRY'] 
 
fruits.pop() with no args --> Returns last item: CHERRY 
resulting 'fruits' List: ['pear', 'orange', 'kiwi', 'banana', 'banana', 'apple', 'MELON', 'GUAVA', 'GRAPE'] 
 
fruits.pop(3) --> Returns item @ idx 3: banana 
resulting 'fruits' List: ['pear', 'orange', 'kiwi', 'banana', 'apple', 'MELON', 'GUAVA', 'GRAPE'] 
 
'copycat' List: ['pear', 'orange', 'kiwi', 'banana', 'apple', 'MELON', 'GUAVA', 'GRAPE'] 
 
'fruits' cleared 'List': []

Some notes from docs:

  • Methods that only modify the list have no return value printed – they return the default None.
    • This is a design principle for all mutable data structures in Python.
    • These include insert, append, extend, reverse, remove, sort
  • Not all data can be sorted or compared.
    • E.g. [None, ‘hello’, 10] doesn’t sort because integers can’t be compared to strings and None can’t be compared to other types.
    • Some types don’t have a defined ordering relation. 3+4j < 5+7j isn’t a valid comparison.

1.1. Using Lists as Stacks (LIFO)

# initialise stack
stack = [3, 4, 5]
print("init stack:     ", stack)
 
# add an item TO THE TOP of the stack
stack.append(6)
stack.append(7)
print("\nstack (appends):", stack)
 
# retrieve item FROM THE TOP of the stack
print("\npop 1 Returns:", stack.pop(), "---> remaining stack:", stack)
print("pop 2 Returns:", stack.pop(), "---> remaining stack:", stack)
print("pop 3 Returns:", stack.pop(), "---> remaining stack:", stack)
 
init stack:      [3, 4, 5]
 
stack (appends): [3, 4, 5, 6, 7]
 
pop 1 Returns: 7 ---> remaining stack: [3, 4, 5, 6]
pop 2 Returns: 6 ---> remaining stack: [3, 4, 5]
pop 3 Returns: 5 ---> remaining stack: [3, 4]

1.2. Using Lists as Queues (FIFO)

from collections import deque
 
# initialise queue
queue = deque(["Eric", "John", "Michael"])
print("init queue:     ", str(queue)[6:-1])
 
# add items TO THE END of the queue
queue.append("Terry")  # Terry arrives
queue.append("Graham")  # Graham arrives
print("\nqueue (appends):", str(queue)[6:-1])
 
# retrieve item FROM THE BEGINNING of the queue
print("\npopleft 1  Returns: ", queue.popleft(), "   ---> remaining queue:", str(queue)[6:-1])
print("popleft 2  Returns: ", queue.popleft(), "   ---> remaining queue:", str(queue)[6:-1])
print("popleft 3  Returns: ", queue.popleft(), "---> remaining queue:", str(queue)[6:-1])
print("pop    (1) Returns: ", queue.pop(), " ---> remaining queue:", str(queue)[6:-1])
 
init queue:      ['Eric', 'John', 'Michael']
 
queue (appends): ['Eric', 'John', 'Michael', 'Terry', 'Graham']
 
popleft 1  Returns:  Eric    ---> remaining queue: ['John', 'Michael', 'Terry', 'Graham']
popleft 2  Returns:  John    ---> remaining queue: ['Michael', 'Terry', 'Graham']
popleft 3  Returns:  Michael ---> remaining queue: ['Terry', 'Graham']
pop    (1) Returns:  Graham  ---> remaining queue: ['Terry']

Some notes from docs

  • Lists are very efficient for LIFO (stacks), but not efficient for FIFO (queues)
    • Only appends and pops from the ends of lists are fast
    • inserts or pops anywhere else, especially the beginning are slow, as all elements have their indices shifted by 1

1.3. List Comprehensions

Concisely create lists:

  • Syntax: Brackets [] containing an expression followed by a for clause, then zero or more for or if clauses.
  • Result: New list resulting from evaluating the expression in the context of the for and if clauses which follow it
  • Note: the order of for and if statements in a listcomp and a set of nested for loops is the same!

Two very common use cases:

  • When a new list’s elements are the result of some operations applied to every member of another sequence or iterable
  • To create a subsequence of elements of another sequence or iterable that satisfy some condition

1.3.1. Example 1: Create a list of squares

# for loops are inefficient and messy. these overwrite `x` which still exists after the loop terminates.
squares = []
for x in range(10):
    squares.append(x**2)
print(squares)
 
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
# with a listcomp
squares = list(map(lambda x: x**2, range(10)))
print(squares)
 
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
# a more concise and readable listcomp
squares = [[x**2 for x in range(10)]]
print(squares)
 
[[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]]

1.3.2. Example 2: Combine elements of two lists if they are not equal

# initialise lists
list_1 = [1, 2, 3]
list_2 = [3, 1, 4]
 
# with inefficient for loops
# NB: (x, y) is parenthesised because it is a tuple
combs = []
for x in [1, 2, 3]:
    for y in [3, 1, 4]:
        if x != y:
            combs.append((x, y))
print(combs)
 
[(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)]
# using a listcomp
combs = [(x, y) for x in list_1 for y in list_2 if x != y]
print(combs)
 
[(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)]

1.3.3. Example 3: Playing with numbers

# initialise a vector
vec = [-4, -2, 0, 2, 4]
print("vec (initial): ", vec)
 
# create new list with values doubled
vec_doubled = [x * 2 for x in vec]
print("vec (doubled): ", vec_doubled)
 
# filter the list to exclude negative numbers
vec_filtered = [x for x in vec if x >= 0]
print("vec (+ve only):", vec_filtered)
 
# apply abs() to all elements
vec_abs = [abs(x) for x in vec]
print("vec (abs. val):", vec_abs)
 
vec (initial):  [-4, -2, 0, 2, 4]
vec (doubled):  [-8, -4, 0, 4, 8]
vec (+ve only): [0, 2, 4]
vec (abs. val): [4, 2, 0, 2, 4]

1.3.4. Example 4: Playing with strings

# init freshfruit
freshfruit = ["  banana", "  loganberry ", "passion fruit  "]
print("freshfruit (initialised):", freshfruit)
 
# call a method (.strip(), removes end whitespace) on each element
freshfruit_stripped = [fruit.strip() for fruit in freshfruit]
print("freshfruit (stripped):   ", freshfruit_stripped)
 
freshfruit (initialised): ['  banana', '  loganberry ', 'passion fruit  ']
freshfruit (stripped):    ['banana', 'loganberry', 'passion fruit']

1.3.5. Example 5: MISC stuff!

NB: Flattening a 2D list of lists (matrix) into 1D is quite useful!

# create a list of 2-tuples like (number, square)
square_tuples = [(x, x**2) for x in range(6)]  # don't forget, tuples must be parenthesised
print("integers and their squares:", square_tuples)
 
# flatten a list using a listcomp (hint: uses 2 `for`s)
vec = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
vec_flat = [num for elem in vec for num in elem]
print("\nvec:       ", vec)
print("vec (flat):", vec_flat)
 
# list increasing digits of pi
from math import pi
 
print("\nmore and more pi:", [str(round(pi, i)) for i in range(1, 6)])
 
integers and their squares: [(0, 0), (1, 1), (2, 4), (3, 9), (4, 16), (5, 25)]
 
vec:        [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
vec (flat): [1, 2, 3, 4, 5, 6, 7, 8, 9]
 
more and more pi: ['3.1', '3.14', '3.142', '3.1416', '3.14159']

1.4. Nested List Comprehensions

Using matrices as the inspiring example

# initialise matrix
matrix = [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]
print(*matrix, sep="\n")
 
[1, 2, 3, 4]
[5, 6, 7, 8]
[9, 10, 11, 12]

1.4.1. Transpose rows and columns

# inefficient nested for loops
transposed = []
for i in range(4):
    # the following 3 lines implement the nested listcomp
    transposed_row = []
    for row in matrix:
        transposed_row.append(row[i])
    transposed.append(transposed_row)
 
print(*transposed, sep="\n")
 
[1, 5, 9]
[2, 6, 10]
[3, 7, 11]
[4, 8, 12]
# slightly less efficient, hybrid of for loop and listcomp
transposed = []
for i in range(4):
    transposed.append([row[i] for row in matrix])
 
print(*transposed, sep="\n")
 
[1, 5, 9]
[2, 6, 10]
[3, 7, 11]
[4, 8, 12]
# as a nested listcomp
transposed = [[row[i] for row in matrix] for i in range(4)]
 
print(*transposed, sep="\n")
 
[1, 5, 9]
[2, 6, 10]
[3, 7, 11]
[4, 8, 12]

1.4.2. IRL you’d use a Python built-in like zip()

NB: https://docs.python.org/3/tutorial/controlflow.html#tut-unpacking-arguments for the * we keep seeing

transposed = list(zip(*matrix))
print(*transposed, sep="\n")
 
(1, 5, 9)
(2, 6, 10)
(3, 7, 11)
(4, 8, 12)

2. The del statement

  • Recall, .remove() removes items from a list given its value (e.g. “banana”)
  • del removes an item based on the its index in the list (also in-place)
a = [-1, 1, 66.25, 333, 333, 1234.5]
print(a)
del a[0]
print(a)
del a[2:4]
print(a)
 
[-1, 1, 66.25, 333, 333, 1234.5]
[1, 66.25, 333, 333, 1234.5]
[1, 66.25, 1234.5]
  • Additionally, use del to delete entire variables!
del a
# print(a) # will throw error
 

3. Tuples and Sequences

  • Lists, strings, ranges are examples of Sequence data types. That’s why they all have such common operations (e.g. slicing, indexing)
  • A tuple (a lot like a .csv) is another example of a Sequence data type.
    • The values (separated by commas) can by of any Type (they can even be other tuples, lists, etc)
    • Initialising tuples:
      • You initialise tuples without brackets [].
      • Strictly speaking () aren’t needed for input but it’s often necessary due to surrounding code
      • Special cases:
        • empty tuple empty = () and
        • 1 item tuple one_item_tuple = 'hello', (trailing comma)
    • Tuples are immutable, but can contain mutable objects

3.1. Sequence (tuple and other) operations include:

OperationResult
x in sTrue if an item of s is equal to x, else False
x not in sFalse if an item of s is equal to x, else True
s + tthe concatenation of s and t
s * n or n * sequivalent to adding s to itself n times
s[i]ith item of s, origin 0
s[i:j]slice of s from i to j
s[i:j:k]slice of s from i to j with step k
len(s)length of s
min(s)smallest item of s
max(s)largest item of s
s.index(x[, i[, j]])index of the first occurrence of x in s (at or after index i and before index j)
s.count(x)total number of occurrences of x in s

3.2. Tuple use cases (from docs):

Though tuples may seem similar to lists, they are often used in different situations and for different purposes:

  • Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via:
    • unpacking (see later in this section)
    • or indexing
    • (or even by attribute in the case of namedtuples).
  • Lists are mutable, and their elements are usually homogeneous (but that’s not neseccary) and are accessed by iterating over the list.
# initialise tuple (aka tuple packing)
t_tup = 12345, 54321, "hello!"
print("t_tup:", t_tup)
print(type(t_tup), "\n")
 
# indexing a tuple
print("0th index:", t_tup[0], "\n")
 
# nesting tuples
u_tup = t_tup, (1, 2, 3, 4, 5), ["1", 2, 3333, "hello"]
print("u_tup:", u_tup, "\n")
 
# tuples are immutable
# u_tup[0] = 88888 # throws error
 
# but tuples can contain mutable objects
u_tup[2][0] = "changed"
print("u_tup:", u_tup, "\n")
 
t_tup: (12345, 54321, 'hello!')
<class 'tuple'> 
 
0th index: 12345 
 
u_tup: ((12345, 54321, 'hello!'), (1, 2, 3, 4, 5), ['1', 2, 3333, 'hello']) 
 
u_tup: ((12345, 54321, 'hello!'), (1, 2, 3, 4, 5), ['changed', 2, 3333, 'hello'])
# init special case tuples
empty_tuple = ()
tuple_with_one_item = ("hello",)  # <--- note trailing comma
print("special case tuple lengths:", len(empty_tuple), len(tuple_with_one_item))
print("special case tuples       :", empty_tuple, tuple_with_one_item, "\n")
 
special case tuple lengths: 0 1
special case tuples       : () ('hello',)
# tuple packing and unpacking
t = 12345, 54321, "hello!"
print("packed tuple (t)        :", t)
 
# tuple unpacking
x, y, z = t
print("unpacked tuple (x, y, z):", x, y, z)
 
packed tuple (t)        : (12345, 54321, 'hello!')
unpacked tuple (x, y, z): 12345 54321 hello!

4. Sets

  • UNORDERED collection with NO DUPLICATE elements
  • Common use cases
    • Membership testing
    • Eleminating duplicate entries

4.1. Set mathematical operations:

  • Union, :
    • All the elements from both sets (i.e. Venn: everything)
  • Intersection, :
    • Elements two sets have in common (i.e. Venn: overlap)
  • Difference, :
    • Elements present on one set, but not on the other (i.e. Venn: remove a whole circle (incl. overlap))
  • Symmetric difference :
    • Elements from both sets, that are not present on the other (i.e. Venn: remove overlap)
  • Initialise with {'elem1','elem2'}
    • Empty sets are initialised with set()
    • Note, empty dictionaries are initialised with {}
# initialise set
basket_set = {"apple", "orange", "apple", "pear", "orange", "banana"}
 
# note, no duplicates!
print(basket_set, "\n")
 
# test membership (fast)
print("orange" in basket_set)
print("crabgrass" in basket_set, "\n")
 
# set operations on unique letters from two words
a = set("abracadabra")
b = set("alacazam")
c = {"jiggerypokery"}
print(a, b, c, "\n")
 
print("unique letters in a            :", a)
print("unique letters in b            :", b)
print("letters in a but not b         : difference           :", a - b)
print("letters in a or b or both      : union                :", a | b)
print("letters in both a and b        : intersection         :", a & b)
print("letters in a or b but not both : symmetric difference :", a ^ b)
 
{'banana', 'pear', 'orange', 'apple'} 
 
True
False 
 
{'c', 'r', 'd', 'b', 'a'} {'m', 'z', 'c', 'l', 'a'} {'jiggerypokery'} 
 
unique letters in a            : {'c', 'r', 'd', 'b', 'a'}
unique letters in b            : {'m', 'z', 'c', 'l', 'a'}
letters in a but not b         : difference           : {'b', 'r', 'd'}
letters in a or b or both      : union                : {'m', 'z', 'c', 'r', 'd', 'b', 'l', 'a'}
letters in both a and b        : intersection         : {'a', 'c'}
letters in a or b but not both : symmetric difference : {'m', 'd', 'b', 'z', 'l', 'r'}

4.2. Set comprehensions (like listcomps)

a = {x for x in "abracadabra" if x not in "abc"}
a
 
{'d', 'r'}

5. Dictionaries

  • A dict is a set of key:value pairs
    • Dictionaries are indexed by (unique) keys
      • keys can be any immutable Type: strings and numbers are fine as keys
      • Tuples are fine as keys (as long as the tuple contains immutables like strings, numbers or other tuples
      • Lists are not valid keys (mutable by append(), extend(), and index/slice assignments)
    • Place comma-separated list of key:value pairs in {} to ppulate the dict
    • {} is an empty dictionary (note set() is an empty set)
  • Main use cases:
    • Storing a value with some key
    • Extracting the value given the key

5.1. Some useful dictionary operations:

  • Delete key:value pairs with del
  • Assignment of an existing key overwrites its value
  • list(d) on a dictionary Returns a list of all the keys used in it (in insertion order)
    • sorted(d) does the same, but sorted instead
  • in checks if a single key is in the dictionary.
tel_dict = {"jack": 4098, "sape": 4139}
print(tel_dict)
 
tel_dict["guido"] = 4127
print(tel_dict)
 
del tel_dict["sape"]
tel_dict["irv"] = 4127
tel_dict["leon"] = 9129
tel_dict["elon"] = 9127
print(tel_dict, "\n")
 
print("--- common dictionary operations:")
print("list(tel_dict): Returns List of keys in insertion order       :", list(tel_dict))
print("sorted(tel_dict): Returns List of keys in alphanum sort order :", sorted(tel_dict))
print("'guido' in tel_dict:", "guido" in tel_dict)
print("'jack' not in tel_dict:", "jack" not in tel_dict, "\n")
 
print("--- dictionary inspect methods:")
print(".items()       : ", tel_dict.items())
print(".keys()        : ", tel_dict.keys())
print(".values()      : ", tel_dict.values())
print(".get('jack')   : ", tel_dict.get("jack"), "\n")
 
print("--- dictionary modification (in-place) methods:\n\n", tel_dict)
print(
    "example 1: .popitem(): \n--> Remove LAST inserted `key:value` pair (LIFO)\n--> Return `key:value` pair",
    tel_dict.popitem(),
    "\n--> Remaining dict:",
    tel_dict,
    "\n",
)
print(
    "example 2: .pop('irv') \n--> Remove SPECIFIED `key:value` pair, \n--> Return `value` only: ",
    tel_dict.pop("guido"),
    "\n--> Remaining dict:",
    tel_dict,
)
 
# print(*dir(tel_dict), sep='\n')
 
{'jack': 4098, 'sape': 4139}
{'jack': 4098, 'sape': 4139, 'guido': 4127}
{'jack': 4098, 'guido': 4127, 'irv': 4127, 'leon': 9129, 'elon': 9127} 
 
--- common dictionary operations:
list(tel_dict): Returns List of keys in insertion order       : ['jack', 'guido', 'irv', 'leon', 'elon']
sorted(tel_dict): Returns List of keys in alphanum sort order : ['elon', 'guido', 'irv', 'jack', 'leon']
'guido' in tel_dict: True
'jack' not in tel_dict: False 
 
--- dictionary inspect methods:
.items()       :  dict_items([('jack', 4098), ('guido', 4127), ('irv', 4127), ('leon', 9129), ('elon', 9127)])
.keys()        :  dict_keys(['jack', 'guido', 'irv', 'leon', 'elon'])
.values()      :  dict_values([4098, 4127, 4127, 9129, 9127])
.get('jack')   :  4098 
 
--- dictionary modification (in-place) methods:
 
 {'jack': 4098, 'guido': 4127, 'irv': 4127, 'leon': 9129, 'elon': 9127}
example 1: .popitem(): 
--> Remove LAST inserted `key:value` pair (LIFO)
--> Return `key:value` pair ('elon', 9127) 
--> Remaining dict: {'jack': 4098, 'guido': 4127, 'irv': 4127, 'leon': 9129} 
 
example 2: .pop('irv') 
--> Remove SPECIFIED `key:value` pair, 
--> Return `value` only:  4127 
--> Remaining dict: {'jack': 4098, 'irv': 4127, 'leon': 9129}

5.2. dict() constructor

Builds dicts from sequences of key:value pairs

tel_list = [("sape", 4139), ("guido", 4127), ("jack", 4098)]
tel_dict = dict(tel_list)
 
print(tel_dict)
 
{'sape': 4139, 'guido': 4127, 'jack': 4098}

5.3. Dictionary Comprehensions (like listcomps)

To create dicts from arbitrary key:value expressions

a_dict = {x: x**2 for x in (2, 4, 6)}
print(a_dict)
 
{2: 4, 4: 16, 6: 36}

5.4. Final bit of dict ease-of-use syntax

If keys are simple strings (good practice), it may be easier to specify key:value pairs as keyword arguments

a_dict = dict(sape=4139, guido=4127, jack=4098)
print(a_dict)
 
{'sape': 4139, 'guido': 4127, 'jack': 4098}

6. Useful Looping Techniques

6.1. Looping over dictionaries

# initialise dictionaries
knights_dict = {"gallahad": "the pure", "robin": "the brave", "lancelot": "the friend"}
 
# retrieve key and corresp. value together using .items()
for k, v in knights_dict.items():
    print(k, v)
 
gallahad the pure
robin the brave
lancelot the friend

6.2. Looping over a sequence (e.g. lists or tuples)

# looping over a list
games_list = ["avalon", "catan", "chess", "checkers"]
for i, v in enumerate(games_list):
    print(i, v)
 
print("FYI, this is the iterable (enumerate) object: ", enumerate(games_list), "\n\n")
 
0 avalon
1 catan
2 chess
3 checkers
FYI, this is the iterable (enumerate) object:  <enumerate object at 0x104103f60>
# looping over a tuple
favourite_concepts_tuple = ("the number 3", 3, ["tic", "tac", "toe"], 333, [[1, 2], [3, 4]])
for i, v in enumerate(favourite_concepts_tuple):
    print(i, v)
 
0 the number 3
1 3
2 ['tic', 'tac', 'toe']
3 333
4 [[1, 2], [3, 4]]

6.3. Looping over two or more sequences at the same time

# looping over two or more lists at a time
questions_list = ["name", "quest", "favorite color"]  # note what happens if we make them unequal length
answers_list = ["lancelot", "the holy grail", "blue"]
 
for q, a in zip(questions_list, answers_list):
    print("What is your {0}? \n It is {1}.\n".format(q, a))
 
print("FYI, this is the iterable (zip) object: ", zip(questions_list, answers_list), "\n\n")
 
What is your name? 
 It is lancelot.
 
What is your quest? 
 It is the holy grail.
 
What is your favorite color? 
 It is blue.
 
FYI, this is the iterable (zip) object:  <zip object at 0x104164480>
# looping over two or more tuples at a time
favourite_concepts_tuple = ("the number 3", 3, ["tic", "tac", "toe"], 333, [[1, 2], [3, 4]])
least_favourite_concepts_tuple = ("the number 4", "slang", 4, 44)  # unequal length ('least' is shorter by 1)
for f, l in zip(favourite_concepts_tuple, least_favourite_concepts_tuple):
    print("I like {0}.\n However, I do not like {1}.\n".format(f, l))
 
I like the number 3.
 However, I do not like the number 4.
 
I like 3.
 However, I do not like slang.
 
I like ['tic', 'tac', 'toe'].
 However, I do not like 4.
 
I like 333.
 However, I do not like 44.

6.4. Looping over a sequence (or dictionary) in reverse

# loop over a reversed range
for i in reversed(range(1, 10, 2)):
    print(i)
 
9
7
5
3
1
# loop over a reversed list
words_list = ["hi", "quotidian", "world", "last word"]
for w in reversed(words_list):
    print(w)
 
last word
world
quotidian
hi
# loop over a reversed tuple
concepts_tuple = ("hi", 12, "ok", [1, [3]])
for c in reversed(concepts_tuple):
    print(c)
 
[1, [3]]
ok
12
hi
# loop over a reversed dict
people_dict = {"person2": "noel", "person1": "elisa", "person3": "kart"}
for p in reversed(people_dict):
    print(p)
 
person3
person1
person2

6.5. Looping over a sequence (or dictionary) in sorted order

basket = ["apple", "orange", "apple", "pear", "orange", "banana"]
 
# NB: like the reverse() example, the following code works for tuples and dictionaries as well
print("\nascending alphanumeric sort:")
for f in sorted(basket):
    print(f)
 
print("\ndescending alphanumeric sort:")
for f in sorted(basket, reverse=True):
    print(f)
 
ascending alphanumeric sort:
apple
apple
banana
orange
orange
pear
 
descending alphanumeric sort:
pear
orange
orange
banana
apple
apple

6.6. Looping over a set

  • Recall, using set() on a sequence eliminates duplicate elements.
  • Using sorted() in combination with set() over a sequence is an idiomatic way to loop over nique elements of the sequence in sorted order
basket = ["apple", "orange", "apple", "pear", "orange", "banana"]
for f in sorted(set(basket)):
    print(f)
 
apple
banana
orange
pear

6.7. Warning: List objects have global scope!

  • Modifying a list (i.e. calling a .method() on a list) inside a for loop will modify the list variable permanently.
  • Always create a new list instead
import math
 
raw_data = [56.2, float("NaN"), 51.7, 55.3, 52.5, float("NaN"), 47.8]
filtered_data = []  # initialise a list where we'll filter out the NaNs
 
for value in raw_data:
    if not math.isnan(value):
        filtered_data.append(value)
 
print(filtered_data)
 
[56.2, 51.7, 55.3, 52.5, 47.8]

7. Additional reading

Go to this python docs link for more information on the following topics:

  • More on Conditions: descriptions and priorities various conditional operators like:
    • Comparison operators
      • in, not in : membership test
      • is, is not : equality test (all objects)
      • chaining of operators a < b == c : this is actually tests 2 comparisons in one! a < b and b == c
    • Boolean operators (in descending order of priority)
      • not, and, or : so A and not B or C means (A and (not B) or C)
      • and and or are short-circuit operators: Eval arguments from L-to-R. Stop as soon as outcome is determined.
  • Comparing sequences and other types. boring lol