Student gets wiser


#1

Stretching wings, as it were, but not getting any smarter.

Code
class Student:
    def __init__(self, name, homework, quizzes, tests):
        self.name = name,
        self.homework = homework,
        self.quizzes = quizzes,
        self.tests = tests
    
class Teacher:
    def __init__(self):
        self.roll = []
    def add(self, name, homework, quizzes, tests):
        student = Student(name, homework, quizzes, tests)
        self.roll.append(student)
    def show(self):
        print self.roll
        
room = Teacher()
room.add("Lloyd",[90.0, 97.0, 75.0, 92.0],[88.0, 40.0, 94.0],[75.0, 90.0])

print room.roll[0].name
print room.roll[0].homework
print room.roll[0].quizzes
print room.roll[0].tests

room.show()

I’m missing something very genuine, here, and would really appreciate an eyeopener. Thanks.

('Lloyd',)
([90.0, 97.0, 75.0, 92.0],)
([88.0, 40.0, 94.0],)
[75.0, 90.0]
[<__main__.Student instance at 0x220ee4>]

Why the tuples? And why isn’t room.roll iterable?


#2

Due to the commas at the ends of the expressions in each of these three lines, each of the instance variables is assigned a tuple ...

        self.name = name,
        self.homework = homework,
        self.quizzes = quizzes,

Based on your concept, I have been experimenting with this ...

This was written and executed in Python 3.6.

def average(numbers):
    return float(sum(numbers)) / len(numbers)

class Student(object):
    def __init__(self, name, homework, quizzes, tests):
        self.name = name
        self.homework = homework
        self.quizzes = quizzes
        self.tests = tests

    def grades_average(self):
        homework = average(self.homework)
        quizzes = average(self.quizzes)
        tests = average(self.tests)
        return 0.1 * homework + 0.3 * quizzes + 0.6 * tests

    def __str__(self):
        res = []
        res.append(self.name)
        res.append("homework: " + " ".join(["{:0.2f}" for h in self.homework]).format(*[h for h in self.homework]))
        res.append("quizzes:  " + " ".join(["{:0.2f}" for q in self.quizzes]).format(*[q for q in self.quizzes]))
        res.append("tests:    " + " ".join(["{:0.2f}" for t in self.tests]).format(*[t for t in self.tests]))
        res.append("average:  {:0.2f}".format(self.grades_average()))
        return "\n".join(res)

class Classroom(object):
    def __init__(self, students):
        self.students = students
    def class_average(self):
        return average([s.grades_average() for s in self.students])
    def __str__(self):
        return "\n\n".join([str(st) for st in self.students]) + \
        "\n\nclass average: {:0.2f}".format(self.class_average())

lloyd = Student("Lloyd", [90.0, 97.0, 75.0, 92.0], [88.0, 40.0, 94.0], [75.0, 90.0])
alice = Student("Alice", [100.0, 92.0, 98.0, 100.0], [82.0, 83.0, 91.0], [89.0, 97.0])
tyler = Student("Tyler", [0.0, 87.0, 75.0, 22.0], [0.0, 75.0, 78.0], [100.0, 100.0])

room = Classroom([lloyd, alice, tyler])
print(room)

Output:

Lloyd
homework: 90.00 97.00 75.00 92.00
quizzes:  88.00 40.00 94.00
tests:    75.00 90.00
average:  80.55

Alice
homework: 100.00 92.00 98.00 100.00
quizzes:  82.00 83.00 91.00
tests:    89.00 97.00
average:  91.15

Tyler
homework: 0.00 87.00 75.00 22.00
quizzes:  0.00 75.00 78.00
tests:    100.00 100.00
average:  79.90

class average: 83.87

#3

room.roll is iterable? but it only contains one element?

class Student:
    def __init__(self, name, homework, quizzes, tests):
        self.name = name
        self.homework = homework
        self.quizzes = quizzes
        self.tests = tests
    
