Prof. Andrea Gallegati
Let's write functions that take programmer-defined objects as parameters and return them as results.
class Time:
"""Represents the time of day.
attributes: hour, minute, second
"""
this class records the time of day. Let's create a new Time
object to assign attributes for hours, minutes, and seconds:
time = Time()
time.hour = 11
time.minute = 59
time.minute = 30
... and a function to print it out
def print_time(time):
print('%.2d:%.2d:%.2d' % (time.hour, time.minute, time.minute))
The format sequence '%.2d'
prints an integer using at least two digits, including a leading zero if necessary.
The prototype and patch development plan, to tackle complex problems, just starts with a simple prototype and incrementally deals with the complications.
def add_time(t1, t2):
sum = Time()
sum.hour = t1.hour + t2.hour
sum.minute = t1.minute + t2.minute
sum.second = t1.second + t2.second
return sum
This simple prototype of add_time
Time
objectis a pure function: not modifying any object passed as argument and with no effect (e.g. printing value), but returning a value.
Let's pass a couple of Time
objects to test it:
To figure out when the movie will be done
start = Time()
start.hour = 9
start.minute = 45
start.second = 0
duration = Time()
duration.hour = 1
duration.minute = 35
duration.second = 0
done = add_time(start, duration)
print_time(done)
10:80:80
The result might not be what you were hoping for.
... not dealing with cases where seconds/minutes are more than sixty.
We have to “carry”
def add_time(t1, t2):
sum = Time()
sum.hour = t1.hour + t2.hour
sum.minute = t1.minute + t2.minute
sum.second = t1.second + t2.second
if sum.second >= 60:
sum.second -= 60
sum.minute += 1
if sum.minute >= 60:
sum.minute -= 60
sum.hour += 1
return sum
Although correct, we could dream of a shorter alternative.
It turns useful for a function to modify the objects it gets as parameters.
def increment(time, seconds):
time.second += seconds
if time.second >= 60:
time.second -= 60
time.minute += 1
if time.minute >= 60:
time.minute -= 60
time.hour += 1
this is a modifiers, as these changes are visible to the caller.
What happens if seconds is **much greater** than 60?
Not enough to "carry" once: just keep doing it until time.second
is less than 60
.
Thus, replacing if statements with** while statements** (not very efficient).
Anything done with modifiers can also be done with pure functions.
Some programming languages allows pure functions only.
Programs that use pure functions are faster to develop and less error-prone, but modifiers are convenient at times and usually more efficient!
Better to:
( functional programming style )
Following a prototype and patch development plan:
Really effective, without a deep understanding of the problem.
However, incremental corrections generates code:
Alternatively, follow a designed development plan:
For example, Time
object is a three-digit number in base 60 !
(see http://en.wikipedia.org/wiki/Sexagesimal)
... suggesting another approach:
convert Time
to integers, taking advantage of their simpler arithmetic!
def time_to_int(time):
minutes = time.hour * 60 + time.minute
seconds = minutes * 60 + time.second
return seconds
def int_to_time(seconds):
time = Time()
minutes, time.second = divmod(seconds, 60)
time.hour, time.minute = divmod(minutes, 60)
return time
and the way back, using divmod
to divide: this returns the quotient and remainder as a tuple.
This approach requires to:
before geting convinced the solution is correct. For example:
x = 120
time_to_int(int_to_time(x)) == x
True
Once convinced, let's rewrite add_time
.
def add_time(t1, t2):
seconds = time_to_int(t1) + time_to_int(t2)
return int_to_time(seconds)
We get a program that is:
( imagine subtracting two `Times` to find the duration in between)
Base conversion is more abstract.
Dealing with Time
objects is more intuitive, but:
Time
as base 60 numbers... ironically: making this problem harder (more general) it becomes easier (fewer special cases/possible errors).
Time
objects are well-formed if:
minute
and second
are between 0 and 60 (not included)hour
is positivehour
and minute
are integers valuessecond
can be a floatThese requirements are invariants because they should always be true.
... otherwise, something has gone wrong.
Write code (boolean functions) to check invariants helps to detect errors, for example:
def valid_time(time):
if time.hour < 0 or time.minute < 0 or time.second < 0:
return False
if time.minute >= 60 or time.second >= 60:
return False
return True
To validate arguments at the beginning of each function
def add_time(t1, t2):
if not valid_time(t1) or not valid_time(t2):
raise ValueError('invalid Time object in add_time')
seconds = time_to_int(t1) + time_to_int(t2)
return int_to_time(seconds)
Even using an assert
statement, to raise an exception:
def add_time(t1, t2):
assert valid_time(t1) and valid_time(t2)
seconds = time_to_int(t1) + time_to_int(t2)
return int_to_time(seconds)
(it distinguishes code that checks for errors)