We’ve asked many of our residents at Hacker School what qualities all great programmers share. There’s very little agreement – clearly, there are a multitude of ways to be a great programmer, and you can think of a counter-example for almost every quality you can name. One of the rare non-controversial statements came first from Jessica McKellar, who identified systematic debugging as a key skill.
What does systematic debugging look like? At this point, I’m focused on two aspects: asking a clear question, and keeping track of my mental “stack”. I had a particularly fun and interesting bug yesterday that I think illustrates this nicely.
The problem at hand was a brainteaser from Jessica:
Using the official SOWPODS Scrabble dictionary, what letters, if any, never appear doubled? (By that I mean — “AA” does appear doubled because it is in “AARDVARK”, “BB” does appear doubled because it is in “BUBBLE” — are there any letters that never appear doubled in a word?)
I came up with a solution, but it was kind of slow, so I was trying to find a faster one. Also, my original solution used a regular expression to match a doubled letter in a word, but it would only find the first pair (e.g. in BOOKKEEPER only the ‘OO’ would be caught).
I decided to try taking the dictionary as a single string, rather than a list of words, and consuming it one letter-pair at a time, as follows.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
If you’re thinking, “Hmm, that looks like it might be slow,” congratulations! It is indeed quite slow. I had two guesses for why it was slow. (It wasn’t the print statement.) First, maybe I was copying the string over and over, and that was slowing down the function. Second, maybe I misunderstodd the regular expression – for example, maybe it was taking a longer time to operate on a longer string. I decided to investigate the second possibility first, since regular expressions were more of a mystery to me.
At this point in the problem solving, my mental stack looks something like this:
--------------------------- | regex taking longer on a | (newest) | longer-length string? | --------------------------- | reg exp? or string copy? | --------------------------- | function is so slow, why? | --------------------------- | find a faster solution | --------------------------- | solve wordplay problem | (oldest) ---------------------------
The next step was to test my theory that my regular expression should not take longer on a longer string. Importantly, the regex had no “greedy” elements – it should match and return the first time it encounters two identical characters, regardless of how long the string is after those matching characters. Here was the experiment I wrote:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
In both cases, I thought, the regex should check “he”, fail, check “ee”, succeed, and return. To my great surprise, the long string test took 70-80x as long to run as the short string test!
1 2 3 |
|
Clearly, I was missing something about regular expressions. I googled around and learned some details about backreferences, the (\1)
in my code (which makes my regular expression not actually “regular”), but that wasn’t very illuminating. (You can picture “something about Finite State Automata” being pushed onto and quickly popped back off the stack.)
My next step was to run timing experiments with a variety of regular expressions to see if I could find an element that made the difference. At this point yesterday, I was still trying to pass strings to timeit
, which was getting unwieldy. (Stay tuned to learn two better ways to do timing.) I switched to a simpler timing script, just taking time.time()
at the start and end of the algorithm.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
|
Hopefully the differences would start to become clear, and I could easily extend this to add more variations on the regular expression.
It printed:
1 2 3 4 |
|
Uh-oh, wait a minute, I thought this was identical code. Where did the 70x time difference go?
I couldn’t see any obvious errors, so I went back to the timeit
code. Could I reproduce these results while still using the timeit
module?
Let’s look back at the problem stack.
--------------------------- | what's up with timeit? | --------------------------- | is this code different? | --------------------------- | timing programs give | | different results, uh-oh | --------------------------- | regex taking longer on a | | longer-length string | --------------------------- | reg exp? or string copy? | --------------------------- | function is so slow, why? | --------------------------- | find a faster solution | --------------------------- | solve wordplay problem | (oldest) ---------------------------
At this point Brandon Rhodes joined me in pair-debugging, and we made a couple of discoveries. The most interesting of these was that two different ways of calling timeit generated wildly different results.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
|
1 2 3 4 5 6 7 8 9 |
|
So now we know something is up with passing strings to timeit.
--------------------------- | timeit string version - ? | --------------------------- | what's up with timeit? | --------------------------- | is this code different? | --------------------------- ....
Ok, we’ve narrowed it down this far – it’s time to take a look at the source. timeit
is a module written in python (yay!), and the relevant parts look pretty straightforward:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
|
Let’s trace the part that’s giving us the long execution string. We call the module-level function timeit.timeit
, which creates a Timer object and returns the result of calling its timeit
method. No surprise that we got identical results between our method #1 and method #3 above.
The Timer
object checks to see if you’ve passed it strings or callables as its stmt
and setup
parameters. In our case both are strings. The init
method splices the strings into the source code here:
1
|
|
We can isolate this in the REPL if we want to be extra-sure of how it works:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
Got it – simple, legal python code is generated. So where is our regular expression getting screwed up? Let’s take a look at the code generated in our actual test by inserting a print statement into the timeit
module. (First we’ll copy the module over to our current working directory, to avoid modifying our actual standard library.)
1 2 |
|
And now calling test_with_timeit.py
again, it prints:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
This looks mostly reasonable … but hey, wait, what happened to the regular expression?
1
|
|
That’s not right! Changing print src
to print repr(src)
gives us a better idea what’s up – the relevant line is now:
1
|
|
And looking back at the SETUP
string … uh-oh, this isn’t a raw string, and it contains an escape character ((\1)
). We can tell we’re in trouble immediately in the REPL:
1 2 3 4 |
|
Why does this happen? We can consult the docs on string literals to find out. Unless you’re working with a raw string (one prefixed with r
or R
), escape characters are interpreted the same way they are in C. Our text, a backslash followed by a number, matches the pattern for an octal digit: a backslash followed by 1-3 integers. \ooo
indicates a character with the octal value ooo
. And that’s exactly what we’ve done:
1 2 3 4 |
|
\x01
is a legal (though unprintable) character, and it doesn’t appear in our long string to test the regular expression. So of course the longer one took a longer time – it had to traverse the entire string looking for \x01
, which it never found.
The fix is quite easy – one character, in fact. Make the SETUP
string raw:
1 2 3 4 5 6 |
|
… and the problem is solved. So we can pop a bunch of frame off our problem stack and go investigate the string copying, which is in fact the issue.
--------------------------- | reg exp? or string copy? | --------------------------- | function is so slow, why? | --------------------------- | find a faster solution | --------------------------- | solve wordplay problem | (oldest) ---------------------------
Lessons learned
Interesting bugs are a ton of fun! Now for a bit of introspection: what could have gone better?
Most obviously, I could have spent less time on the regular-expression branch of why the function was slow, especially as the stack grew. Whether I should have done this depends on your perspective: at Hacker School, I’m almost 100% learning motivated and about 0% get-it-done motivated, so my time to solve the bug is virtually unbounded.
I was slightly overconfident about understanding how the
timeit
code splicing happened. The code looked so simple that I didn’t bother to check my understanding at first.Passing
timeit.timeit
strings is an unwieldy way of doing timing. I should have been using either the callable option, or just timing withtime python test_file.py
from the command line. Both of these would have been easier to work with and would have sidestepped the bug.My mental model of regular expressions was largely correct. That’s satisfying.
Strings! Ouch. This may not seem like much of a lesson, but it’s useful to add to my mental list of Places That Bugs Lurk.