There are two common approaches for representing or storing a time value in an object. One approach stores the three components—hours, minutes, and seconds—as three separate fields. To distinguish between AM and PM, either the hours are stored in 24-hour format or a Boolean flag is stored in a fourth field. With this representation, it is easy to access the individual components, but it's more difficult to compare two times or to compute the elapsed number of seconds between two times.
The second approach stores the time as a positive integer value representing the number of seconds that have elapsed since midnight. For example, the time 7:38:05 AM would be represented as 27,485 seconds, the number of seconds that have elapsed between midnight and 7:38:05 AM. Given the time in elapsed seconds, we can compute any of the three time components and simply subtract the two integer values to determine which occurs first or how many seconds separate the two times. We are going to use the latter approach as it is very common for storing times in computer applications and provides for an easy implementation.
Constructing the Time
We begin our discussion of the implementation with the class constructor. Remember, the purpose of a constructor is to define and initialize the instance variables necessary to store the data for an instance of the class. In this case, our constructor must define and initialize a single instance variable that stores the number of seconds that have elapsed since midnight. Here, we use the name numSeconds
:
class Time :
def __init__(self, hours, minutes, seconds) :
self._numSeconds = hours * 3600 + minutes * 60 + seconds
the value for which is computed from the three time components using the formula:
elapsed seconds = (hours x seconds per hour) +
(minutes x seconds per minute) + seconds
NOTE
|
|
Private Attributes.
Note the use of the underscore in the name of the _numSeconds instance variable. This indicates that the variable is suppose to be private to the class and should not be accessed by code outside of the class definition. See the Special Topic below on "Protected Attributes and Helper Methods" for more information related to the use of protected attributes and methods.
|
Figure 1.6.1 illustrates the abstract and physical views of our Time ADT. The definition of the ADT provides an abstract view of the new data type, while the implementation provides the real or physical view.
Figure 1.6.1: The abstract and physical views of the Time ADT.
Protected Attributes and Helper Methods |
The instance variables contained in an object are usually hidden from the client or user code that is outside the class. Only the methods defined by the class for use with the object should directly access the instance variables. This prevents the accidental corruption of the data that can occur when directly accessed by code outside the class.
Sometimes we may also need to protect methods from outside use such as with a helper method . Helper methods are like any other method, but they are commonly used in the implementation of a class to allow for the subdivision of a larger method into smaller parts or to reduce code repetition by defining a single method that can be called from within other methods as needed.
While most object-oriented languages provide a mechanism to hide or protect the instance variables from outside access, Python does not. Instead, the designer of a class in Python is supposed to indicate what instance variables and methods are suppose to be protected. It's then the responsibility of the user of the class not to violate this protection. By convention, we use identifier names that begin with a single underscore to flag those instance variables and methods that should be protected and trust that the user of the class will not attempt a direct access to those members.
The definition for the Time class in the mytime.py program listing illustrates the use of the single underscore to protect the numSeconds instance variable. You will note that we directly accessed the instance variables of the otherTime object from within the elapsedTime method and several of the operator methods. The reason we could do this and not violate the protection is that otherTime is an instance of the same class as the method we were implementing. When using an instance of a class from outside its implementation, however, we must use the methods provided by the class. Otherwise, we would violate the protection that the interface is attempting to impose.
|
The Time Components
To access the individual time components, each component must be computed from the number of seconds that have elapsed since midnight. The hours component represents the number of full hours that have elapsed since midnight. Since each hour is comprised of 3600 seconds, we divide the elapsed seconds by 3600 using integer division to determine the hour:
def hours(self) :
return self._numSeconds // 3600
The minutes component is computed from the number of elapsed seconds remaining after determining the number of seconds that comprise full hours. The number of remaining elapsed seconds can be determined using the integer remainder operator. After computing that value, we divide by 60, the number of seconds per minute:
def minutes(self) :
return (self._numSeconds % 3600) // 60
The seconds component of the time is the number of elapsed seconds remaining that are not part of the hour and minute components.
def seconds(self) :
return (self._numSeconds % 3600) % 60
To determine if the time is AM or PM, we need to examine the time and compare it to the number of elapsed seconds between midnight and 12 noon.
def isAM(self) :
return self._numSeconds < (12 * 3600)
Elapsed Time
To compute the elapsed time between two times is simply a matter of subtracting the elapsed seconds used to represent each time. %In Python code, These values are obtained using the instance variables from the two objects referenced by self
and otherTime
:
def elapsedTime(self, otherTime) :
return abs(self._numSeconds - otherTime.numSeconds)
Consider the following code segment:
timeA = Time(7, 38, 5) # 7:38:05 AM
timeB = Time(14, 0, 0) # 2:00:00 PM
diff = timeA.elapsedTime(timeB)
print("The elapsed number of seconds =", diff)
which creates two Time
objects, timeA
and timeB
(see
Figure 1.6.2), and computes the elapsed time between the two. The
resulting output
The elapsed number of seconds = 22915
is produced by subtracting 50400 (the number of elapsed seconds representing 2 PM) from 27485 (the number of elapsed seconds representing 7:38:05 AM) and returning the absolute value.
Figure 1.6.2: Two Time
objects representing 7:38:05 AM (left) and 2:00:00 PM (right).
Comparable Objects
We can logically compare two Time
instances to determine their numerical order. When using the number of seconds that have elapsed since midnight to represent a time, the comparison is as simple as comparing the two integer values and returning the appropriate Boolean value based on the result of that comparison. The comparable operation defined for the ADT is implemented using Python's special logical comparison operators (see the Special Topic "Overloading Operators" below):
def __eq__(self, otherTime) :
return self._numSeconds == otherTime._numSeconds
def __lt__(self, otherTime) :
return self._numSeconds < otherTime._numSeconds
def __le__(self, otherTime) :
return self._numSeconds <= otherTime._numSeconds
By implementing the methods for the logical comparison operators, instances of
the class become comparable objects. That is, the objects can be compared against each other to produce a logical ordering.
PROGRAMMING TIP
|
|
Python allows classes to define or overload various operators that can be used more naturally in a program without having to call a method by name. The special methods defined by Python are indicated with names that begin and end with two underscores. Do not confuse this notation with that used for protected attributes and methods, which are denoted with names beginning with a single underscore.
The special methods should not be called directly by name. Instead use the corresponding operator or function and let Python call the method for you. See the Special Topic "Protected Attributes and Helper Methods" at the end of the page for more information on defining and using Python's special operator methods.
|
You will notice that we implemented only three of the logical comparison operators. The reason for this is that starting with Python version 3, Python will automatically swap the operands and call the appropriate reflective method when necessary. For example, if we use the expression timeA > timeB
with Time
objects in a program,
if timeA > timeB :
print("timeA occurs after timeB")
Python will automatically swap the operands and call timeB < timeA
instead since the lt method is defined but not gt. It will do the same for timeA >= timeB
and timeA <= timeB
. When testing for equality, Python will automatically invert the result when only one of the equality operators (==
or !=
) is defined. Thus, we need only define one operator from each of the following pairs to achieve the full range of logical comparisons: <
or >
, <=
or >=
, and ==
or !=
.
|
In Python, we can define and implement methods that will be called automatically when a standard operator (+ , * , == , < ) is applied to an instance of a class. This allows for a more natural use of the objects instead of having to call specific methods by name. For example, to test whether two times are equal, we could define and implement a method isEqual and use it as follows:
<code>
if timeA.isEqual(timeB) :
print("The times are equal.")
</code>
Of course, we would prefer to use the == operator, which would provide cleaner and more natural code. This can be achieved by defining the special method eq :
<code>
def __eq__(self, otherTime) :
return self._numSeconds == otherTime._numSeconds
</code>
This method is called automatically when we compare two Time objects using the == operator:
<code>
if timeA == timeB :
print("The times are equal.")
</code>
If we need to check for a specific time, we can also use the equality operator by first creating an unnamed Time object:
<code>
if timeA == Time(12, 0, 0) :
print("The time is 12 noon.")
</code>
Some special methods are called when an instance of the class is passed to a built-in function. For example, suppose you attempt to convert a Time object to an integer value using the int function:
<code>
numSecs = int(timeA)
</code>
Then the int special method is called. We could implement that method as part of our Time class and have it return the number of seconds that have elapsed since midnight:
<code>
def __int__(self) :
return self._numSeconds
</code>
Similarly, when an object is printed or converted to a string using the str function, Python will automatically call the repr special method on the object. The method is suppose to return a meaningful string representation of the object's value or state. If this method is not defined for a class, and you attempt to print an instance of the class or convert it to a string:
<code>
myobject = My Class()
print(myobject)
</code>
then Python automatically builds a string that indicates the type of object and it's address:
<MyClass object at 0x7f396e4f7be0>
Special methods, which can be defined for any of Python's operators, should never be called directly by name. Instead, you are suppose to use the corresponding operator or function and let Python call the method for you. See Chapter Notation for a list of the special methods that can be overloaded in a Python class and the corresponding operator or function used to invoke that method.
|
String Representations
The toString
operation defined by the ADT is also implemented using a special operator method. This repr special method is supposed to build and return a meaningful string representation of the object's value. The contents of the string depends on the specific ADT. For the Time ADT, it should return a string representing the time in the 24-hour format "hh:mm:ss"
.
Since the time is stored as the number of seconds that have elapsed since midnight, we must call the appropriate time component method (hours
, minutes
, and seconds
) on the object itself to obtain the three time components. These values can then be used to create and return the string. The implementation of the repr special method is provided below:
def __repr__(self) :
hrs = self.hours()
mins = self.minutes()
secs = self.seconds()
return "%d:%02d:%02d" % (hrs, mins, secs)
A partial implementation of the Time
class is provided below. The implementation of the remaining methods is left as an exercise. As a reminder, the ADT operations specified in the definition using italicized text are implemented using Python's special methods and not using the name given in the ADT definition. Thus, for example, there is no method named toString in the Time
class. Instead that operation is implemented using the repr special method. Likewise, there is no comparable method. That ADT operation is implemented by the three special logical operators: eq, lt, and le.
Program Listing
# Implementation of the Time ADT using the number of seconds that have
# elapsed since midnight.
class Time :
# Creates a new Time instance from the given time components.
def __init__(self, hours, minutes, seconds) :
self._numSeconds = hours * 3600 + minutes * 60 + seconds
# Returns the hour part of the time (0 - 23)
def hours(self) :
return self._numSeconds // 3600
# Returns the minutes part of the time (0 - 59)
def minutes(self) :
return (self._numSeconds % 3600) // 60
# Returns the seconds part of the time (0 - 59)
def seconds(self) :
return (self._numSeconds % 3600) % 60
# Returns True if this time is ante meridiem (AM) and False otherwise.
def isAM(self) :
return self._numSeconds < (12 * 3600)
# Returns the number of seconds between this time and the otherTime.
def elapsedTime(self, otherTime) :
return abs(self._numSeconds - otherTime._numSeconds)
# Logically compares this time with the otherTime.
def __eq__(self, otherTime) :
return self._numSeconds == otherTime._numSeconds
def __lt__(self, otherTime) :
return self._numSeconds < otherTime._numSeconds
def __le__(self, otherTime) :
return self._numSeconds <= otherTime._numSeconds
# Returns a string representation of the time as hh:mm:ss
def __repr__(self) :
hrs = self.hours()
mins = self.minutes()
secs = self.seconds()
return "%d:%02d:%02d" % (hrs, mins, secs)
# --- The remaining methods go here ---
|