39-8 Overloading technique

Hi,

I’m having difficulties trying to understand the overloading technique in Object-Oriented Programming.

In

def __lt__(self, other): 
    return self.average_age() < other.average_age()

what is the other parameter taking when we run Team("Utah Jazz")?

Thanks.

Regards,
Miquel

Hey, Hahamiyo.

There’s something that causes me a little confusion. The idea is the same as in the previous screen. Did you also not understand the previous screen?

If you understood it, then can you try to clarify what is it about screen 8 that is different from screen 7?

My question applies to previous and following screens where the parameter “other” is passed. Thanks.

When you compare two objects of the same type, in this case to see if one is less than (<) the other, the function that performs the comparison (__lt__) would need access to both objects, right?

So the self parameter is the object you want to compare, and the other parameter is the object you want to compare it with.

For instance

>>> 3 < 5
>>> True

Here, 3 is self and 5 is other

In reality, you could name the self and other parameters any way you wanted, just like when you write custom functions. Calling them self and other is just a convention and considered best practice.

I hope this clarified it for you a bit.

The whole code-snippet looks like this:

older_team = max(Team("Utah Jazz"), Team("Detroit Pistons"))

The other parameter is the “Detroit Pistons” team, which is being compared to the “Utah Jazz” team.

It’s better to say instantiate Team("Utah Jazz"), i.e. we create an object whose type is Team which will take as a defining feature the string Utah Jazz.
Let’s think about a more familiar example: int("3"). As you probably know, this creates an integer, namely 3. When you run a script whose only line of code is int("3"), it creates the object and that’s it.
This is important because comparison doesn’t make sense unless you have two arguments to compare.

Now let’s instead create two integers and compare them.

>>> x=int("3")
>>> y=int("5")
>>> x<y
True
>>> x==y
False
>>> y>x
True

Hopefully this makes it clear that we need another object to compare Team("Utah Jazz") with.


The familiar notations x<y, x==y and y>x can be seen as mere syntactic sugar, they exist as replacement of more formal versions, respectively:

>>> x.__lt__(y)
True
>>> x.__eq__(y)
False
>>> y.__gt__(x)
True

So the comparison operators really are methods. More on this in the docs.

The other parameter is whatever inside the parentheses. I the examples above other is respectively the values of y, y and x. And below it is the number 1337.

>>>x.__gt__(1337)
False

Where as self is the object whose method we’re using.


The goal of screens 7 and 8 is to show that the meaning of <, == and so on can be modified (or overloaded, or overridden, or overloaded, whatever you prefer).

On screen 7 we read the following.

These methods already exist in the object class by default, and we’ve used these operators to compare integers, floating point numbers (decimals), and strings. The operators work because classes like string have implemented them specifically.

Since the class Team was created from object, it will inherit its methods. Since the comparison operators are methods in object, they are inherited from this class.

On screen 8 we’re tasked with redefining these methods in a way that team_1 □ team_2 compares (in the usual sense) the average age of team_1 with the average age of team_2, where can be any of the typical comparison operators: <, == and so on.

Technically, int("3") does not create a new integer object. Python will return the reference of an existing integer 3 back to you.

According to Python 2 Documentation for “Plain Integer” (same for Python 3),

The current implementation keeps an array of integer objects for all integers between -5 and 256 , when you create an int in that range you actually just get back a reference to the existing object.

Whenever you have an integer value between -5 and 256 inclusively, the reference is always the same to the integer of the same value between -5 and 256 inclusively. That is, there only exists one memory location or reference to integer belong to the range -5 and 256 inclusively.

Common pitfalls are when you use is operator to check for values between integers.

If you use the is operator to compare two integers, a comparison will always equate to true when the value is the same but within the range from -5 to 256 inclusively.

>>> x = 256
>>> y = int("256")
>>> id(y)
4544169264
>>> id(x)
4544169264
>>> x is y
True

If you use the is operator to compare two integers, a comparison will always equate to false when the value is the same but outside of the range from -5 to 256.

>>> x = 257
>>> y = int("257")
>>> id(y)
4550895472
>>> id(x)
4546461456
>>> x is y
False

Therefore, use == equality operator to compare the value between two integers.

Within the range [-5, 256]:

>>> x = 256
>>> y = int("256")
>>> x == y
True

Outside of the range [-5, 256]:

