A danger of default values for function arguments (in illustrated form)
Due to recent events, I've been working on a program to measure our disk IO latencies. Since it only needs timing accuracy in the millisecond range, I've been writing it in Python (which is more than fast enough to not add distortions to the IO timings). In the process of developing this code, I made a classic absent-minded mistake that shows a danger of default arguments.
The code needs to know the size of the range it will be doing IO on. In the beginning, it worked only on files and got the size from the size of the file, and the code looked something like:
def process(fname): size = sizeof(fname) [.....] def main(args): for a in args: process(a)
Then I discovered that I needed to make the code work on raw disks
too. Getting the size of a raw disk is much more complicated than
getting the size of a file and anyways, they're huge and I didn't want
to do that much testing. I decided that clearly the thing to do was give
process() an optional argument to specify the size to work on and
revised the code to look like this:
def process(fname, size = None): if size is None: size = sizeof(fname) [....] def main(args): size = None [set 'size' from a -s switch] for a in args: process(a)
I then spent an embarrassing amount of time trying to figure out what was wrong with my code such that the IO offsets weren't being computed right (there were complicating factors, evidently including insufficient coffee).
The problem (as you could probably see immediately) is that I'd
forgotten to update the code in
main() to actually pass the new
process(); I'd only added the code to optionally get it
from the command line. If I had not cleverly made
size an argument
with a default, I would have discovered this mistake immediately because
Python would have reported an argument count mismatch. But using
default argument values hides argument count errors so when I left out
an argument that was in practice not optional I didn't get an error,
just an oddly broken program.
(I was lucky that things broke in a clear, directly observable way.)
This is not the first time I have revised function arguments this way. When I add a new argument to a function I always have a temptation to give it a default and make it optional; there's a little voice in the back of my head that says 'this is the right way to keep existing code working'. Very often this is clearly wrong for the same reason as it was here, namely that I'm going to immediately revise all of the callers to explicitly pass in this new 'optional' argument. If I'm always passing an argument, giving it a default value too is simply inviting argument count errors. I really should know better by now.
(There are cases where default values for arguments are really useful, but this is not one of them.)