Emulating "switch/case" Statements
00:00 I want to talk about a really cool Python trick, and that is a way to emulate switch/case statements in Python.
00:09
What I frequently see in Python code is that people end up with these relatively long if
statements—these kind of chained if
statements to decide on what action to take based on a condition in their code. To give you an example, let’s say we had this if
statement where we had a condition and we wanted to compare it to a certain value and then, “Okay, if we get A, I want to call handle_a()
, and if we get a condition B, then we’re going to call handle_b()
, and if we get none of these, then we’re going to call handle_default()
. Right?
00:50 And you can imagine that this would go on for much longer, and this would get kind of cumbersome, right? It’s a lot of typing, and the reason for that is Python doesn’t really have a switch/case statement with which we could use to condense this a bit and make it more terse and easier to read.
01:14 So, there is actually a way to emulate that switch/case behavior in Python, and I want to show you how to do that. The way to do that is by leveraging the fact that Python has first-class functions. So, functions in Python—they’re just like any other object.
01:28 You can pass them as arguments to other functions, and you can return them from functions as values, and you can store them in variables and store them in data structures.
01:40
So, this is actually a really flexible feature in Python. To give you an example, we can define a simple function here that takes two arguments and just returns the sum of both. I could actually go ahead and say, “Hey, I’m going to store this function in a list that I’m going to call funcs
,” and then I could just refer—whoops—I could just refer to that function and call it.
02:11
Right? So if I did that, I would actually get a result that is 5
because it’s adding together 2
and 3
, right? I would just—well, with the index here—grab the first function in there—which is myfunc()
, which we defined up there—and then I can just call it with the braces syntax.
02:32 So, this is extremely flexible, and this is a really cool feature that’s going to allow us to emulate how a switch/case statement would work. All right. So, let’s come back to our original plan here, which was emulating a switch/case statement. So, now that we know about these first-class functions, what we can do here is actually create a function dictionary that maps from the condition name—or, from the actual condition to the handler function.
03:03 Right? So, something like this where we’re just mapping from the condition to the handler function. Then, I could look up that handler function just by passing the condition as a key into that dictionary, right?
03:25 Then, I could just call the resulting function. And this would actually be—
03:32 sort of—a switch/case statement, right? And we could shorten that further—I mean, you know, we don’t have to give a name to this. Like, we could just do this, and this would already be kind of terse or,
03:48
you know, there’s not a lot of overhead to this. But it has a big downside, and that is we’re going to get a KeyError
if the condition isn’t found. There’s no default at the moment, right? Previously with the if
statement we had an else
all the way at the bottom, and that would allow us to have a default branch that we can take and a default handler that we would call—and we can’t really do that right now.
04:17
So actually, one way around that is—and let me undo some of that—so actually one way around that would be to use the .get()
function on the dictionary. We could just call .get()
, and then that would either return the value for that key, if it’s found, or it would return a default. In this case, we would just pass in a handle_default
function that we’d have to define, and then call that.
04:50
So, this would actually solve the problem with the KeyError
, and we’d be able to have a default and have this dispatch based on this function dictionary here, which is already kind of neat. And you can imagine, like, this becomes really useful if you have many, many, many, many functions that you want to map to, or many conditions that you want to map to functions.
05:12
Because then, it actually becomes a lot more efficient than an if
/ else
statement, because an if
/ else
—it needs to try all these conditions in sequence, but with a dictionary, it can actually take a shortcut and do a lookup based on the hash of that condition.
05:29 That can be a lot faster if you’re dealing with many of these mappings. I mean, if you’re only dealing with tens or potentially even hundreds of them, it won’t matter, so it would be a premature optimization, but for a really large number of functions and conditions, this could be extremely helpful.
05:46
So, this could be a really nice trick for you to actually speed up your Python code, to optimize it. So now, this might look kind of weird syntactically, and there’s actually one step we can go beyond this to clean this up, and I want to show you how to do that now. All right, so let’s take a look at a little bit more realistic example, a little bit more complex example. So, I’ve got some code here that is an if
statement that would do the dispatch for us.
06:23 The idea for this would be, you know, maybe we’re implementing some kind of virtual machine and want to be able to pass it an operator and two operands, and then it would look up the operator and then carry out some operation based on that, right? Like, it would either add the two numbers, or subtract them, or multiply them, or divide them. So, you know, it’s just a little toy example here. Okay, so just to show you how you would use this function, we’re going to call the function, and then we would pass the instruction or the operator to it—so in this case we want multiplication—and then we would pass two values to it.
07:00
And the result of that—we would expect the result of that to be 16
, right? 2 * 8
. And this would just kind of go through that if
, and say, “Okay, well, we’re looking for the operator == 'mul'
, so I’m just going to return x * y
.” This is how this would work.
07:17 Now, if we wanted to implement the same function using this dictionary as a switch/case emulation of sorts, then it would look a little bit different. And I’m introducing another Python feature here into the mix, so let’s just walk through that, right? So, this would actually be a little bit shorter. What this function would do is it would create a dictionary, and then it would define these handler functions with a bunch of lambdas here.
07:52
So, this is a way for us to not have to define these handle_add()
, and handle_sub()
, and all these helper functions separately—but we can kind of keep that actual behavior inline, here.
08:06
Then, we’re using—well, we’re creating—this dictionary and then we’re using the .get()
method on the dictionary with the operator and our default is just returning None
. So, you know, as you might’ve seen here: this if
—if we fall through all of the cases and none of them match—then Python does an implicit return None
here at the end. Or, you know, we could also explicitly add that, and it would probably be better for real-world code: doing something like this, or just doing something like this where there’s just no way we can fall through. I mean, in this case, they would both be equivalent, so if I wanted to remove that to kind of show how terse this could also be. But, really, here you can see that this is equivalent to this code up here, and it looks kind of nice. I mean, it’s not exactly a switch/case statement, and there’s actually one big downside to this that I want to mention as well, but it works kind of nicely, and it can be a good way to work in these dispatch situations where you want to map some condition to some kind of behavior. It can be a really helpful way to structure your code. Now, one downside to doing it like this is that you’re constructing a dictionary object, and a bunch of lambda or a bunch of functions every time this dispatch_dict()
function is called—which is not great, right?
09:36 For memory reasons and for performance reasons, it’s not a good decision. I mean, it won’t matter if you’re not calling this a lot—it honestly won’t matter, and I would go with whatever is cleaner. But one way to get around this would be to take this value—or, this dictionary, and actually store it in some variable in some other place, and then you can refer to it, right? Like, if you have this massive dispatch table mapping from condition to handler, I would just cache that somewhere—keep it in a variable—and then in your actual dispatch code, you would just take that dictionary and look up the function and then call the function, and that way you don’t have to recreate this dictionary every single time the dispatch function is called. But, you know, like I said, if you’re doing this with just a couple of cases and it’s not in, like, a tight loop, like a hot spot in your program—it probably won’t matter, and it would be premature optimization.
10:35 I would go with whatever looks better to you. I mean, another downside here is that this is pretty advanced Python, so if you’re doing this—you know, depending on the skill levels in your team, or the people you’re working with—people might have a hard time dealing with this, right? Because it can be so foreign.
10:52
Like, you need to understand first-class functions, all of these advanced-level features, whereas a nice and clean if
statement—it gets the job done and it might actually be the better choice in a lot of situations.
11:03 So, I just want to make sure you are aware of that, but it definitely has its place and it definitely has its applications. And again, I think it’s a really cool feature that teaches you about some of the cool, lesser-known aspects of Python.
Rick Jakubowski on April 26, 2020
I figured it out:
operation_dict = {
'add': lambda x,y: x + y,
'sub': lambda x,y: x - y,
'mul': lambda x,y: x * y,
'div': lambda x,y: x / y,
}
#trying to store the dictionary
def dispatch_dict(operator, x, y):
f = operation_dict.get(operator, None)
print(f)
if f != None:
print(f(x,y))
return f(x,y)
else:
return None
Dan B on Nov. 10, 2020
This is too hacky for my taste. Why not just implement case statements in python?
Bartosz Zaczyński RP Team on Nov. 12, 2020
@Dan B The official documentation has a section explaining why there isn’t a switch or case statement in Python.
There was a proposal back in 2006 to add a switch/case statement to Python, but it was rejected due to little interest in the idea. There’s a new proposal for somewhat similar pattern matching, which has the “draft” status at the moment.
Dawn0fTime on June 5, 2021
@Rick Jakubowski, great way to illustrate what @Dan Bader mentions in the video! You can shorten your def even further:
def dispatch_dict(operator, x, y):
return operation_dict.get(operator, None)(x, y)
Ondřej Vacek on Aug. 10, 2021
As from release of python 3.10 structural pattern matching will be added (PEP 634, 635, 636) which basically gives you option to emulate switch in much more readable format.
def switch(operator, x ,y)
match operator:
case "add":
return x + y
case "sub":
return x - y
case "mul":
return x * y
case "div"
return x / y
case _:
return None
thehitman on Jan. 17, 2024
Thanks Dan,
I really like Python and Real python. It is a major learning curve for me.
from datetime import datetime
from dateutil.relativedelta import relativedelta
def datediff(date_part: str, orderdate: str, deliverydate: str) -> int:
"""_Function to calculate the difference between two dates based on the date_part
Args:
date_part (str):
YY : Year, year, y, yy, or yyyy
QU : Quarter, quarter, qq, q
MO : Month, month, mm, m
Day: dayofyear, day, dd, d
WK : Week, week, wk, ww
HR : Hour, hour, hh
MI : Minute, minute, mi, n
SS : Second, second, ss, s
orderdate (str): is the starting date for the date difference
deliverydate (str): is the ending date for the date difference
Returns:
int: The date difference between the two dates
"""
mm,dd,yyyy = get_month_day_and_year(orderdate)
start_date = datetime(yyyy,mm,dd)
mm,dd,yyyy = get_month_day_and_year(deliverydate)
end_date = datetime(yyyy,mm,dd)
delta = relativedelta(end_date, start_date)
date_part_units = {
'YY': lambda: delta.years,
'QU': lambda: (delta.years * 4) + (delta.months // 3),
'MO': lambda: (delta.years * 12) + delta.months,
'DD': lambda: (end_date - start_date).days,
'WK': lambda: ((end_date - start_date).days // 7),
'HR': lambda: ((end_date - start_date).total_seconds() // 3600),
'MI': lambda: ((end_date - start_date).total_seconds() // 60),
'SS': lambda: (end_date - start_date).total_seconds()
}
return date_part_units.get(date_part.upper(), lambda: None)()
a = datediff('QU', '7/21/92', '11/20/97')
print(a)
You must own this product to join the conversation.
Rick Jakubowski on April 26, 2020
Could you please show how to pass arguments once the functions are in a dictionary outside dispatch_dict? I’d still like to use the get method in the return.