Recall our previous discussion that introduced the object oriented way of programming. Well, it wasn't quite object oriented, but rather more like procedural oriented.
In this discussion we will see how to move the functions into the class, to encapsulate the data along with the behavior into a self-containing unit, the object.
A method is simply a function that belongs to a class.
As such they differ from functions in two distinct ways:
methods are defined inside a class definition and become attributes of the class.
methods are called syntactically differently than functions.
Let's add a method to the Time
class for printing the object. This will be the same function as what we had in our last discussion.
class Time:
def printTime(self):
print(f'{self.hour:02d}:{self.minute:02d}:{self.second:02d}')
To use this method we have two options, although one is non-conventional.
time = Time()
time.hour, time.minute, time.second = 10, 10, 10
Time.printTime(time)
Notice the use of the class Time
and the time
instance as the argument that maps to the self
parameter. The use of self
to represent the object acted upon is conventional.
time.printTime()
In this case notice the use of the time
reference and the absence of any argument to the method. Python will automatically pass a reference to the time
instance as the argument, which will subsequently map to the self
parameter.
Time
class¶Let's define the Time
class now and add some useful methods to it.
class Time:
# An initializer or constructor method.
def __init__(self, hour = 0, minute = 0, second = 0):
self.hour, self.minute, self.second = hour, minute, second
# A descriptor method.
def __str__(self):
return '{:02d}:{:02d}:{:02d}'.format(self.hour, self.minute, self.second)
def time2seconds(self):
return self.hour * 3600 + self.minute * 60 + self.second
# A utility method.
@staticmethod
def seconds2time(sec):
time = Time()
time.hour, sec = divmod(sec, 3600)
time.minute, time.second = divmod(sec, 60)
return time
# Add time method.
def addTime(self, other):
sec = self.time2seconds() + other.time2seconds()
return self.seconds2time(sec)
# Increment method.
def addSeconds(self, sec):
sec += self.time2seconds()
return self.seconds2time(sec)
# isAfter method.
def isAfter(self, other):
return (self.hour, self.minute, self.second) > (other.hour, other.minute, other.second)
# Overloaded '+' operator.
# Handles: time + time or time + sec
def __add__(self, other):
if isinstance(other, Time):
return self.addTime(other)
else:
return self.addSeconds(other)
# Overloaded '+' operator.
# Handles: sec + time
def __radd__(self, other):
return self.__add__(other)
def __init__(self, hour = 0, minute = 0, second = 0):
self.hour, self.minute, self.second = hour, minute, second
lines 3-4: define the initializer method or constructor. Its main purpose is to initialize the instace variables.
Notice how each parameter is given default values (0
) which makes each an optional parameter. Each parameter is assigned to its corresponding attribute.
t0 = Time() # default hour, minute, and second
t1 = Time(10) # default minute and second
t2 = Time(10, 10) # default second
t3 = Time(9, 45, 0)
lines 7-8: define the descriptor method. It describes the object as the formatted string: hh:mm:ss
.
print(t0)
print(t1)
print(t2)
print(t3)
00:00:00 10:00:00 10:10:00 09:45:00
def __str__(self):
return '{:02d}:{:02d}:{:02d}'.format(self.hour, self.minute, self.second)
lines 10-11: define the conversion method that converts a time to seconds.
print(t3.time2seconds())
35100
@staticmethod
def seconds2time(sec):
time = Time()
time.hour, sec = divmod(sec, 3600)
time.minute, time.second = divmod(sec, 60)
return time
lines 15-21: define yet another conversion method; this time from seconds to time.
This is a special method, a static method used as a utility method by the other methods. Notice that you do not list self
as the first argument for this one. Its intended purpose is to support other methods, not clients of the class.
Notice the decorator @staticmethod
that identifies the method as a static method.
print(Time.seconds2time(35100))
09:45:00
def addTime(self, other):
sec = self.time2seconds() + other.time2seconds()
return self.seconds2time(sec)
lines 24-26: define the method that returns a new Time
object by adding self
and other
together.
start = Time(9, 45)
duration = Time(1, 35)
end = start.addTime(duration)
print(end)
11:20:00
def addSeconds(self, sec):
sec += self.time2seconds()
return self.seconds2time(sec)
lines 29-31: define the method that returns a new Time
object by adding self
with sec
, an int
.
end = start.addSeconds(5700)
print(end)
11:20:00
def isAfter(self, other):
return (self.hour, self.minute, self.second) > (other.hour, other.minute, other.second)
lines 34-35: define a method that indicates the ordering of two Time
objects.
print(end.isAfter(start))
True
def __add__(self, other):
if isinstance(other, Time):
return self.addTime(other)
else:
return self.addSeconds(other)
lines 39-43: define our first method that overloads the +
operator. When the +
operator is used with two Time
objects, this method is called instead.
Overloading operators (there are numerous) makes your coding more natural and easier to write/read.
I have added a simple test to determine if the other
parameter is a Time
or an int
and call the appropriate method to handle each.
The isinstance()
function tests whether its first argument is of the type of its second argument.
This type of test is called type-based dispatch since we are dispatching the appropriated method call based on the test results.
print(start + duration)
print(start + 5700)
11:20:00 11:20:00
def __radd__(self, other):
return self.__add__(other)
lines 47-48: define our second overloaded operator. This version handles the case where the left hand side operand is an integer. It simply calls the regular add.
print(5700 + start)
11:20:00
This is a topic we will discuss next, but bears a mention here.
Polymorphism allows for the execution of the proper method based on the type of arguments provided. In other words, Python is able to determine which method to call (dispatch) given some argument.
Python has many built-in functions that work on several types that share the same characteristics, i.e. len()
can be used with strings, lists, dictionaries, tuples and so on.
Obviously each internal determination will be different based on the actual type being analyzed, but that is trasparent to us and makes it easier for us to use it without having to be concerned with details.
In object oriented programming, one of the goals is to provide software that is more maintainable.
This means that small changes should not trickle down to major changes. This can be accomplished by making the clear distinction between the interface and the implementation.
Basically, we should be able to change the implementation of a class and not impact any client code of the class. As things change and new techniques discovered, we should be able to use them without fear we are breaking existing code.
Changes to the interace thus must be minimized, since any such changes will impact the clients of the class.
Developing the right interface takes skill and good design.
Create a separate Python source file (.py) in VSC to complete each exercise.
Use the Time
class from this discussion as your starting point.
Change the attributes of Time
to be a single integer representing seconds since midnight.
Then modify the methods and the function time2seconds()
to work with the new implementation. You should not have to make any changes to the test code.
When you are done, the output should be the same as before.
This exercise is a cautionary tale about one of the most common, and difficult to find, errors in Python. Write a definition for a class named Kangaroo
with the following methods:
An __init__()
method that initializes an attribute named pouchContents
to an empty list.
A method named putInPouch()
that takes an object of any type and adds it to pouchContents
.
A __str__()
method that returns a string representation of the Kangaroo
object and the contents of the pouch.
Use the following testing code to test your class and see the issue created.
def main():
kanga = Kangaroo()
kanga.putInPouch('kanga')
roo = Kangaroo()
roo.putInPouch('roo')
kanga.putInPouch(roo)
print(kanga)
main()
Now try to fix the issue.
Hint: When adding a Kangaroo
object in the pouch, what should really be added?