Sorting students and what exams they are doing

Question:

I have a list of tuples and the tuples look like this (2, 11) which means exam 2 must be taken by student 11. The exams are numbered from 0 to however many exams there are and the same with students. I need to produce a 2D list where the first list is the exams the 0th student is taking and the second list is the exams student number 1 is taking etc. I have this code:

examsEachStudentsIsDoing = []
exams = []
number_of_students = 14
exams_to_students =  [(0, 1), (0, 4), (0, 5), (0, 3), (0, 10), (0, 13), (0, 9), (0, 11), (0, 12), (0, 2), (0, 7), (0, 6), (1, 7), (2, 7), (2, 5), (2, 0), (2, 11), (2, 13), (3, 4), (4, 6), (4, 8)]
    
for i in range(0,number_of_students):
    exams.clear()
    for j in range(0,len(exams_to_students)):
        if (exams_to_students[j][1]==i):
            exams.append(exams_to_students[j][0])
    examsEachStudentsIsDoing.append(exams)

print(examsEachStudentsIsDoing)

if i add a print line just before examsEachStudentsIsDoing.append(exams) then i get the result:

[2]
[0]
[0]
[0]
[0, 3]
[0, 2]
[0, 4]
[0, 1, 2]
[4]
[0]
[0]
[0, 2]
[0]
[0, 2]
[[0, 2], [0, 2], [0, 2], [0, 2], [0, 2], [0, 2], [0, 2], [0, 2], [0, 2], [0, 2], [0, 2], [0, 2], [0, 2], [0, 2]]

why is it repeatedly appending on the last students exams and not each one individually

Asked By: SomethingNormal123

||

Answers:

The exams is a list. In python, lists are passed by reference, so when you are appending exams to examsEachStudentsIsDoing you are just appending a reference to the exams in the examsEachStudentsIsDoing.

At the end of the loop, for last student, the exams is set to [0,2], hence for all the entries in examsEachStudentsIsDoing, you see that value.

So instead of appending the exams, you can append a copy of the current student’s exams to the examsEachStudentsIsDoing. To get a copy of the list you have different options – list.copy() , copy.copy() method, or just slicing list[:].

Try the following code –

examsEachStudentsIsDoing = []
exams = []
number_of_students = 14
exams_to_students =  [(0, 1), (0, 4), (0, 5), (0, 3), (0, 10), (0, 13), (0, 9), (0, 11), (0, 12), (0, 2), (0, 7), (0, 6), (1, 7), (2, 7), (2, 5), (2, 0), (2, 11), (2, 13), (3, 4), (4, 6), (4, 8)]
    
for i in range(0,number_of_students):
    exams.clear()
    for j in range(0,len(exams_to_students)):
        if (exams_to_students[j][1]==i):
            exams.append(exams_to_students[j][0])
    examsEachStudentsIsDoing.append(exams.copy())   #updated

print(examsEachStudentsIsDoing)

Output:

[[2], [0], [0], [0], [0, 3], [0, 2], [0, 4], [0, 1, 2], [4], [0], [0], [0, 2], [0], [0, 2]]

To avoid such issues, you can create new exams list for each student, so a better way to rewrite your code might be –

examsEachStudentsIsDoing = []
number_of_students = 14
exams_to_students =  [(0, 1), (0, 4), (0, 5), (0, 3), (0, 10), (0, 13), (0, 9), (0, 11), (0, 12), (0, 2), (0, 7), (0, 6), (1, 7), (2, 7), (2, 5), (2, 0), (2, 11), (2, 13), (3, 4), (4, 6), (4, 8)]
    
for i in range(0,number_of_students):
    exams = []
    for j in range(0,len(exams_to_students)):
        if (exams_to_students[j][1]==i):
            exams.append(exams_to_students[j][0])
    examsEachStudentsIsDoing.append(exams)

print(examsEachStudentsIsDoing)
Answered By: Jay

Optionally, you can use a dictionary , I used json just to easy print. Also, there is no treatment on the data, meaning that one student can take the same exam twice.

import json

number_of_students = 14
exams_to_students =  [(0, 1), (0, 4), (0, 5), (0, 3), (0, 10), (0, 13), (0, 9), (0, 11), (0, 12), (0, 2), (0, 7), (0, 6), (1, 7), (2, 7), (2, 5), (2, 0), (2, 11), (2, 13), (3, 4), (4, 6), (4, 8)]
"""
(2, 11) which means exam 2 must be taken by student 11
"""
disposal = {}
# Create a key for every student
for i in range(0, number_of_students):
    disposal[i] = { 'exams': []}


