Good, Better, Best? : FizzBuzz
The Zen of Python states: “There should be one — and preferably only one — obvious way to do it.” In real life it is not always that easy. There are usually many different ways to do something.
Lets pretend we’ve got three levels of code:
- Good: it works.
- Better: it uses Python’s features well and follows the coding standards.
- Best?: Clean code, short functions, meaningful names, readable.
What I think is “best” changes all the time and may be different from you, which is why I added a question mark.
The problem
We will solve the popular ‘FizzBuzz‘ problem:
- Count from 1 to 100
- If the number is divisible by 3, print ‘Fizz’
- If by 5, print ‘Buzz’
- if by 3 and 5, print ‘FizzBuzz’
To make it a bit more interesting, we will do a more generalised version, which accepts a list of (divisor, word) tuples. For the classic FizzBuzz this would be: generalised_fizz_buzz(100, [(3, ‘Fizz’), (5, ‘Buzz’)]).
The Good version
def print_it(n, v):
i = 0
to_print = ''
while i < len(v):
word = v[i]
if n % word[0] == 0:
to_print = to_print + word[1]
i = i + 1
if len(to_print) > 0:
print(n, to_print)
def generalised_fizz_buzz(nums=100, v=((3, 'Fizz'), (5, 'Buzz'))):
number = 1
while number <= nums:
print_it(number, v)
number = number + 1
generalised_fizz_buzz(100, [(3, 'Fizz'), (5, 'Buzz')])
3 Fizz
5 Buzz
6 Fizz
9 Fizz
10 Buzz
12 Fizz
15 FizzBuzz
.. etc. ..
From ‘Good’ to ‘Better’
To improve this:
- Instead of “i = 0; while i < …; i = i + 1” we can use iteration: “for i in range(len(v))”.
- Some of the variable and function names are short and/or too vague. Rename ‘v’ to ‘vocabulary’, ‘nums’ to ‘last_number’, ‘n’ to ‘number’, ‘print_it’ to ‘print_fizz_buzz’, etc..
- Instead of word[0] and word[1] we can “unpack” each of the tuples in the vocabulary directly into the divisor and the output (e.g. 3 and ‘Fizz’).
- We can shorten “len(to_print) > 0” to “to_print” in an “if” statement. This does exactly the same and is more readable once you get used to it.
- Instead of “a = a + 1” we can use an assignment operator: “a += 1”.
The Better version
def print_fizz_buzz(number, vocabulary):
to_print = ''
for i in range(len(vocabulary)):
divisor, output = vocabulary[i]
if not number % divisor:
to_print += output
if to_print:
print(number, to_print)
def generalised_fizz_buzz(last_number=100, vocabulary=((3, 'Fizz'), (5, 'Buzz'))):
for number in range(1, last_number + 1):
print_fizz_buzz(number, vocabulary)
generalised_fizz_buzz(100, [(3, 'Fizz'), (5, 'Buzz')])
The output remains the same.
From ‘Better’ to ‘Best?’
To improve this:
- The ‘print_fizz_buzz’ function no longer prints. This makes it easier to test and lets us re-use it in other places. It also means we can use it in an assignment expression (see below). Because it has a slightly different function, it is now called ‘get_fizz_buzz’.
- ‘get_fizz_buzz’ uses a generator expression to create the parts (‘Fizz’ and/or ‘Buzz’) which are then combined using <empty_string>.join(). This could be a single line, but I decide to break it up to make it easier to read.
- The main functions uses an assignment expression (using “walrus operator” because := looks a bit like walrus teeth). This does two things: assign the result of calling get_fizz_buzz to the fizz_buzz variable, and it also returns the result as if the whole assignment is actually an expression. This means we can use it in the “if” statement.
The ‘Best?’ function
def get_fizz_buzz(number, vocabulary):
return ''.join(
word
for (divisor, word) in vocabulary
if not number % divisor
)
def generalised_fizz_buzz(last_number=100, vocabulary=((3, 'Fizz'), (5, 'Buzz'))):
for number in range(1, last_number + 1):
if fizz_buzz := get_fizz_buzz(number, vocabulary):
print(number, fizz_buzz)
generalised_fizz_buzz(100, [(3, 'Fizz'), (5, 'Buzz')])
The output is still the same.
If we call it with a different ‘vocabulary’, such as:
generalised_fizz_buzz(40, ((3, 'A'), (12, 'b'), (5, '.')))
We get:
3 A
5 .
6 A
9 A
10 .
12 Ab
15 A.
18 A
20 .
21 A
24 Ab
25 .
27 A
30 A.
33 A
35 .
36 Ab
39 A
40 .
What’s next?
What I think is “best” changes all the time and may be different from you, which is why I added a question mark. There is probably more I could do, but this is enough for now.
Note: This code does not need to be fast. I did not optimise it for performance.
Nor does it need to be secure and robust. No type hints or unit tests.