Preparation#
Reading material#
In the Think Python (TP) book, inheritance is covered in Chapter 17 Inheritance. Operator overloading is covered in one section of Chapter 15 Classes and Methods.
Looking at the lecture notes for the CS50 course, this week is covered in two sections of Lecture 8 Object-Oriented Programming, sections on inheritance and operator overloading.
Copy-and-Run#
Prep 11.1: Inheritance#
Try to figure out what the following class is intended to do.
class NameTracker:
def __init__(self):
self.longest = ''
self.shortest = ''
def add(self, name):
if len(name) > len(self.longest):
self.longest = name
if len(name) < len(self.shortest) or self.shortest=='':
self.shortest = name
def print(self):
print('Names tracked so far')
print(' Longest :', self.longest)
print(' Shortest:', self.shortest)
Now try it out using the code below. You should place both the class definition and the code using the class in the same script. In general this week, we will often only provide code for you to add or modify. This is to avoid having long code cells in the exercises.
girls = NameTracker()
girls.add('Mary')
girls.print()
girls.add('Augusta')
girls.print()
girls.add('Josephine')
girls.print()
What would happen if you added 'Pippilotta Rullegardinia'
to girls
?
Imagine that you would like the name tracker which perform some checks, before assigning a name to one of its attributes. For example, you want to ignore the names with a space or a hyphen in them. But, you want to keep the functionality of NameTracker
unchanged. Look now carefully at the following code, and execute it.
class CarefulNameTracker(NameTracker):
def add(self, name):
if not (' ' in name or '-' in name):
if len(name) > len(self.longest):
self.longest = name
if len(name) < len(self.shortest) or self.shortest=='':
self.shortest = name
girls_first = NameTracker()
girls_second = CarefulNameTracker()
for name in ['Mary', 'Augusta', 'Josephine', 'Pippilotta Rullegardinia', 'I-I']:
girls_first.add(name)
girls_second.add(name)
girls_first.print()
print()
girls_second.print()
This is an example of inheritance. We call the one class a parent class (or superclass), and the other a child class (or subclass).
Since inheritance involves two classes, the examples on inheritance contain more than just few lines of code. But let’s try to identify the key elements of inheritance in the code above. Try to answer the following questions.
What is the parent class? What is the child class? And where in the code do we say to Python that one class is the parent of the other?
When calling
girls_first.add(name)
which method is called? How does Python know which method to call?When calling
girls_second.add(name)
which method is called? How does Python know which method to call?When calling
girls_first.print()
which method is called? How does Python know which method to call?When calling
girls_second.print()
which method is called? How does Python know which method to call?When
girls_first
was created, which constructor is called? Why?When
girls_second
was created, which constructor is called? Why?
You can add the print statements to the code to see which methods are called. For example, add print('Method add from NameTracker called!')
as the first line of the method, and similar for the other methods. Than add just one name at a time.
Answers
The parent class is
NameTracker
and the child class isCarefulNameTracker
. This is dictated by the lineclass CarefulNameTracker(NameTracker)
.When calling
girls_first.add(name)
the methodadd
from theNameTracker
class is called. Python knows which method to call because it checks the type ofgirls_first
, andgirls_first
is an object of theNameTracker
class. (Try printingtype(girls_first)
.)When calling
girls_second.add(name)
the methodadd
from theCarefulNameTracker
class is called. Again, python knows which method to call because it checks the type ofgirls_second
, andgirls_second
is an object of theCarefulNameTracker
class.When calling
girls_first.print()
the methodprint
from theNameTracker
class is called, for the same reasons as before.When calling
girls_second.print()
the methodprint
from theNameTracker
class is called. This is because theCarefulNameTracker
class does not have aprint
method, so Python looks for the method in the parent class.When
girls_first
was created asNameTracker()
, so the constructor of theNameTracker
class is called. TWhen
girls_second
was created asCarefulNameTracker()
, the constructor of theNameTracker
class is called. This is because theCarefulNameTracker
class does not have a constructor, so Python looks for the constructor in the parent class.
In the example above, we say that the add
method in the CarefulNameTracker
class overrides the add
method in the NameTracker
class.
Prep 11.2: Function super()
#
In the code above the add
method from CarefulNameTracker
contains a copy of the code from the add
method in NameTracker
. An experienced programmer would make sure to reuse as much code as possible. Try to use this method in the CarefulNameTracker
class. Make sure to indent the code correctly.
def add(self, name):
if not (' ' in name or '-' in name):
super().add(name)
Does the code work as expected? Did the CarefulNameTracker
have any names added to it, and were those name as expected?
Try to answer the following questions:
Which method is called when
girls_second.add(name)
is executed? Why?Did the
add
method in theNameTracker
got executed? How do you know? Which code made it happen?What do you thing the
super()
function does?
Answers
The
add
method from theCarefulNameTracker
class is called, becausegirls_second
is an object of theCarefulNameTracker
class, andCarefulNameTracker
has anadd
method.The
add
method in theNameTracker
class was executed. We know this because the names got added to the attributes of the tracker, and the only place where this happens is in theadd
method of theNameTracker
class. It must be thesuper().add(name)
line that makes this happen.The
super()
function gives access to the methods from the parent class. In this case, it calls theadd
method from theNameTracker
class.
The same principle can be applied to the constructor __init__
. It is very common that child class has some additional attributes that need to be initialized.
Imagine that we want the CarefulNameTracker
to have an additional attribute forbidden_characters
, which is a list of characters that are not allowed in the names. We want to initialize this list to [' ', '-']
in the constructor. Look now at this code, and add it to the class definition, of the CarefulNameTracker
class.
def __init__(self, forbidden_characters):
self.forbidden_characters = forbidden_characters
super().__init__()
Initialize a name tracker as girls_second = CarefulNameTracker([' ', '-', '_', '&'])
. Try to access the forbidden_characters
attribute of the girls_second
object. Try to access the attributes longest
and shortest
.
Try removing the super().__init__()
line from the constructor of CarefulNameTracker
. Can you still access the longest
and shortest
attributes of the girls_second
object? You were able to access them before adding __init__
to the CarefulNameTracker
class. What do you think happened?
Answer
Before adding __init__
to subclass, it inherits the __init__
method from the parent class, where the attributes are initialized. After adding __init__
to the subclass, the __init__
method from the parent class is not called. Therefore, we need to make sure that all the attributes are initialized in the subclass as well, for example by calling super().__init__()
.
Try adding the names 'Pippi'
, 'Pippilotta Rullegardinia'
, and 'Pippi_Longstocking'
to the girls_second
object. Which names are added to the tracker? Why?
You have probably realized that we have not yet implemented the check for the forbidden characters. Modify the add
method of the CarefulNameTracker
as show below.
def add(self, name):
valid = True
for char in self.forbidden_characters:
if char in name:
valid = False
if valid:
super().add(name)
You have seen that child class inherits the methods from the parent class. Let’s investigate whether the opposite is true. Add this method to the CarefulNameTracker
class.
def print_lengths(self, name):
print('Lengths of names tracked so far')
print(' Longest :', len(self.longest))
print(' Shortest:', len(self.shortest))
Try now to call this method from the parent class.
girls_test = NameTracker()
girls_test.add('Mary')
girls_test.print_lengths()
Does inheritance work in both directions?
Prep 11.3: Special Methods#
Look at this definition for the Time
class.
class Time:
def __init__(self, hours, minutes):
self.hours = hours
self.minutes = minutes
def increment_hours(self, ):
self.hours += 1
if self.hours == 24:
self.hours = 0
def increment_minutes(self, ):
self.minutes += 1
if self.minutes == 60:
self.minutes = 0
self.increment_hours()
def print(self):
print(f"{self.hours:02}:{self.minutes:02}")
Predict what the following code will do, assuming that the Time
class has been defined as shown above. Run the code to verify your prediction.
my_time = Time(13, 55)
for i in range(10):
my_time.increment_minutes()
my_time.print()
Now try to run the following code, again assuming that you have defined Time
and my_time
as shown above.
print(my_time)
print(str(my_time))
You have already seen that the text displayed by the build-in print
function seems somewhat useless, and so does the string obtained when we call str(my_time)
. We made our own print
method, but it is not used when we call print(my_time)
.
It turns out that the build-in print
and str()
functions both call a __str__()
method, which is built into all classes in Python. The default behavior of the __str__()
method for the Time
class is not very useful for us.
Add the following method to the Time
class, and make sure to indent it correctly.
def __str__(self):
return f"{self.hours:02d}:{self.minutes:02d}"
Now try printing my_time
again. Try also to print str(my_time)
. What is the result now?
Now, the str()
function has a customized behavior for the Time
class. This is sometimes called overloading.
Remove the print
method from the definition of the Time
. What happens now if you ask for my_time.print()
? What happens if you ask for print(my_time)
Prep 11.4: Operator Overloading#
You have seen that by overriding (customizing) __str__()
method, we can change the behavior of the str()
function. It turns out that you can do similar to change the behavior of operators like +
, -
, *
, /
, ==
, <
, >
.
Let’s start by investigating what ==
gives for two Time
objects.
first_time = Time(13, 45)
second_time = Time(13, 45)
print(first_time==first_time)
print(first_time==second_time)
print(second_time==second_time)
This is not useful. The default behavior checks whether the memory addresses for the two objects are equal.
Add this method to the definition of Time
.
def __eq__(self, other):
return (self.hours == other.hours) and (self.minutes == other.minutes)
Try now comparing first_time
and second_time
. Does it work differently than before? Define third_time = Time(8, 15)
and compare that against first_time
.
Let’s now try to overload operator +
. Check the default behavior.
print(first_time + second_time)
Is there a default behavior for the addition of two Time
objects?
Add this method to the class Time
.
def __add__(self, other):
minutes = self.minutes + other.minutes
hours = self.hours + other.hours
if minutes >= 60:
minutes = minutes - 60
hours = hours + 1
if hours >= 24:
hours = hours - 24
return Time(hours, minutes)
Now try adding two Time
objects. Does it work as expected?
Self quiz#
Question 11.1#
Which of the following is the correct way to define a class Dog
that inherits from a class Animal
?
Question 11.2#
What is the relationship between the class Bird
and the class Parrot
in the following code snippet?
class Bird:
pass
class Parrot(Bird):
pass
Question 11.3#
What is printed when the following code is executed?
class Animal:
def sound(self):
return "Some sound"
class Dog(Animal):
def sound(self):
return "Bark"
print(Dog().sound())
Question 11.4#
What is printed when the following code is executed?
class Animal:
def sound(self):
return "Some sound"
class Dog(Animal):
def sound(self):
return "Bark"
print(Animal().sound())
Question 11.5#
What is printed when the following code is executed?
class Animal:
def sound(self):
return "Some sound"
class Dog(Animal):
pass
print(Dog().sound())
Question 11.6#
What is printed when the following code is executed?
class Animal:
def sound(self):
return "Some sound"
class Dog(Animal):
pass
print(Animal().sound())
Question 11.7#
What is printed when the following code is executed?
class Animal:
pass
class Dog(Animal):
def sound(self):
return "Bark"
print(Dog().sound())
Question 11.8#
What is printed when the following code is executed?
class Animal:
pass
class Dog(Animal):
def sound(self):
return "Bark"
print(Animal().sound())
Question 11.9#
What is printed when the following code is executed?
class One:
def say(self):
print('Hi')
class Two(One):
def say(self):
super().say()
super().say()
Two().say()
Question 11.10#
What is printed when the following code is executed?
class Single:
def __init__(self):
self.one = 1
class Double(Single):
def __init__(self):
self.two = 2
print(Double().one)
Question 11.11#
What is printed when the following code is executed?
class Single:
def __init__(self):
self.one = 1
class Double(Single):
def __init__(self):
self.one = 2
super().__init__()
print(Double().one)
Question 11.12#
Which special method should be defined to customize how class objects are printed by the print()
function?
Question 11.13#
What is printed when the following code is executed?
class Star:
def __init__(self, name):
self.name = name
def __str__(self):
return f'Name: {self.name}'
sun = Star('Sun')
print(sun)
Question 11.14#
In what order will the boolean values be printed when the following code is executed?
class Number:
def __init__(self, nr):
self.value = nr
a = Number(5)
b = Number(6)
c = Number(5)
print(a == a)
print(a == b)
print(a == c)
Question 11.15#
In what order will the boolean values be printed when the following code is executed?
class Number:
def __init__(self, nr):
self.value = nr
def __eq__(self, other):
return self.value == other.value
a = Number(5)
b = Number(6)
c = Number(5)
print(a == a)
print(a == b)
print(a == c)
Question 11.16#
In what order will the boolean values be printed when the following code is executed?
class Number:
def __init__(self, nr):
self.value = nr
def __eq__(self, other):
return abs(self.value - other.value) < 3
a = Number(5)
b = Number(6)
c = Number(5)
print(a == a)
print(a == b)
print(a == c)
Question 11.17#
What is printed when the following code is executed?
class Number:
def __init__(self, nr):
self.value = nr
a = Number(5)
b = Number(6)
c = a + b
print(c.value)
Question 11.18#
Which method is used to overload the +
operator in Python?
Question 11.19#
What is printed when the following code is executed?
class Number:
def __init__(self, nr):
self.value = nr
def __add__(self, other):
return self.value + other.value
a = Number(5)
b = Number(6)
c = a + b
print(c.value)
Question 11.20#
What is printed when the following code is executed?
class Number:
def __init__(self, nr):
self.value = nr
def __add__(self, other):
return Number(self.value + other.value)
a = Number(5)
b = Number(6)
c = a + b
print(c.value)