# loop through tuples
# add exam to the designated student
for value in exams_to_students:
    disposal[value[1]]['exams'].append(value[0])

json_object = json.dumps(disposal, indent=4)
print(json_object)

#Output
{
    "0": {        
        "exams": [
            2     
        ]
    },
    "1": {        
        "exams": [
            0     
        ]
    },
    "2": {        
        "exams": [
            0     
        ]
    },
    "3": {        
        "exams": [
            0
        ]
    },
    "4": {
        "exams": [
            0,
            3
        ]
    },
    "5": {
        "exams": [
            0,
            2
        ]
    },
    "6": {
        "exams": [
            0,
            4
        ]
    },
    "7": {
        "exams": [
            0,
            1,
            2
        ]
    },
    ...
}

To avoid duplicated values:

# loop through your tuples
for value in exams_to_students:
    if value[0] not in disposal[value[1]]['exams']:
        disposal[value[1]]['exams'].append(value[0])
Answered By: Niko

I agree with @Daniel Hao, a defaultdict offers the simplest solution for this. Try to avoid tricky solutions if you can.

from collections import defaultdict

exams_to_students =  [(0, 1), (0, 4), (0, 5), (0, 3),
                      (0, 10), (0, 13), (0, 9), (0, 11),
                      (0, 12), (0, 2), (0, 7), (0, 6),
                      (1, 7), (2, 7), (2, 5), (2, 0),
                      (2, 11), (2, 13), (3, 4), (4, 6), (4, 8)]

tracker = defaultdict(list)
for (exam, student) in exams_to_students:
    tracker[student].append(exam)

print("Exams by student using a defaultdict")
for student in sorted(tracker.keys()):
    print(student, tracker[student])

print("Exams by student using a list")
exams_by_student_as_list = [tracker[student] for student in sorted(tracker.keys())]
for exams in exams_by_student_as_list:
    print(exams)
Answered By: Frank

Description of what is happening here

Lists in python (also other objects) are mutable. It is a detailed article, but briefly, when you create a list and saves it in a variable like l then the l will point to a location of memory that List had created there. When you assign another variable for this list with a=l (or in your case you append it to another list) it will use the same pointer so then a and l will point to same location in your memory.

when you commit the list (append, remove, clear, …) this functions will change the list in your memory, can say they change the reference and when you need the data, all variables that point to that location will return just the same value.

Solutions

There are to many solution for this problem, one of them is to replace exam.clear() with exam = []:

examsEachStudentsIsDoing = []
# no need to write exams = [] here
number_of_students = 14
exams_to_students =  [(0, 1), (0, 4), (0, 5), (0, 3), (0, 10), (0, 13), (0, 9), (0, 11), (0, 12), (0, 2), (0, 7), (0, 6), (1, 7), (2, 7), (2, 5), (2, 0), (2, 11), (2, 13), (3, 4), (4, 6), (4, 8)]
    
for i in range(0,number_of_students):
    print(f'{i=}')
    exams = [] # replaced exams.clear()
    for j in range(0,len(exams_to_students)):
        if (exams_to_students[j][1]==i):
            print(f'index ({j})={exams_to_students[j]}')
            exams.append(exams_to_students[j][0])
    print(f'{exams=}')
    examsEachStudentsIsDoing.append(exams)

print(examsEachStudentsIsDoing)

But I can also improve your code and re-write it like this:

exams_to_students =  [(0, 1), (0, 4), (0, 5), (0, 3), (0, 10), (0, 13), (0, 9), (0, 11), (0, 12), (0, 2), (0, 7), (0, 6), (1, 7), (2, 7), (2, 5), (2, 0), (2, 11), (2, 13), (3, 4), (4, 6), (4, 8)]
examsEachStudentsIsDoing = [list() for _ in exams_to_students]
for exam, student in exams_to_students:
    examsEachStudentsIsDoing[student].append(exam)

This code is much more smaller and more reliable. For more clarity, I create a new and separate list for each member of the main list in the second line so that the said problem does not occur.

And at the end I recommend you to use shorter variable names!

Answered By: BSimjoo
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.