class Teacher:
    def __init__(self):
        self.roll = []
    def add(self, name, homework, quizzes, tests):
        student = Student(name, homework, quizzes, tests)
        self.roll.append(student)
    def show(self):
        for student in self.roll:
          print student.name
        
room = Teacher()
room.add("Lloyd",[90.0, 97.0, 75.0, 92.0],[88.0, 40.0, 94.0],[75.0, 90.0])
room.add("Alice", [100.0, 92.0, 98.0, 100.0], [82.0, 83.0, 91.0], [89.0, 97.0])



room.show()

#4

Blows me away how they got in there, and escaped my notice. Thanks for pointing it out. Removing them made a big difference, though that should be expected.

for i in range(len(room.roll)):
    print room.roll[i].name
    print room.roll[i].homework
    print room.roll[i].quizzes
    print room.roll[i].tests

Lloyd
[90.0, 97.0, 75.0, 92.0]
[88.0, 40.0, 94.0]
[75.0, 90.0]
Alice
[100.0, 92.0, 98.0, 100.0]
[82.0, 83.0, 91.0]
[89.0, 97.0]
Tyler
[0.0, 87.0, 75.0, 22.0]
[0.0, 75.0, 78.0]
[100.0, 100.0]

Thank you @appylpye, for taking the time to drum up an example. I'll study it and see what I can glean for ideas.

Right, and adding more data creates more elements. My issue now is how to iterate over the keys of each dictionary.

for i in range(len(room.roll)):
    for key in room.roll[i]:
        print key, room.roll[i][key]

Traceback (most recent call last):
  File "<stdin>", line 50, in <module>
TypeError: 'Student' object is not iterable

#5

Student is not a dict, and it has no keys.

If you want Student to be a type of dict, you could do something like this ...

class Student(dict):
    def __init__(self, name, homework, quizzes, tests):
        self["name"] = name
        self["homework"] = homework
        self["quizzes"] = quizzes
        self["tests"] = tests
    def show(self):
        for key in self:
            print(key, self[key])
    
lloyd = Student("Lloyd",[90.0, 97.0, 75.0, 92.0],[88.0, 40.0, 94.0],[75.0, 90.0])
lloyd.show()

Output ...

name Lloyd
homework [90.0, 97.0, 75.0, 92.0]
quizzes [88.0, 40.0, 94.0]
tests [75.0, 90.0]

However, if you iterate through a Student, as above, the order of the output might not always be the same.


#6

The more I go forward, the further behind I get. Thank you for clearing that up. (blushes). Back to the drawing board.


#7

I think it is actually a matter of your using a variety of programming languages, including JavaScript, where there are a variety of syntaxes for accessing properties of an object ...

lloyd.name
lloyd["name"]

It is common for programmers to inadvertently carry a feature from one language into a program written in another language.


#8

So true. I find myself making that mistake out on the boards. Need to double check, often.

Starting to come together. My aim is to contain all the students of a classroom in a teacher instance, rather than have named objects in global scope. That much is working so now to hammer out some methods and follow the example above for printing. ( __str__ ).

class Student(dict):
    def __init__(self, name, homework, quizzes, tests):
        self['name'] = name
        self['homework'] = homework
        self['quizzes'] = quizzes
        self['tests'] = tests
class Teacher(object):
    keys = ['name', 'homework', 'quizzes', 'tests']
    criteria = {'homework': 0.1, 'quizzes': 0.3, 'tests': 0.6}
    def __init__(self):
        self.roll = []
    def add(self, name, homework, quizzes, tests):
        student = Student(name, homework, quizzes, tests)
        self.roll.append(student)
    def show(self):
        for i in range(len(room.roll)):
            for item in self.keys:
                print item, room.roll[i][item]
        
room = Teacher()

room.add("Lloyd",[90.0, 97.0, 75.0, 92.0],[88.0, 40.0, 94.0],[75.0, 90.0])
room.add("Alice",[100.0, 92.0, 98.0, 100.0],[82.0, 83.0, 91.0],[89.0, 97.0])
room.add("Tyler",[0.0, 87.0, 75.0, 22.0],[0.0, 75.0, 78.0],[100.0, 100.0])

