“It is an ex parrot. It has ceased to be.”
No, of course not. I’m talking about the other Python. Not the one with the silly walks, dead parrot and singing lumberjacks, but the one with the idiosyncratic approach to whitespace.
One of the APIs available for the Web21C SDK is Python, and there was recently some concern that we didn’t have enough expertise in the team to continue providing technical support for the Python SDK. A bunch of us have volunteered to start learning the language, and as a first step, Tim set us some homework:
Write a program that processes a list of numbers from 1 to 100. For each number, if the number is a multiple of 3, print “FIZZ”; if the number is a multiple of 5, print “BANG”; otherwise, print the number.
You are *NOT* allowed to use any *IF/ELSE* statements in your code. You can use the list-accessing ternary operator hack, but whilst I’ll accept your homework if you do, you’ll miss out on the prize (alcoholic), which goes to the most concise code (not including whitespace).
Now I have no idea what this mysterious ‘list-accessing ternary operator hack’ might be, but it sounds painful (as does missing out on an alcoholic prize), so it looks like I need to eschew ifs completely.
Since I’ve never written a line of Python in my life, I decided the easiest way to proceed would be to start off by figuring out enough syntax to solve the problem as defined by the first paragraph, then write a test which the simple code passes, then factor out the conditional logic. Normally I’d start with the test, but I think learning a new language is one of the cases where TDD isn’t really appropriate.
So here’s my first working code:
for n in range(1, 100): if n % 3 == 0: if n % 5 == 0: print 'FIZZBANG' else: print 'FIZZ' elif n % 5 == 0: print 'BANG' else: print n
This prints out what you would expect:
PyMate r8111 running Python 2.5.1 (/usr/bin/env python) >>> fizzbang.py 1 2 FIZZ 4 BANG FIZZ 7 8 FIZZ BANG 11 FIZZ 13 14 FIZZBANG 16 ...
OK, so far so good. The next step was to find out what the Pythonistas use for unit testing. As a card-carrying BDD and RSpec fan, I was initially interested to see that there’s a PySpec, but at first glance it doesn’t look that great (maybe it’s just that I’m not used to reading Python code). Then I came across the bundled DocTest, and while I’m not entirely convinced by its description on the Wikipedia BDD page as ‘BDD for Python’, it looked ideal for the task at hand.
Here’s the same code, but with the addition of a parameter to specify the number to count up to, and a DocTest test:
#!/usr/bin/env python """ Print numbers up to limit, replacing those divisible by 3 and/or 5 with FIZZ and/or BANG. >>> fizzbang(20) 1 2 FIZZ 4 BANG FIZZ 7 8 FIZZ BANG 11 FIZZ 13 14 FIZZBANG 16 17 FIZZ 19 BANG """ def fizzbang(limit): for n in range(1, limit + 1): if n % 3 == 0: if n % 5 == 0: print 'FIZZBANG' else: print 'FIZZ' elif n % 5 == 0: print 'BANG' else: print n def _test(): import doctest doctest.testmod() if __name__ == "__main__": _test()
OK, now to start thinking about the hard bit – getting rid of those ifs. As a first step, I got rid of the nested logic by appending the ‘FIZZ’ and/or ‘BANG’ to a temporary string, then either printing that string, or the original number if it was empty:
def fizzbang(limit): for n in range(1, limit + 1): out = '' if n % 3 == 0: out += 'FIZZ' if n % 5 == 0: out += 'BANG' print out or n
At this point, I hit on the idea of using arrays to select either an empty string or the appropriate word:
def fizzbang(limit): for n in range(0, limit): print ['', '', 'FIZZ'][n%3] + ['', '', '', '', 'BANG'][n%5] or n + 1
Now all that’s left is to remove the limit parameter so the code merely satisfies the original criteria:
for n in range(100): print ['', '', 'FIZZ'][n%3] + ['', '', '', '', 'BANG'][n%5] or n + 1
I expect there are other more concise (and no doubt more efficient) solutions – I look forward to seeing what the other ‘pupils’ come up with!