>>> x = 257
>>> y = int("257")
>>> x == y
True
1 Like

Great stuff! Thanks for catching this.

1 Like

No worries. Not many people know this. Good to share.

Hey @hahamiyo,

We have a solved feature that allows you the ability to mark something as the “correct” answer, which helps future students with the same question quickly find the solution they’re looking for.

Here’s an article on how to mark posts as solved - I don’t want to do this for you until I know that solution/explanation works.

Best,
Alvin.

1 Like

Thank you all for your explanations, but I’m still confused. When we instantiate Team (“Utah Jazz”), what values are self.average_age() and other.average_age() taking?

When you instantiate Team (“Utah Jazz”), a new Team object is created. This Team object has two attributes: self.team_name and self.roster, as seen below.

class Team(object):
    def __init__(self, team_name):
        self.team_name = team_name
        self.roster = []
        for row in nba:
            if row[3] == self.team_name:
                self.roster.append(Player(row))

Unless you call the method average_age() on your Team object by comparing it to another Team object, the “other” parameter never comes into play. Can you see why?

In the function I’m talking about, average_age() always comes into play to make comparisons. And those comparisons are returned as True or False. Am I right?

   def __init__(self, team_name):
       self.team_name = team_name
       self.roster = []
       for row in nba:
           if row[3] == self.team_name:
               self.roster.append(Player(row))
   def num_players(self):
       count = 0
       for player in self.roster:
           count += 1
       return count
   def average_age(self):
       return math.fsum([player.age for player in self.roster]) / self.num_players()
   def __lt__(self, other):
       return self.average_age() < other.average_age()
   def __gt__(self, other):
       return self.average_age() > other.average_age()
   def __le__(self, other):
       return self.average_age() <= other.average_age()
   def __ge__(self, other):
       return self.average_age() >= other.average_age()
   def __eq__(self, other):
       return self.average_age() == other.average_age()
   def __ne__(self, other):
       return self.average_age() != other.average_age()

True

Please wrap your code inside backticks ``` your code here ``` for better formatting!

So,what values are self.average_age() and other.average_age() taking when we instantiate Team(“Utah Jazz”)? (Can the moderators please take a look into this?)

There are many reasons why the code below is bad. Rationale behind bad and good examples are given below. And the updated code section fixes all the mentioned issues.

The format of the examples are given as follows:

Bad Title
Bad Code
Why? Bad

Good Title
Good Code
Why? Good

Bad: Possible unintended modification to the original list

self.roster = []
for row in nba:
       if row[3] == self.team_name:
           self.roster.append(Player(row))

Why?

  1. Within the for loop, there might be a possibility that the original list get modified which is not intended.

  2. From reading the logic of for loop, it shows the process of how we want to add append data. It does not show top level idea of self.roster contains only players from the same team name.

Good: Use Python list comprehension.

self.roster = [Player(row) for row in nba if row[3] == self.team_name]

Why?

  1. To prevent any modification to the original list nba.
  2. New instance of an object is created for each element in the new list.
  3. List comprehension optimized for speed and is way faster than original loop using for
  4. A single line of thought can express exactly what self.roster contains. That is, self.roster contains any player that belongs to the same team name.

This helps with data integrity and prevent future bugs when unwanted behavior due to changes made to the new list resulted in modification of the original list.

Bad: Time complexity is O(N).

 def num_players(self):
       count = 0
       for player in self.roster:
           count += 1
       return count

Why?

  1. Do we really need to iterate through every single element of the list self.roster to count the size or length of the list?

  2. The time complexity of function num_players is dominated by the for loop. That is, the function performs O(1) * N operations, where N is the total elements in the list.

Good: Time complexity is O(1).

 def num_players(self):
       return len(self.roster) if self.roster else 0 

Why?

  1. Since we know that self.roster is of type list, we can use the built-in len method to count the number of players.

Example usage of len:

>>> x = [1, 3, 4]
>>> len(x)
3
>>> len([])
0
>>> len(None)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: object of type 'NoneType' has no len()

len(None) gives an error. So we want to handle this edge case, by checking if self.roster. If self.roster is not None, it will return True. Otherwise, False.

  1. The time complexity of function num_players is now improved to O(1) which is constant.

Bad: Space Complexity O(N). Additional list space created is not necessary.

math.fsum([player.age for player in self.roster]) 

