In [1]:
class MyTime:
def __init__(self, hrs=0, mins=0, secs=0):
""" Create a MyTime object initialized to hrs, mins, secs """
self.hours = hrs
self.minutes = mins
self.seconds = secs
def __str__(self):
return "{h}:{m}:{s}".format(h=self.hours, m=self.minutes, s=self.seconds)
In [2]:
tim1 = MyTime(11, 59, 30)
tim2 = MyTime()
print(tim1)
print(tim2)
In [ ]:
In [3]:
def add_time(t1, t2):
h = t1.hours + t2.hours
m = t1.minutes + t2.minutes
s = t1.seconds + t2.seconds
sum_t = MyTime(h, m, s)
return sum_t
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.
>>> current_time = MyTime(9, 14, 30)
>>> bread_time = MyTime(3, 35, 0)
>>> done_time = add_time(current_time, bread_time) # When does this break?
>>> print(done_time)
12:49:30
In [4]:
current_time = MyTime(1, 10, 0)
bread_time = MyTime(0, 60, 0)
done_time = add_time(current_time, bread_time)
print(done_time)
In [6]:
def add_time(t1, t2): # are we good now?
h = t1.hours + t2.hours
m = t1.minutes + t2.minutes
s = t1.seconds + t2.seconds
if s >= 60:
s -= 60
m += 1
if m >= 60:
m -= 60
h += 1
sum_t = MyTime(h, m, s)
return sum_t
In [8]:
current_time = MyTime(1, 10, 0)
bread_time = MyTime(0, 120, 0)
done_time = add_time(current_time, bread_time)
print(done_time)
In [ ]:
def increment(t, secs): # Is this good?
t.seconds += secs
if t.seconds >= 60:
t.seconds -= 60
t.minutes += 1
if t.minutes >= 60:
t.minutes -= 60
t.hours += 1
In [ ]:
def increment(t, seconds): # How about now?
t.seconds += seconds
while t.seconds >= 60:
t.seconds -= 60
t.minutes += 1
while t.minutes >= 60:
t.minutes -= 60
t.hours += 1
In [ ]:
class MyTime:
# Previous method definitions here...
def increment(self, seconds):
self.seconds += seconds
while self.seconds >= 60:
self.seconds -= 60
self.minutes += 1
while self.minutes >= 60:
self.minutes -= 60
self.hours += 1
In [ ]:
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.
In [ ]:
class MyTime:
# ...
def to_seconds(self):
""" Return the number of seconds represented
by this instance
"""
return self.hours * 3600 + self.minutes * 60 + self.seconds
In [ ]:
## ---- Core time conversion logic ----- #
hrs = tsecs // 3600
leftoversecs = tsecs % 3600
mins = leftoversecs // 60
secs = leftoversecs % 60
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.)
In [ ]:
class MyTime:
# ...
def __init__(self, hrs=0, mins=0, secs=0):
""" Create a new MyTime object initialized to hrs, mins, secs.
The values of mins and secs may be outside the range 0-59,
but the resulting MyTime object will be normalized.
"""
# Calculate total seconds to represent
totalsecs = hrs*3600 + mins*60 + secs
self.hours = totalsecs // 3600 # Split in h, m, s
leftoversecs = totalsecs % 3600
self.minutes = leftoversecs // 60
self.seconds = leftoversecs % 60
In [ ]:
def add_time(t1, t2):
secs = t1.to_seconds() + t2.to_seconds()
return MyTime(0, 0, secs)
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?”
In [ ]:
class MyTime:
# Previous method definitions here...
def after(self, time2):
""" Return True if I am strictly greater than time2 """
if self.hours > time2.hours:
return True
if self.hours < time2.hours:
return False
if self.minutes > time2.minutes:
return True
if self.minutes < time2.minutes:
return False
if self.seconds > time2.seconds:
return True
return False
We invoke this method on one object and pass the other as an argument:
In [ ]:
if current_time.after(done_time):
print("The bread will be done before it starts!")
The logic of the if statements deserve special attention here. Lines 11-18 will only be reached if the two hour fields are the same. Similarly, the test at line 16 is only executed if both times have the same hours and the same minutes.
Could we make this easier? Yes!
In [ ]:
class MyTime:
# Previous method definitions here...
def after(self, time2):
""" Return True if I am strictly greater than time2 """
return self.to_seconds() > time2.to_seconds()
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__:
In [40]:
class MyTime:
"""This makes the clock.
>>> timer = MyTime(5, 4, 3) # Makes a timer with time 5 hr ...
"""
def __init__(self, hrs=0, mins=0, secs=0):
""" Create a new MyTime object initialized to hrs, mins, secs.
The values of mins and secs may be outside the range 0-59,
but the resulting MyTime object will be normalized.
"""
# Calculate total seconds to represent
totalsecs = hrs*3600 + mins*60 + secs
self.hours = totalsecs // 3600 # Split in h, m, s
leftoversecs = totalsecs % 3600
self.minutes = leftoversecs // 60
self.seconds = leftoversecs % 60
def __str__(self):
return "{h}:{m}:{s}".format(h=self.hours, m=self.minutes, s=self.seconds)
def __add__(self, other):
return MyTime(0, 0, self.to_seconds() + other.to_seconds())
def to_seconds(self):
""" Return the number of seconds represented
by this instance
"""
return self.hours * 3600 + self.minutes * 60 + self.seconds
In [41]:
t1 = MyTime(0, 0, 42000)
t2 = MyTime(3, 50, 30)
t3 = t1 + t2
print(t3)
In [39]:
help(MyTime())
>>> t1 = MyTime(1, 15, 42)
>>> t2 = MyTime(3, 50, 30)
>>> t3 = t1 + t2
>>> print(t3)
05:06:12
In [ ]:
def front_and_back(front):
import copy
back = copy.copy(front)
back.reverse()
print(str(front) + str(back))
To determine whether a function can be applied to a new type, we apply Python’s fundamental rule of polymorphism, called the duck typing rule:
If all of the operations inside the function can be applied to the type, the function can be applied to the type. The operations in the front_and_back function include copy, reverse, and print.
Write a Boolean function between that takes two MyTime objects, t1 and t2, as arguments, and returns True if the invoking object falls between the two times. Assume t1 <= t2, and make the test closed at the lower bound and open at the upper bound, i.e. return True if t1 <= obj < t2.
Turn the above function into a method in the MyTime class.
Overload the necessary operator(s) (see this and this) so that instead of having to write
if t1.after(t2): ...
we can use the more convenient
if t1 > t2: ...
Rewrite increment as a method that uses our “Aha” insight.
Create some test cases for the increment method. Consider specifically the case where the number of seconds to add to the time is negative. Fix up increment so that it handles this case if it does not do so already. (You may assume that you will never subtract more seconds than are in the time object.)
Can physical time be negative, or must time always move in the forward direction? Some serious physicists think this is not such a dumb question. Does your model handle reverse time? What does that even mean? Think deeply about time for the next thirty seconds.
test(recursive_min([2, 9, [1, 13], 8, 6]) == 1)
test(recursive_min([2, [[100, 1], 90], [10, 13], 8, 6]) == 1)
test(recursive_min([2, [[13, -7], 90], [1, 100], 8, 6]) == -7)
test(recursive_min([[[-13, 7], 90], 2, [1, 100], 8, 6]) == -13)
test(count(2, []), 0)
test(count(2, [2, 9, [2, 1, 13, 2], 8, [2, 6]]) == 4)
test(count(7, [[9, [7, 1, 13, 2], 8], [7, 6]]) == 2)
test(count(15, [[9, [7, 1, 13, 2], 8], [2, 6]]) == 0)
test(count(5, [[5, [5, [1, 5], 5], 5], [5, 6]]) == 6)
test(count("a",
[["this",["a",["thing","a"],"a"],"is"], ["a","easy"]]) == 4)
test(flatten([2,9,[2,1,13,2],8,[2,6]]) == [2,9,2,1,13,2,8,2,6])
test(flatten([[9,[7,1,13,2],8],[7,6]]) == [9,7,1,13,2,8,7,6])
test(flatten([[9,[7,1,13,2],8],[2,6]]) == [9,7,1,13,2,8,2,6])
test(flatten([["this",["a",["thing"],"a"],"is"],["a","easy"]]) ==
["this","a","thing","a","is","a","easy"])
test(flatten([]) == [])
In [ ]: