Classes and Methods

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.

Methods

A method is simply a function that belongs to a class.

As such they differ from functions in two distinct ways:

  1. methods are defined inside a class definition and become attributes of the class.

  2. methods are called syntactically differently than functions.

Printing Objects

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.

As a function - 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.

As a method - 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.

__init__() : The initializer

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.

__self__() : The descriptor

lines 7-8: define the descriptor method. It describes the object as the formatted string: hh:mm:ss.

time2seconds() : A converter

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.

seconds2time() : Another converter

@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.

addTime() : A two Time adder

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.

addSeconds() : A Time + int adder

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.

isAfter() : A boolean ordering method

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.

__add__() : An overloaded operator '+' (Time + Time or Time + int)

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.

_radd__() : Right-side add overloaded operator '+' (int + Time)

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.

Polymorphism

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.

Interface and Implementation

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.

Practice problems

Create a separate Python source file (.py) in VSC to complete each exercise.


p1: mytime.py

Use the Time class from this discussion as your starting point.

  1. Change the attributes of Time to be a single integer representing seconds since midnight.

  2. 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.

  3. When you are done, the output should be the same as before.


p2: kangaroo.py

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:

  1. An __init__() method that initializes an attribute named pouchContents to an empty list.

  2. A method named putInPouch() that takes an object of any type and adds it to pouchContents.

  3. A __str__() method that returns a string representation of the Kangaroo object and the contents of the pouch.

  4. 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?

[Classes and Functions] [TOC] [Inheritance]