Why?

  1. Since we are only interested performing some operation/computation on elements, there is no need for an additional space to contain the elements.

  2. Space complexity is O(N) since we have to store all N elements within a list.

Good: Space Complexity O(1). No additional space was created.

math.fsum(player.age for player in self.roster) 

Why?

  1. No more additional list generated by removing the brackets [ and ]. Now the expression becomes a generator comprehension. No additional space is created.

  2. Space complexity is O(1) which is constant.

Bad: Possible divide by zero error.

def average_age(self):
       return math.fsum(player.age for player in self.roster)/self.num_players()

Good: Avoids divide by zero.

   def average_age(self):
       return math.fsum(player.age for player in self.roster)/len(self.roster) if len(self.roster) > 0 else 0 

Updated code:

class Team(object):

   def __init__(self, team_name):
       self.team_name = team_name
       self.roster = [Player(row) for row in nba if row[3] == self.team_name]

   def num_players(self):
       return len(self.roster) if self.roster else 0 

   def average_age(self):
       return math.fsum(player.age for player in self.roster)/len(self.roster) if len(self.roster) > 0 else 0  

   def __lt__(self, other):
       return self.average_age() < other.average_age()
   def __gt__(self, other):
       return self.average_age() > other.average_age()
   def __le__(self, other):
       return self.average_age() <= other.average_age()
   def __ge__(self, other):
       return self.average_age() >= other.average_age()
   def __eq__(self, other):
       return self.average_age() == other.average_age()
   def __ne__(self, other):
       return self.average_age() != other.average_age()

Reply to @hahamiyo question:

utah = Team("Utah Jazz")

When you instantiate Team(“Utah Jazz”), utah.roster gets initialize to contains all players from “Utah Jazz”; and utah.name gets initialize to “Utah Jazz”.

utah.average_age and utah.num_players are function. Functions do not get initialize.

utah.average_age()

The above function call returns the average age of players in “Utah Jazz”.

Suppose there’s another team called “Seattle Rockets”.

utah = Team("Utah Jazz")
seattle = Team("Seattle Rockets")
utah > seattle

The above comparison is made.

Then from left to right, the python interpreter will read as utah.__gt__(seattle).

Now utah is self and seattle is other with respect to the Class Team definition.

Then, python interpreter will go to the Class Team definition and go to __gt__ function definition.

Perform return self.average_age() > other.average_age()

Which in turn will perform return utah.average_age() > seattle.average_age()

If utah’s average age is greater than seattle’s, then the function will return True. Otherwise, False.

Only when a comparison between two objects is made, then it will called one of the following functions depending on the operator:

  • > operator was used in a comparison, then Python interpreter will call the object’s function __gt__.

  • < operator was used in a comparison, then Python interpreter will call the object’s function __lt__.

  • == equality operator was used in a comparison, then Python interpreter will call the object’s function __eq__.

  • != not equality operator was used in a comparison, then Python interpreter will call the object’s function __ne__.

  • <= operator was used in a comparison, then Python interpreter will call the object’s function __le__.

  • >= operator was used in a comparison, then Python interpreter will call the object’s function __ge__.

Team object is a custom object.

And, in order to support >, <, >=, <=, ==, != comparison between two Team objects, the class Team needs to implement __gt__, __lt__, __ge__, __le__, __eq__, and __ne__.

In other words, through implementing these functions, we are simply overriding these function from the parent object.

Overriding means replacing the existing function (whether from inheriting from a parent or via an abstract class by polymorphism) within the current object (in our case is Team).

1 Like

Thanks a lot alvinctk for your thorough explanation. In relation to the code, I think that you should update the Dataquest course with your improvements.

2 Likes

Hey @hahamiyo,

I do not work fo DQ and need to ask DQ staff to update.

We have a solved feature that allows you the ability to mark something as the “correct” answer, which helps future students with the same question quickly find the solution they’re looking for.

Here’s an article on how to mark posts as solved - I don’t want to do this for you until I know that solution/explanation works.

Best,
Alvin.

@Sahil, can you help the code on the DQ Mission.

We won’t be making this modification for the following reasons:

  • There’s no bug here.
  • This course is towards the end of the path and the material is covered earlier. We will eventually discontinue this course.
  • We could use some work with regards to best practices, but this needs to be tackled holistically. This modification would just be a drop in the ocean.