Can I make my class summable?

https://www.codecademy.com/courses/learn-python-3/lessons/data-types/exercises/review

While working on Unit 10, Lesson 1, Exercise 14 - Review, I wrote a method for my student class that takes the average of the list of Grades it contains, with Grade being a class I wrote that contains a score. I solved the exercise successfully with an accumulating loop like so:

    def get_average(self):
        grade_sum = 0
        for grade in self.grades:
            grade_sum += grade.score
        return grade_sum / len(self.grades)

What I’m wondering, however, is if I can represent my grade class in such a way that I could instead write that method as:

    def get_average(self):
        return sum(self.grades) / len(self.grades)

Python’s sum() won’t work on my Grade type, throwing a TypeError (and understandably so). I know that the repr dunder method can be used to return a string representation of a class, but I want to find a way to allow my Grade class to be interpreted as the integer value in my_grade.score for simplified use inside other functions. I’ve tried a variety of ugly hacks potential solutions, but nothing has worked so far. The most promising route seems to be the dunder methods __add__ and __radd__, but neither the explanations I found nor the python documentation make it clear to me how these methods are used.

Here’s the complete code for context:

class Student:

    def __init__(self, name, year):
        self.name = name
        self.year = year
        self.grades = []

    def add_grade(self, grade):
        if type(grade) == Grade:
            self.grades.append(grade)

    def get_average(self):
        grade_sum = 0
        for grade in self.grades:
            grade_sum += grade.score
        return grade_sum / len(self.grades)
        # return sum(self.grades) / len(self.grades) <- This is what I want to implement


class Grade:

    minimum_passing = 65

    def __init__(self, score):
        self.score = score

    def is_passing(self):
        return self.score >= self.minimum_passing


pieter = Student('Pieter Bruegel the Elder', 'year 8')

grade_1 = Grade(100)
grade_2 = Grade(44)
grade_3 = Grade(70)

pieter.add_grade(grade_1)
pieter.add_grade(grade_2)
pieter.add_grade(grade_3)

print(pieter.get_average())
1 Like

Looking at the documentation, the following is said about sum:

The sum() function adds the items of an iterable and returns the sum.

So by that logic, we could implement a iterable in our class:

https://www.programiz.com/python-programming/iterator#build-own

so we can use sum(). At least, sounds plausible on paper.

I have never done this, so I want to see if you can figure this out :slight_smile: Either way, let me know. If you manage, well done :slight_smile: If not, I will look into it further and help you further

2 Likes

In my case, sum is already being used on an iterable. Inside the Student class I have a list of Grades assigned to self.grades. In my get_average() method, sum iterates over that list. My problem is that my Grade type inside the list is not a type that can be added (TypeError: unsupported operand type(s) for +: ‘int’ and ‘Grade’), hence why I’m researching the __add__ path. I actually think I’m fairly close to a solution, though. I stopped getting a TypeError, but the value being returned is not what it should be.

I see. But then I feel the solution is really simple, no?

we could implement __radd__ method in Grade class, and make it return the score? I tried, this seem to work. Okay, that doesn’t work… weird

Edit: implementing __radd__ then correctly helps a lot. Yep, got it working with __radd__

1 Like

Edit: You edited yours while I was writing my reply :sweat_smile:

The good news is that I ironed out the bug and got it to work. The bad news is that I don’t entirely understand how it works, and that doesn’t sit well with me (even though this is my first time learning programming, so I don’t understand a whole lot :sweat_smile: ). I have a few leads to research now though, and I’ll come back with what I’ve found. For now, here’s the working code:

class Student:

    def __init__(self, name, year):
        self.name = name
        self.year = year
        self.grades = []
        self.attendance = {}

    def add_grade(self, grade):
        if type(grade) == Grade:
            self.grades.append(grade)

    def get_average(self):
        return sum(self.grades) // len(self.grades)


class Grade:

    minimum_passing = 65

    def __init__(self, score):
        self.score = score

    def __add__(self, other):
        return self.score + other

    def __radd__(self, other):
        if other == 0:
            return self.score
        else:
            return self.__add__(other)

    def is_passing(self):
        return self.score >= self.minimum_passing


pieter = Student('Pieter Bruegel the Elder', 'year 8')

grade_1 = Grade(100)
grade_2 = Grade(44)
grade_3 = Grade(70)

pieter.add_grade(grade_1)
pieter.add_grade(grade_2)
pieter.add_grade(grade_3)

print(pieter.get_average())
2 Likes

I simply did this:

    def __radd__(self, value):
        return value + self.score

from what i read here, python somewhere checks if __radd__ implemented before raising a TypeError. sum() seems to obey this.

2 Likes