name Lloyd
homework [90.0, 97.0, 75.0, 92.0]
quizzes [88.0, 40.0, 94.0]
tests [75.0, 90.0]
name Alice
homework [100.0, 92.0, 98.0, 100.0]
quizzes [82.0, 83.0, 91.0]
tests [89.0, 97.0]
name Tyler
homework [0.0, 87.0, 75.0, 22.0]
quizzes [0.0, 75.0, 78.0]
tests [100.0, 100.0]

#9

Getting there...

class Student(dict):
    criteria = {'homework': 0.1, 'quizzes': 0.3, 'tests': 0.6}
    def __init__(self, name, homework, quizzes, tests):
        self['name'] = name
        self['homework'] = homework
        self['quizzes'] = quizzes
        self['tests'] = tests
    def average(self):
        weighted_average = 0
        for key in self.criteria:
            weighted_average += sum(self[key]) / len(self[key]) * self.criteria[key]
        return weighted_average

class Teacher(object):
    keys = ['name', 'homework', 'quizzes', 'tests']
    def __init__(self):
        self.roll = []
    def add(self, name, homework, quizzes, tests):
        self.roll.append(Student(name, homework, quizzes, tests))
    def show(self):
        for i in range(len(room.roll)):
            print "\n",
            for item in self.keys:
                print "{}: {}".format(item, self.roll[i][item])
    def get_averages(self):
        print "\nWeighted Averages"
        for i in range(len(self.roll)):
            x = self.roll[i]
            print "  {}: {:0.2f}".format(x['name'], x.average())
    def class_average(self):
        total = 0
        for i in range(len(self.roll)):
            total += self.roll[i].average()
        return "Class Average: {:0.2f}".format(total / len(room.roll))

Call the methods...

room.print_averages()
print room.class_average()

Assumes presence of data.

Weighted Averages
  Lloyd: 80.55
  Alice: 91.15
  Tyler: 79.90
Class Average: 83.87

#10

To the student dict we’ve added a student id, sid by key name,

    def add(self, name, homework, quizzes, tests):
        sid = len(self.roll)
        self.roll.append(Student(sid, name, homework, quizzes, tests))

and in Student,

    def __init__(self, sid, name, homework, quizzes, tests):
        self['sid'] = sid

add to Teacher,

    def roll_call(self):
        print "\nRoll Call"
        for i in range(len(self.roll)):
            x = self.roll[i]
            print "{:>10d}: {:s}".format(x['sid'], x['name'])

Now with the information in this list we can garner an individual’s grades.

    def grades(self):
        print "\nGrades for,  {:s}".format(self['name'])
        for key in self.criteria:
            x = self[key]
            print "{:>10s}: ".format(key),
            y = ["{:0.1f}".format(item) for item in x]
            print ", ".join(y)
>>> room.roll_call()
Roll Call
  0: Lloyd
  1: Alice
  2: Tyler
>>>> room.roll[0].grades()
Grades for,  Lloyd
   quizzes:  88.0, 40.0, 94.0
     tests:  75.0, 90.0
  homework:  90.0, 97.0, 75.0, 92.0

Teacher.show() gets an upgrade.

    def show(self):        
        for i in range(len(self.roll)):
            self.roll[i].grades()

Grades for,  Lloyd
   quizzes:  88.0, 40.0, 94.0
     tests:  75.0, 90.0
  homework:  90.0, 97.0, 75.0, 92.0

Grades for,  Alice
   quizzes:  82.0, 83.0, 91.0
     tests:  89.0, 97.0
  homework:  100.0, 92.0, 98.0, 100.0

Grades for,  Tyler
   quizzes:  0.0, 75.0, 78.0
     tests:  100.0, 100.0
  homework:  0.0, 87.0, 75.0, 22.0
