As another example of a user-defined type, we’ll define a class called MyTime that records the time of day. We’ll provide an __init__ method to ensure that every instance is created with appropriate attributes and initialization. The class definition looks like this:
The state diagram for the object looks like this:
We should also add a __str__ method so that MyTime objects can print themselves decently.
In the next few sections, we’ll write two versions of a function called add_time, which calculates the sum of two MyTime objects. They will demonstrate two kinds of functions: pure functions and modifiers.
The following is a rough version of add_time:
The function creates a new MyTime object and returns a reference to the new object. This is called a pure function because it does not modify any of the objects passed to it as parameters and it has no side effects, such as updating global variables, displaying a value, or getting user input.
Here is an example of how to use this function. We’ll create two MyTime objects: currentTime, which contains the current time; and breadTime, which contains the amount of time it takes for a breadmaker to make bread. Then we’ll use add_time to figure out when the bread will be done.
The output of this program is 12:49:30, which is correct. On the other hand, there are cases where the result is not correct. Can you think of one?
The problem is that this function does not deal with cases where the number of seconds or minutes adds up to more than sixty. When that happens, we have to carry the extra seconds into the minutes column or the extra minutes into the hours column.
Here’s a better version of the function:
This function is starting to get bigger, and still doesn’t work for all possible cases. Later we will suggest an alternative approach that yields better code.
There are times when it is useful for a function to modify one or more of the objects it gets as parameters. Usually, the caller keeps a reference to the objects it passes, so any changes the function makes are visible to the caller. Functions that work this way are called modifiers.
increment, which adds a given number of seconds to a MyTime object, would be written most naturally as a modifier. A rough draft of the function looks like this:
The first line performs the basic operation; the remainder deals with the special cases we saw before.
Is this function correct? What happens if the parameter seconds is much greater than sixty? In that case, it is not enough to carry once; we have to keep doing it until seconds is less than sixty. One solution is to replace the if statements with while statements:
This function is now correct when seconds is not negative, and when hours does not exceed 23, but it is not a particularly good solution.
Once again, OOP programmers would prefer to put functions that work with MyTime objects into the MyTime class, so let’s convert increment to a method.
The transformation is purely mechanical — we move the definition into the class definition and (optionally) change the name of the first parameter to self to fit with Python style conventions.
Now we can invoke increment using the syntax for invoking a method, as on line 32. Again, the object on which the method is invoked gets assigned to the first parameter, self. The second parameter, seconds gets the value 70.
Often a high-level insight into the problem can make the programming much easier.
In this case, the insight is that a MyTime object is really a three-digit number in base 60! The second component is the ones column, the minute component is the sixties column, and the hour component is the thirty-six hundreds column.
When we wrote add_time and increment, we were effectively doing addition in base 60, which is why we had to carry from one column to the next.
This observation suggests another approach to the whole problem — we can convert a MyTime object into a single number and take advantage of the fact that the computer knows how to do arithmetic with numbers. The following method is added to the MyTime class to convert any instance into a corresponding number of seconds:
Now, all we need is a way to convert from an integer back to a MyTime object. Supposing we have tsecs seconds, some integer division and mod operators can do this for us:
You might have to think a bit to convince yourself that this technique to convert from one base to another is correct.
In OOP we’re really trying to wrap together the data and the operations that apply to it. So we’d like to have this logic inside the MyTime class. A good solution is to rewrite the class initializer so that it can cope with initial values of seconds or minutes that are outside the normalized values. (A normalized time would be something like 3 hours 12 minutes and 20 seconds. The same time, but unnormalized, could be 2 hours 70 minutes and 140 seconds.)
Let’s rewrite a more powerful initializer for MyTime:
Now we can rewrite add_time like this:
This version is much shorter than the original, and it is much easier to demonstrate or reason that it is correct.
In some ways, converting from base 60 to base 10 and back is harder than just dealing with times. Base conversion is more abstract; our intuition for dealing with times is better.
But if we have the insight to treat times as base 60 numbers and make the investment of writing the conversions, we get a program that is shorter, easier to read and debug, and more reliable.
It is also easier to add features later. For example, imagine subtracting two MyTime objects to find the duration between them. The naive approach would be to implement subtraction with borrowing. Using the conversion functions would be easier and more likely to be correct.
Ironically, sometimes making a problem harder (or more general) makes the programming easier, because there are fewer special cases and fewer opportunities for error.
Specialization versus Generalization
Computer Scientists are generally fond of specializing their types, while mathematicians often take the opposite approach, and generalize everything.
What do we mean by this?
If we ask a mathematician to solve a problem involving weekdays, days of the century, playing cards, time, or dominoes, their most likely response is to observe that all these objects can be represented by integers. Playing cards, for example, can be numbered from 0 to 51. Days within the century can be numbered. Mathematicians will say “These things are enumerable — the elements can be uniquely numbered (and we can reverse this numbering to get back to the original concept). So let’s number them, and confine our thinking to integers. Luckily, we have powerful techniques and a good understanding of integers, and so our abstractions — the way we tackle and simplify these problems — is to try to reduce them to problems about integers.”
Computer Scientists tend to do the opposite. We will argue that there are many integer operations that are simply not meaningful for dominoes, or for days of the century. So we’ll often define new specialized types, like MyTime, because we can restrict, control, and specialize the operations that are possible. Object-oriented programming is particularly popular because it gives us a good way to bundle methods and specialized data into a new type.
Both approaches are powerful problem-solving techniques. Often it may help to try to think about the problem from both points of view — “What would happen if I tried to reduce everything to very few primitive types?”, versus “What would happen if this thing had its own specialized type?”
Some languages, including Python, make it possible to have different meanings for the same operator when applied to different types. For example, + in Python means quite different things for integers and for strings. This feature is called operator overloading.
It is especially useful when programmers can also overload the operators for their own user-defined types.
For example, to override the addition operator +, we can provide a method named __add__:
As usual, the first parameter is the object on which the method is invoked. The second parameter is conveniently named other to distinguish it from self. To add two MyTime objects, we create and return a new MyTime object that contains their sum.
Now, when we apply the + operator to MyTime objects, Python invokes the __add__ method that we have written. The expression currentTime + breadTime is equivalent to currentTime.__add__(breadTime), but obviously more elegant.
As an exercise, add a method __sub__(self, other) that overloads the subtraction operator, and try it out.