Thanks for the help :blue_heart:! I did a bit more research and fiddling around and I think I finally understand what’s going on. The if branch in the code I found was confusing me, but given that the post I found it in is nearly 5 years old, I’m going to guess it was some sort of implementation issue that no longer exists. My code is working great now and I’ve added the __add__, __radd__, __sub__, and __rsub__ methods to my class just to make sure I understand what I’m doing. I’m still having a little trouble wrapping my head around classes and class interaction, but this little exercise helped a lot!

class Student:

    def __init__(self, name, year):
        self.name = name
        self.year = year
        self.grades = []

    def add_grade(self, grade):
        if type(grade) == Grade:
            self.grades.append(grade)

    def get_average(self):
        return sum(self.grades) // len(self.grades)


class Grade:

    minimum_passing = 65

    def __init__(self, score):
        self.score = score

    def __add__(self, other):
        return self.score + other

    def __radd__(self, other):
        return other + self.score

    def __sub__(self, other):
        return self.score - other

    def __rsub__(self, other):
        return other - self.score

    def is_passing(self):
        return self.score >= self.minimum_passing


pieter = Student('Pieter Bruegel the Elder', 'year 8')

grade_1 = Grade(0)
grade_2 = Grade(44)
grade_3 = Grade(0)

pieter.add_grade(grade_1)
pieter.add_grade(grade_2)
pieter.add_grade(grade_3)

print(pieter.get_average())
print(0 + grade_3)
print(grade_3 + 5)
print(grade_3 - 10)
print(100 - grade_3)
print(grade_1 + grade_3)

Do you still have the post somewhere? I am curious

I thought I understood classes, but I only truly started to understand/appreciate classes once I started working on more serious projects/real world applications. Projects I work in now, I have dozen (if not hundreds) of classes, and that are merely the classes I made/created

Classes really help to organize the code. Python classes are not the greatest example of class implementation, private and protected methods do not really exists

Any way, don’t worry too much about it. I have been programming for years, you will get better with time :slight_smile: There are always new things to be learned, discovered and improved :slight_smile:

I was working from a ~3 year old question on StackOverflow which referenced this post (bit about __radd__ is at the end) from April 2014. Neither provided any explanation as to why the case of ‘other’ being equal to zero is handled separately instead of just returning self + 0. I thought I had read something about it somewhere, but if I did, it is unfortunately lost in the ocean of tabs I’ve been opening and closing all morning. I’ve tried every which way I could think of to add zero to my Grade (or add my Grade to zero) and get an error with the simple version of __radd__, but everything worked exactly how I expected it to. I figure it’s either an old problem, covers a use case too advanced for me to stumble into, or there was simply never a good reason for it in the first place and the StackOverflow answer just copied it verbatim.

I have a basic understanding of them, but I just feel like I’m missing some key piece that fits everything together. I have had no problems whatsoever with any of the exercises about them here on CodeCademy, but when I try to think about why I’m typing the code that I am or how I would implement these ideas into my own projects, I draw a blank. Corey Schafer’s YouTube video series on Python classes cleared up a lot of things I feel like the course didn’t explain very well, but I still have a lot further to go. It definitely feels like I can’t see the forest for the trees when it comes to classes. I’m hoping a bit of practical experience and/or a more fundamental programming course will help. Thank you for the encouragement; sometimes I have trouble reminding myself I don’t always need to understand everything immediately :blue_heart:.

Edit: I accidentally some words

In marina mele blog post its briefly explained:

So if we implement __add__ as well, we need this if condition in __radd__, given python doesn’t know about Grade. So __radd__ is called before a TypeError is thrown (that python doesn’t know how to deal/cope)

this is one of the most challenging accepts. Experience and practice are vital in programming.

can be troublesome indeed, but you need to remind yourself. There is no shortcut for experience. Sometimes I implement a solution, then the next I have to implement something similar I would do things differently. Because I learned pitfalls or potential problems with a certain solution/implementation.

I had a whole long reply typed up when I finally realized there’s a key difference between what I’m doing and what the blog is doing. In the blog’s example, the addition being defined is working on a 2-tuple, which means (if I’m understanding good programming practices correctly) that it makes more sense to call the already-defined __add__ method to avoid repeating the same code in __radd__.

The branch wasn’t because you necessarily need to branch if you implement both __add__ and __radd__ as is evidenced by the fact that my latest code works perfectly without it. Python not knowing about a user defined types necessitates using __radd__ to solve the case of int.__add__(user_type) that results from the use of sum(). The branch inside of __radd__ stems from the blog example wanting to avoid repeating the code to perform the addition of the elements of a 2-tuple. That code, which did in fact have a good reason for the branch, then got copied verbatim into the stack overflow answer I was working from where said branch was no longer necessary, and that is where all this confusion seems to have stemmed from in the first place.

Thank you so much for your patience in dealing with my weird nitpicking :sweat_smile:. If I’ve gotten something wrong in my current understanding of the issue, please do feel free to let me know. Otherwise, so long and thanks for all the fish help!

I am not entirely sure I follow, but that is okay. Been a while since I have programmed in python. As long as you understand it, its good :slight_smile: