Python Tip #8 – reducing looping by using dicts

In situations where you have a list of objects and have to retrieve then in random order, dictionaries can act as lookup tables.

users = list of class User 

# Get users one by one by looking up ids
user_1 = next((u for u in users if u.id == user_1_id), None)
user_2 = next((u for u in users if u.id == user_2_id), None)
...

# Simpler solution using lookup table
lookup = dict((u.id, user) for u in users)
user_1 = lookup[user_1_id]
user_2 = lookup[user_2_id]
...

This tip is not very obvious, hence this explanation:

user_1 = next((u for u in users if u.id == user_1_id), None)

This method employs a iterator looping through the list of users every time we have to find a user, which means we have to run this loop a hundred times. This poses a complexity of O(N2).

lookup = dict((u.id, user) for u in users)
user_1 = lookup[user_1_id]

This method on the other hand iterates through the users list one time and create a lookup table that we can again and again without having to iterate through the list every time. This reduces the complexity to O(N) which could theoretically lead up to 10 times faster execution of the program.

Python Tip #7 – getattr()

Sometimes we have to deal with external objects and their attributes. getattr() can save you at those times.

# Get the attribute name
name = obj.name  # AttributeError if name is not present

# Check if the attribute is present before fetching
try:
    name = obj.name
except AttributeError:
    name = "Guest"

# Simpler solution
name = obj.name if hasattr(obj, "name") else "Guest"

# Simplest Solution
name = getattr(obj, "name", "Guest")

Python Tip #6 – Merging Dictionaries

Merge or combine dictionaries

d1 = { "a": 1 }
d2 = { "b": 2 }

# Adding elements of one dictionary to another
d1.update(d2)  # d1 => { "a": 1, "b": 2 }

# Create a new dict with values from other dictionaries
d3 = { **d1, **d2 }  # d3 => { "a": 1, "b": 2 }
d4 = { **d3, "c": 3 }  # d4 => { "a": 1, "b": 2, "c": 3 }

** is the unpacking operator

Python Tip #5 – Get value from dict if key is present

Check for existence of a key in dictionary and retrieve its value if present.

dictionary = { "key": "value" }

# checking for the presence and key and getting the value
wanted = None
if "key" in dictionary:
    wanted = dictionary["key"]

# Simpler version
wanted = dictionary.get("key", None)

Python Tip #4 – Find an element in a list satisfying condition

What if you want to find the first item that matches the condition instead of getting a list of items?

selected = None
for i in items:
if condition:
selected = i
break

# Simpler version using next()
selected = next((i for i in items if condition), None)

next() is a built in function which is not that well known.

Python Tip #1 – Setting flags without using if statements

When you have to check for the presence of a value in a list and set a flag based on it, we can avoid typical:

set default => check => update 

routine in Python and condense it to a single line like this.

orders = ['pizza', 'coke', 'fries']
order_book = {}

# Setting an yes or no flag in another dictionary or object
order_book['pizza'] = False
if 'pizza' in orders:
    order_book['pizza'] = True

# Simpler Version
order_book['pizza'] = 'pizza' in orders

Python – Extended Assignment Operator and Lists

Extended Assignment Operators – we use them all the time as shorthands for assigning values.
Lets see how this works when using multiple identifiers (variables) in terms of simple data like a number.

>>> a = 5
>>> b = a
>>> b += 3
>>> print(a)
5
>>> print(b)  # only the value of b is changed even though we have assigned a to b
8
>>> b = b+4  # the long form assignment also doesn't affect the original a
>>> print(b)
12
>>> print(a)  
5

Pitfall

Let us apply a similar set of operations on list

>>> a = [1,2,3]
>>> b = a
>>> b += [4,5]
>>> print(a)  # surprise! - changing the value of b also affect the value of a
[1, 2, 3, 4, 5]
>>> print(b)
[1, 2, 3, 4, 5]
>>> b = b+[6,7]
>>> print(b)
[1, 2, 3, 4, 5, 6, 7]
>>> print(a)
[1, 2, 3, 4, 5]  # surprise again !! - but assigning a new value to b doesn't

Takeaway

Now the question is, why does the value of a change when adding elements to b?
And furthermore why doesn’t it change when we do it using the long form?

The answer is: Extended assignment operators act differently on mutable and immutable values.
So when we say b += [4,5] we are basically saying add 4 and 5 to the list b, but when we say
b = b + [6,7], we are saying “take the list b, add 6, 7 to it and create a new list from it, then assign it to the identifier b”.

This subtle difference will come to bite us when we least expect it. So be aware and take precautions 🙂

Peer Pairing Algorithm

Problem Statement

A class has N number of students. When the students submit an assignment, they are assigned K peers to review their assignments and provide feedback such that 0 < K = N

Example:

Input: N=3, K=1

Output:

{
    0: [1],
    1: [2],
    2: [0]
}

Input: N=3, K=2

{
    0: [1,2],
    1: [2,0],
    2: [0,1]
}

Solution

def generate_peer_pairs(N, K):
    """A function that provides unique peers for reviewing.
    The generated matches follow the following rules:
        1. The no.of reviews/rounds of review is less than total no.of users.
        2. The user won't be reviewing his/her own work
        3. All the reviewers assigned would be unique.
        4. Each user will have equal no.of reviews to give and receive.

    :param N: Total no.of student for whom the matching is to be done.
    :param K: The no.of reviews each student is supposed to receive.
    :returns: a dictionary of graders and their peers { grader_id: [peer_1, peer_2, ...]}
    """
    if K >= N:
        return False

    available_offsets = list(range(1, N))
    offsets = []

    while len(offsets) < rounds:
        offset = random.choice(available_offsets)
        offsets.append(offset)
        available_offsets.remove(offset)

    students = list(range(N))
    random.shuffle(students)

    allocations = {}
    for idx, student in enumerate(students):
        allocations[student] = [students[(idx + offset) % N] for offset in offsets])

    return allocations

Notes

The problem is an interesting one. I started out with the idea that the peers should be arranged in a random way and wrote an algorithm by selecting a random recipient while looping over each grader. It was completely non-roboust and failed to make the right pairs more times that it worked.

The solution if you had noticed, is completely a position shifting algorithm. What appears a non-repeating random order for the grader and the recipient is not really that random.