Code
class Student(dict):
    criteria = {'homework': 0.1, 'quizzes': 0.3, 'tests': 0.6}
    def __init__(self, sid, name, homework, quizzes, tests):
        self['sid'] = sid
        self['name'] = name
        self['homework'] = homework
        self['quizzes'] = quizzes
        self['tests'] = tests
    def average(self):
        weighted_average = 0
        for key in self.criteria:
            weighted_average += sum(self[key]) / len(self[key]) * self.criteria[key]
        return weighted_average
    def grades(self):
        print "\nGrades for,  {:s}".format(self['name'])
        for key in self.criteria:
            x = self[key]
            print "{:>10s}: ".format(key),
            y = ["{:0.1f}".format(item) for item in x]
            print ", ".join(y)
        
class Teacher(object):
    keys = ['sid', 'name', 'homework', 'quizzes', 'tests']
    def __init__(self):
        self.roll = []
    def add(self, name, homework, quizzes, tests):
        sid = len(self.roll)
        self.roll.append(Student(sid, name, homework, quizzes, tests))
    def show(self):        
        for i in range(len(self.roll)):
            self.roll[i].grades()
    def get_averages(self):
        print "\nWeighted Averages"
        for i in range(len(self.roll)):
            x = self.roll[i]
            print "{:>13s}: {:0.2f}".format(x['name'], x.average())
    def class_average(self):
        total = 0
        for i in range(len(self.roll)):
            total += self.roll[i].average()
        return "Class Average: {:0.2f}".format(total / len(room.roll))        
    def roll_call(self):
        print "\nRoll Call"
        for i in range(len(self.roll)):
            x = self.roll[i]
            print "{:>6d}: {:s}".format(x['sid'], x['name'])

room = Teacher()
room.add("Lloyd",[90.0, 97.0, 75.0, 92.0],[88.0, 40.0, 94.0],[75.0, 90.0])
room.add("Alice",[100.0, 92.0, 98.0, 100.0],[82.0, 83.0, 91.0],[89.0, 97.0])
room.add("Tyler",[0.0, 87.0, 75.0, 22.0],[0.0, 75.0, 78.0],[100.0, 100.0])
room.roll_call()
room.show()
room.get_averages()
print room.class_average()

#11

really nice, good job :slight_smile:


#12

The Python 3 linter on repl.it issues the following caution on this line:

  def __init__(self, sid, name, homework, quizzes, tests):

Too many arguments (6/5)
__init__ method of base class 'dict' is not called

I've done some digging around but don't have a clear idea how to address this issue. Can anybody offer an explanation, please?

https://repl.it/G0AD/6


#13

Add this as the first line within the __init__ method of Student ...

        super(Student, self).__init__()

It calls the __init__ method of the dict type, which initializes the new instance, self, as an empty dict. Thereafter, you can initialize the four items based on the parameters, as you do now.


#14

Still telling me there are too many arguments. What am I missing, here?


#15

Following is another approach.

Remove dict as the base class ...

class Student():

Revise the __init__ method as follows to initialize an internal dict ...

  def __init__(self, sid, name, homework, quizzes, tests):
    self.data = dict()
    self['sid'] = sid
    self['name'] = name
    self['homework'] = homework
    self['quizzes'] = quizzes
    self['tests'] = tests

Add a __setitem__ and a __getitem__ method, so that Student objects can be accessed via keys, as with a dict ...

  def __setitem__(self, key, item):
    self.data[key] = item
  def __getitem__(self, key):
    return self.data[key]

Leave everything else as is.


#16

Thanks, again, @appylpye. Both of your approaches work fine, but I'm still scratching my head over the too many arguments linter caution. Is there some kind of rule in Python that limits the number of arguments in __init__?

After reading some more I see it is a caution that may indicate refactoring is needed to lighten the load, and not really a limit issue. Here's my first kick at that can...

  def __init__(self, args):
    self.data = dict()
    self['sid'] = args[0]
    self['name'] = args[1]
    self['homework'] = args[2]
    self['quizzes'] = args[3]
    self['tests'] = args[4]

Student((sid, name, homework, quizzes, tests))  # named tuple

I haven't worked much with *args and **kwargs. How would they be applied in this case?


