Hang onto your socks programmers, we’re about to dive deep. What are we up to here today? Well, we’re going to look into switch statement alternatives in Python (if you don’t know what a switch statement is don’t worry we’ll cover that bit), how you might use that in a practical real-world situation, and why that’s even an idea worth considering. With that in mind let’s dig-in and start to pull apart what Switch Statements are, and why you should care.
From 20,000 feet, switch-case statements are an approach to handling different situations by way of a look-up table rather than with a series of if-else statements. If you’re furrowing your brow consider situations when you may have encountered complex if-else statements where once change breaks everything… for so so much longer than you might want. Also consider what happens if you want to extend that if-else ladder into something more complicated… maybe you want to call different functions or methods based on input conditions, maybe you need to control a remote machine and suddenly you’re scratching your head as you ponder how on earth you’re going to handle complex logic statements across a network. Maybe you’re just after a better code-segmentation solution. Or maybe you’ve run into a function so long you’re starting to loose cycles to long execution times. These are just a few of the situations you might find yourself in and a switch statement might just be the right tool to help – except that there are no switch-case statements in Python.
What gives?!
While there aren’t any switch-case statements, we can use dictionary mappings to get to a similar result… a result so powerful we’re really in for a treat. Before we get there though, we need to look at the situation we’re trying to avoid.
So what exactly is that situation? Let’s consider a problem where we want to only call one function and then let that code block handle all of the various permutations of our actions. That might look like our worst case solution below.
Worst
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
def switcher(func_name, val1, val2): | |
if func_name == "Add": | |
result = val1 + val2 | |
elif func_name == "Subtract": | |
result = val1 – val2 | |
elif func_name == "Multiply": | |
result = val1 * val2 | |
elif func_name == "Divide": | |
result = val1 / val2 | |
return result | |
print( switcher("Subtract", 1, 2) ) |
To get started, what do we have above? We have a single function called switcher() that takes three arguments – the name of the function we want to call, and two values. In this example we have four different math operations, and we want to be able to access any of the four as well as pass in two values and get a result just by calling a single function. That doesn’t seem so hideous on the face of it, so why is this the worst approach?
This example probably isn’t so terrible, but what it does do is bury all the functional mathematical portions of our code inside of a single function. It means we can’t add and test a new element without possibly breaking our whole functional code block, we can only access these operations from within switcher(), and if we decide to add additional operations in the future our code block will just continue to accrue lines of code. It’s a naive approach (naive in the programming sense – as in the first brute force solution you might think of), but it doesn’t give us much room for modularity or growth that doesn’t also come with some unfortunate side effects.
Okay… fine… so what’s a good solution then?
Good
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
def switcher(func_name, val1, val2): | |
if func_name == "Add": | |
result = Add(val1, val2) | |
elif func_name == "Subtract": | |
result = Subtract(val1, val2) | |
elif func_name == "Multiply": | |
result = Multiply(val1, val2) | |
elif func_name == "Divide": | |
result = Divide(val1, val2) | |
return result | |
def Add(val1, val2): | |
sum_val = val1 + val2 | |
return sum_val | |
def Subtract(val1, val2): | |
sum_val = val1 – val2 | |
return sum_val | |
def Multiply(val1, val2): | |
sum_val = val1 * val2 | |
return sum_val | |
def Divide(val1, val2): | |
sum_val = val1 / val2 | |
return sum_val | |
print( switcher("Subtract", 1, 2) ) |
A good solution segments our functions into their own blocks. This allows us to develop functions outside of our switcher() function, call them independently, and have a little more flexible modularity. You might well be thinking that this seems like a LOT more lines… can we really say this is better?! Sure. The additional lines are worth it if we also get some more handles on what we’re doing. It also means we probably save some serious debugging time by being able to isolate where a problem is happening. In our worst case approach we’re stuck with a single function that if it breaks, none of our functions work… and if our logic got sufficiently complex we might be sifting through a whole heap of code before we can really track down what’s happening. Here at least there’s a better chance that a problem is going to be isolated to a single function block – that alone is a HUGE help.
All that said, we’re still not really getting to switch-case statements… we’re still stuck in if-else hell where we’ll have to evaluate our incoming string against potentially all of the possible options before we actually execute our actual code block. At four functions this isn’t so bad, but if we had hundreds we might really be kicking ourselves.
So how can we do better?
Better
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
def switcher(func_name, val1, val2): | |
functions = { | |
"Add" : Add, | |
"Subtract" : Subtract, | |
"Multiply" : Multiply, | |
"Divide" : Divide | |
} | |
active_function = functions.get(func_name) | |
result = active_function(val1, val2) | |
return result | |
def Add(val1, val2): | |
sum_val = val1 + val2 | |
return sum_val | |
def Subtract(val1, val2): | |
sum_val = val1 – val2 | |
return sum_val | |
def Multiply(val1, val2): | |
sum_val = val1 * val2 | |
return sum_val | |
def Divide(val1, val2): | |
sum_val = val1 / val2 | |
return sum_val | |
print( switcher("Subtract", 1, 2) ) |
Better is to remember that the contents of a python dictionary can be any data type – in fact they can even be function names, or Python objects. How does that help use? Well, it means we can look up what function we want to call on the fly, call it, and even pass in variables. In the example above our switcher() function holds a dictionary of all the possible functions at our disposal – when we call our switcher we pass in the name of the function with the variables that will in turn get passed to the function. Above our active_function variable becomes the variable that’s fetched from our dictionary, which we in turn pass our incoming variables along to.
That’s great in a lot of ways, but especially in that it gets us away from long complicated if-else trees. We can also use this as a mechanism for handling short-hand names for our methods, or multiple assignments – we might want two different keys to access the same function (maybe “mult” and “Multiple” both call the same function for example).
So far this is far away a better approach, so how might we make this better still?
Best
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
def switcher(func_name, vals): | |
functions = { | |
"Add" : Add, | |
"Subtract" : Subtract, | |
"Multiply" : Multiply, | |
"Divide" : Divide | |
} | |
active_function = functions.get(func_name) | |
result = active_function(vals) | |
return result | |
def Add(vals): | |
sum_val = 0 | |
for item in vals: | |
sum_val += item | |
return sum_val | |
def Subtract(vals): | |
sum_val = vals[0] | |
for item in vals[1:]: | |
sum_val -= item | |
return sum_val | |
def Multiply(vals): | |
if len(vals) > 2: | |
sum_val = 'Invalid Call – "Multiply" only takes two vals' | |
else: | |
sum_val = vals[0] * vals[1] | |
return sum_val | |
def Divide(vals): | |
if len(vals) > 2: | |
sum_val = 'Invalid Call – "Divide" only takes two vals' | |
else: | |
sum_val = vals[0] / vals[1] | |
return sum_val | |
vals = [3, 4] | |
print( switcher("Multiply", vals) ) |
We might take this one step further and start to consider how we might address accepting an arbitrary number of vals. Above we have a simple way to tackle this – probably not what you’d end up with in production, but something that should hopefully get you thinking. Here the variable vals becomes a list that can be any number of values. In the case of both our Add() and Subtract() functions we loop through all of the values – adding each val, or subtracting each val respectively. In the case of our Multiply() and Divide() functions we limit these operations to only two values for the sake of our example. What’s interesting here is that we can return can think about error handling based on the array of values that’s coming into our function.
The above is great, of course, but it’s really just the beginning of the puzzle. Where this really starts to become interesting is how you might think of integrating this approach in your python extensions.
Or if vals is a a dictionary in it’s own right rather than a simple list.
Or if you can send a command like this over the network.
Or if you can start to think about how to build out blocks of code that are specific to a single job, and universal blocks that apply to all of your projects.
Next we’ll start to pull apart some of those very ideas and see where this concept really gets exciting and creates spaces for building tools that persists right alongside the tools that you have to build for a single job.
In the meantime, experiment with some Python style switch statements to see if you can get a handle on what’s happening here, and how you might take better advantage of this method.
Happy programming!
References
Looking for another perspective on this approach form a more pure Python perspective? Check out this post on Jaxenter.com.
2 comments
Comments are closed.