Sometimes ya gotta laugh. I just spent nearly an hour chasing one of those bugs that “can’t be happening” because “the code looks absolutely correct (as far as I can tell).” Of course, the bug, once you find it, was always hiding in plain view.
This time the fix involved adding just two characters, incidentally improving the program semantics, and very much reminded me of the old joke about the huge cargo ship that breaks down at sea…
Large ships carry entire maintenance crews with some very capable engineers and mechanics, so it’s rare when several days pass without anyone on board being able to fix the problem. Meanwhile, the ship sits dead in the water.
Finally the captain calls for an expert to be flown in by helicopter. When she arrives, she listens to the various explanations, nods her head, and spends a bit of time walking around the ship’s engine room looking at gauges.
Finally she opens her tool kit, takes out a large rubber mallet, and bangs on one particular pipe very hard for several blows. She indicates the crew should try the engines, and sure enough, they work just fine now.
Upon this, she presents the captain with a bill for $85,000.
The captain is taken aback by such a large bill and protests that all she did was look at gauges and bang on a pipe for a few seconds.
The expert famously explains, “The bill is for knowing where to bang.”
Different languages have their pitfalls, and as wonderful as Python is, it’s got a few things to watch out for.
A common vernacular in many languages allows a truth value for strings and numbers (at least integers, but often floats). The usual semantic is that an empty string (a zero-length string) is false, a string with length is true. And a numeric value of zero is false while all non-zero values are true.
Some languages also treat a test of an object variable as true if an object exists and false if it doesn’t. Essentially, it’s a test for NULL.
Python is such a language. A variable can have the value None, which is false. Any variable can have the value None, even integers and strings. And therein lies the rub.
Consider the following:
def __push_context (self, data_bag): self.ctx_idx = data_bag['#'] if '#' in data_bag else None self.ctx_bag = data_bag['@'] if '@' in data_bag else None def __pop_context (self, data_bag): if self.ctx_idx: data_bag['#'] = self.ctx_idx if self.ctx_bag: data_bag['@'] = self.ctx_bag
The intention is to preserve two values in data_bag if they were there in the first place. One of them (the ‘@’-indexed value) is an object, so things work as expected.
But the ‘#’-indexed value is a plain integer, and as the code was originally written, it could have the value zero. (Do you immediately see the problem? I sure didn’t.)
What was happening (and driving me crazy until I figured it out) was that when that value was zero, the __push_context() method did save it, but since zero tests as false, the __pop_context() method didn’t restore it.
Lesson learned. (Remembered is another story.)
At first I thought I’d have to do a messier test, explicitly checking for != None, but then I realized that the integer actually shouldn’t ever be zero in the first place.
What that integer represents is the iteration number of loop in a process that builds an output from a fixed input template merged with run time data. One feature is building output by iterating over the input data to make rows or columns (or both) in the output.
But while programmers appreciate counting from zero, most normal humans do not. (And I met a programmer once who really hated it.) From a user perspective, loops that number from one are easier to understand. (The first item is “1” not “0”!)
So the fix was…
while len(toks): tok = toks.pop(0) data_bag['#'] = 1+rx data_bag['@'] = sub_bag tok(fp, data_bag, toks) del data_bag['#'] del data_bag['@']
…to add the bit in red. That’s where I looked at things, thought about it, and then just banged on a pipe a few times.
Debugged for almost an hour. And then just added two characters and made the whole thing, not just work, but better.
Ain’t it nice when it goes that way!