Chapter 16 · pp. 177–182
Classes and Functions
This chapter shows how to write functions that take programmer-defined objects as arguments and return them as results. Using a Time class that stores the time of day, it compares two kinds of functions (pure functions and modifiers) and two ways of planning a program (prototype and patch, and designed development). It ends with a way to catch errors early by checking conditions that must always hold.
A Time class and its attributes
A programmer-defined class lets you bundle related values into one object. A class named Time can describe a time of day with three attributes: hour, minute and second. You create an instance by calling the class, then give each attribute a value (p. 177).
time = Time()
time.hour = 11
time.minute = 59
time.second = 30
Now time holds 11:59:30. An object diagram draws this as a box labeled Time, with each attribute name pointing to its value (11, 59 and 30). Seeing the diagram helps you track which variable refers to which object (pp. 177–178).
Pure functions
A pure function does not change any object it receives. It also has no side effects, such as printing something or asking the user for input. All it does is return a value (p. 178).
The first version of add_time is pure. It makes a brand new Time object, sets its attributes to t1.hour + t2.hour, and so on, and returns it. The objects t1 and t2 are unchanged afterward. Most pure functions are fruitful, meaning they return a value (pp. 178, 182).
This first version has a bug. Say a movie starts at 9:45:00 and runs 1 hour 35 minutes. The function gives 10:80:00, because it never carries when minutes pass 59 (p. 178). The fix is to carry:
- If seconds are 60 or more, subtract 60 from seconds and add 1 to minutes.
- If minutes are 60 or more, subtract 60 from minutes and add 1 to hours.
The fixed function works, but it is already getting long (pp. 178–179).
Modifiers
A modifier is a function that changes the objects it receives, so the caller sees the change. For example, increment(time, seconds) adds seconds straight to time.second, then carries into minute and hour. After the call, the original object holds the new time. Most modifiers are void, meaning they return None (pp. 179, 182).
The rough draft of increment carries only once. If seconds is very large (say 500), one carry is not enough, because the seconds would still be over 59. Replacing each if with a while would make it correct but slow. A loop-free version needs a better idea, which comes later in the chapter (p. 179).
Pure functions versus modifiers
Anything a modifier does, a pure function can do by building and returning a new object. A pure version of increment would create a new Time and leave its parameter alone (pp. 179–180).
Programs built from pure functions tend to be quicker to write and have fewer errors. Modifiers can be more convenient and efficient, and purely functional programs are often less efficient. The advice is to use pure functions whenever reasonable and choose a modifier only when it has a clear advantage. This is called a functional programming style (pp. 179–180).
Prototype and patch
In prototype and patch, you write a simple first version that does the basic calculation. Then you test it and fix the errors one at a time. Our first add_time was the prototype, and the carrying code was the patch (pp. 178–180).
This works well when you do not yet understand the problem deeply. Its weaknesses are that patching many special cases makes code long and complicated, and it is hard to know whether you have found every error (p. 180).
Designed development
Designed development starts with an insight about the problem. Here the insight is that a Time is a three-digit number in base 60. The second attribute is the "ones column," minute is the "sixties column," and hour is the column worth 3600. Our carrying code was just base-60 addition (p. 180).
So we can convert to ordinary integers, add them, and convert back (pp. 180–181):
time_to_int computes (hour×60+minute)×60+second.
int_to_time uses divmod(seconds, 60) to get minutes and seconds. Then it uses divmod(minutes, 60) to get hours and the leftover minutes.
add_time becomes int_to_time(time_to_int(t1) + time_to_int(t2)).
Example: 9:45:00 is 35,100 seconds and 1:35:00 is 5,700 seconds. The sum is 40,800 seconds, which converts to 11:20:00.
The new add_time is shorter and easier to check. Adding a feature is also easier. To find the difference between two times, you just subtract the integers instead of writing "borrowing" code. The same conversion also gives a loop-free increment (p. 181).
Consistency checks
How do you know the two conversion functions are right? A consistency check tests that converting there and back gives the original value:
time_to_int(int_to_time(x))=x
If the functions are true inverses, this holds for every valid x. Trying it on many values builds confidence. For example, x=3661 becomes 1:01:01 and converts back to 3661 (p. 180).
Abstraction can make things easier
A common belief is that a more general or abstract version of a problem must be harder. Converting times to base-10 integers does feel more abstract than working with hours and minutes directly. But it removes special cases like carrying and borrowing. The result is shorter, easier to read and debug, and more reliable. Sometimes making a problem more general makes it easier, because there are fewer chances for mistakes (p. 181).
Invariants and debugging
An invariant is a condition that should always be true. If it is false, something has gone wrong. For a well-formed Time, hour, minute and second must not be negative, and minute and second must be less than 60 (p. 181).
You can check this in several ways (pp. 181–182):
- Write a function like
valid_time that returns False if any rule is broken.
- At the start of
add_time, test the inputs and use raise ValueError(...) if they are invalid.
- Use
assert valid_time(t1) and valid_time(t2). If the condition fails, Python raises an exception.
An assert is handy because it keeps error-checking code separate from the code that handles normal cases (p. 181).
Check yourself
- What makes a function pure? Give two things a pure function does not do.
- Why did the first
add_time print 10:80:00 for 9:45:00 plus 1:35:00?
- Convert the time 2:03:05 to seconds using
time_to_int's formula.
- Why is it useful to treat a Time as a base-60 number?
- What is an invariant, and how does
assert help you check one?