#17

Using *args

Change the __init__ method of Student to this ...

  def __init__(self, *args):
    self.data = dict()
    self['sid'] = args[0]
    self['name'] = args[1]
    self['homework'] = args[2]
    self['quizzes'] = args[3]
    self['tests'] = args[4]

Using **kwargs

Change the __init__ method of Student to this ...

  def __init__(self, **kwargs):
    self.data = dict()
    self['sid'] = kwargs['sid']
    self['name'] = kwargs['name']
    self['homework'] = kwargs['homework']
    self['quizzes'] = kwargs['quizzes']
    self['tests'] = kwargs['tests']

... and change the add method of Teacher to this ...

  def add(self, name, homework, quizzes, tests):
    sid = len(self.roll)
    self.roll.append(Student(sid=sid, name=name, homework=homework, quizzes=quizzes, tests=tests))

With the **kwargs solution, you could vary the positions of the arguments when you instantiate a Student, if you want.


#18

Following is another version of the __init__ method for Student, using **kwargs, that would allow you to choose, on the fly, the number and names of the arguments that you supply when you instantiate a Student ...

  def __init__(self, **kwargs):
    self.data = dict()
    for key in kwargs:
        self[key] = kwargs[key]

If you do this, you can arbitrarily supply categories of grades and other data to a Student. However, you would then need to make sure that all the other methods have the flexibility to accommodate the variety of data that might be supplied to a Student object.


#19

Thank you, once again, Glenn.. After going over the reading and putting together all the pieces in your previous posts I've got the Student class down to the following...

class Student(dict):
  criteria = {'homework': 0.1, 'quizzes': 0.3, 'tests': 0.6}
  def __init__(self, **kwargs):
    super(Student, self).__init__()
    for key in kwargs:
      self[key] = kwargs[key]
  def average(self):
    weighted_average = 0
    for key in self.criteria:
      x = self[key]
      weighted_average += sum(x) / len(x) * self.criteria[key]
    return weighted_average
  def grades(self):
    print ("\nGrades for,  {:s}".format(self['name']))
    for key in self.criteria:
      x = self[key]
      y = ["{:0.1f}".format(item) for item in x]
      print ("{:>10s}: ".format(key), ", ".join(y))

Given all the inputs from previous, the following calls,

room.roll_call()
room.show()
room.get_averages()
print (room.class_average())

give these outputs..

Roll Call
         0:  Lloyd
         1:  Alice
         2:  Tyler

Grades for,  Lloyd
     tests:  75.0, 90.0
  homework:  90.0, 97.0, 75.0, 92.0
   quizzes:  88.0, 40.0, 94.0

Grades for,  Alice
     tests:  89.0, 97.0
  homework:  100.0, 92.0, 98.0, 100.0
   quizzes:  82.0, 83.0, 91.0

Grades for,  Tyler
     tests:  100.0, 100.0
  homework:  0.0, 87.0, 75.0, 22.0
   quizzes:  0.0, 75.0, 78.0

Weighted Averages
     Lloyd:  80.55
     Alice:  91.15
     Tyler:  79.90
Class Average: 83.87

revision #9


#20

Nice work, Roy.

There is a special hazard, however, with using **kwargs. The caller of the function might omit a named argument, supply an extra named argument that should not be there, or might misspell the name of an argument. In the case of a Student object, this would lead to either some data not getting included in the computation of the average or a KeyError. To make the instantiation process more friendly, it might be best to raise an exception accompanied by some informative description, if there is either a missing named argument or an extra one.

See the following ...

  def __init__(self, **kwargs):
    # check for missing grade category
    for key in self.criteria:
      if key not in kwargs:
        raise ValueError("missing category: {:s}".format(key))
    # check for invalid grade category
    for key in kwargs:
      if key not in (list(self.criteria.keys()) + ["name"]):
        raise ValueError("invalid category: {:s}".format(key))
    # arguments are fine; proceed
    super(Student, self).__init__()
    for key in kwargs:
      self[key] = kwargs[key]