So, I was catching up on some of the talks from PyCon 2020. First, I watched Steven Lott - Type Hints: Putting more Buzz in your Fizz and then Elizaveta Shashkova - The Hidden Power of the Python Runtime. I think both talks were really interesting, however, together they inspired me to write this abomination of Fizz buzz implementation. If you haven't heard about Fizz buzz, see this Wikipedia page.
So from Elizavetas talk, I learned a bit about how Python stack frames are easily accessible at runtime. Most importantly for this post, when you capture an exception using the
except-keyword you can get the current stack frame from the traceback in the exception.
try: raise ValueError() except ValueError as e: tb = e.__traceback__ current_stack_frame = tb.tb_frame
After experimenting a bit I also found that you can use
tb.tb_next, to get the next traceback object in the chain. So, if you iterate until there is no tb_next then you have reached the bottom of the trace. This is where the error was raised.
Now let's get to the actual implementation. First, we will define three functions, fizz, buzz and fizzbuzz:
def fizzbuzz(val): if val % 3 == 0 and val % 5 == 0: raise ValueError() fizz(val) def fizz(val): if val % 3 == 0: raise ValueError() buzz(val) def buzz(val): if val % 5 == 0: raise ValueError()
fizzbuzz is the only function we will call from another part of the code. Therefore, we can think of that as our entry point to this call stack. In fizzbuzz we do our first check, to see if the value is both divisible by 5 and 3 in which case we should print fizzbuzz. We will get to how we print the values in just a little bit. However, if the value didn't fulfill the condition we call fizz. Here again, the fizz-condition is checked, and if the condition fails it calls buzz which tests the buzz-condition.
Now let's see how we write this to the console:
def exception_driven_fizzbuzz(val): try: fizzbuzz(val) print(val) except ValueError as e: tb = e.__traceback__ while tb.tb_next: tb = tb.tb_next print(tb.tb_frame.f_code.co_name)
Given some value, we call fizzbuzz. This triggers the call stack we just looked at. The idea is that the function which raises the exception should have its name written to the console. This is what we do in the except-block of this function. If none of the functions in the call stack raised an error the print-function after the fizzbuzz call will run, writing just the number to the console. As no error then has been raised the except-block isn't evaluated. However, if one the functions do raise an error we will iterate until we reach the last traceback. This traceback corresponds to the function which raised the error. Therefore, we extract the stack frame from this traceback and access it's code-object (
f_code) which contains a name,
f_name is the name of the function or module of the code object. In this case this will be either: fizz; buzz or fizzbuzz, depending on which function raised the exception.
Let's take an example to see how this is evaluated. We call the function with
val=3. This doesn't trigger an error in fizzbuzz, but it does trigger one in fizz. The program goes into the except-block, without evaluating the print in the try-block. The first value of
tb is the traceback in this function
exception_driven_fizzbuzz. We take one lap in the loop, tb now correspond to
fizzbuzz. Then we take a final lap in the loop and tb is now
fizz. From tb we then extract the name fizz and write it to the console. If, we do the same for
val=2, this will trigger no error in our call stack. Therefore, just the try-block will be executed, but this time it reaches the print-function.
Now we just need to slap a loop around this and our fizzbuzz is done:
for val in range(1, 16): exception_driven_fizzbuzz(val)
1 2 fizz 4 buzz fizz 7 8 fizz buzz 11 fizz 13 14 fizzbuzz
I think this is a fun way to challenge yourself and learn new things about the language. However, I wouldn't do this on my next job interview if I were you.