Emulating Switch/Case Statements With Dicts
用字典仿制一个switch+case语句的写法
Python doesn’t have switch/case statements so it’s sometimes necessary to write long if...elif...else chains as a workaround. In this chapter you’ll discover a trick you can use to emulate switch/case statements in Python with dictionaries and first-class functions. Sound exciting? Great—here we go!
Imagine we had the following if-chain in our program:
>>> if cond == 'cond_a':
... handle_a()
... elif cond == 'cond_b':
... handle_b()
... else:
... handle_default()
Of course, with only three different conditions, this isn’t too horrible yet. But just imagine if we had ten or more elif branches in this statement. Things would start to look a little different. I consider long if- chains to be a code smell that makes programs more difficult to read and maintain.
One way to deal with long if...elif...else statements is to replace them with dictionary lookup tables that emulate the behavior of switch/case statements.
The idea here is to leverage the fact that Python has first-class functions. This means they can be passed as arguments to other functions, returned as values from other functions, and assigned to variables and stored in data structures.
函数作为第一公民可以被传递到其他的函数,从其他函数中返回值,并将值赋给变量,存储在数据结构中。
For example, we can define a function and then store it in a list for later access:
>>> def myfunc(a, b):
... return a + b
...
>>> funcs = [myfunc]
>>> funcs[0]
<function myfunc at 0x107012230>
The syntax for calling this function works as you’d intuitively expect— we simply use an index into the list and then use the “()” call syntax for calling the function and passing arguments to it:
>>> funcs[0](2, 3) 5
Now, how are we going to use first-class functions to cut our chained if-statement back to size? The core idea here is to define a dictionary that maps lookup keys for the input conditions to functions that will carry out the intended operations:
>>> func_dict = {
... 'cond_a': handle_a,
... 'cond_b': handle_b
... }
这确实是一个很好的思路。
Instead of filtering through the if-statement, checking each condition as we go along, we can do a dictionary key lookup to get the handler function and then call it:
>>> cond = 'cond_a'
>>> func_dict[cond]()
This implementation already sort-of works, at least as long as cond can be found in the dictionary. If it’s not in there, we’ll get a KeyError exception.
So let’s look for a way to support a default case that would match the original else branch. Luckily all Python dicts have a get() method on them that returns the value for a given key, or a default value if the key can’t be found. This is exactly what we need here:
>>> func_dict.get(cond, handle_default)()
This code snippet might look syntactically odd at first, but when you break it down, it works exactly like the earlier example. Again, we’re using Python’s first-class functions to pass handle_default to the get()-lookup as a fallback value. That way, if the condition can’t be found in the dictionary, we avoid raising a KeyError and call the default handler function instead.
Let’s take a look at a more complete example for using dictionary lookups and first-class functions to replace if-chains. After reading through the following example, you’ll be able to see the pattern needed to transform certain kinds of if-statements to a dictionary-based dispatch.
We’re going to write another function with an if-chain that we’ll then transform. The function takes a string opcode like "add" or "mul" and then does some math on the operands x and y:
>>> def dispatch_if(operator, x, y):
if operator == 'add':
return x + y
elif operator == 'sub':
return x - y
elif operator == 'mul':
return x * y
elif operator == 'div':
return x / y
To be honest, this is yet another toy example (I don’t want to bore you with pages and pages of code here), but it’ll serve well to illustrate the underlying design pattern. Once you “get” the pattern, you’ll be able to apply it in all kinds of different scenarios.
You can try out this dispatch_if() function to perform simple calculations by calling the function with a string opcode and two numeric operands:
>>> dispatch_if('mul', 2, 8)
16
>>> dispatch_if('unknown', 2, 8)
None
Please note that the 'unknown' case works because Python adds an implicit return None statement to the end of any function.
So far so good. Let’s transform the original dispatch_if() into a new function which uses a dictionary to map opcodes to arithmetic operations with first-class functions.
>>> def dispatch_dict(operator, x, y):
return {
'add': lambda: x + y,
'sub': lambda: x - y,
'mul': lambda: x * y,
'div': lambda: x / y,
}.get(operator, lambda: None)()
上面写得确实很美
This dictionary-based implementation gives the same results as the original dispatch_if(). We can call both functions in exactly the same way:
>>> dispatch_dict('mul', 2, 8)
16
>>> dispatch_dict('unknown', 2, 8)
None
There are a couple of ways this code could be further improved if it was real “production-grade” code.
但是还是有不少可以改进的地方
First of all, every time we call dispatch_dict(), it creates a temporary dictionary and a bunch of lambdas for the opcode lookup. This isn’t ideal from a performance perspective. For code that needs to be fast, it makes more sense to create the dictionary once as a constant and then to reference it when the function is called. We don’t want to recreate the dictionary every time we need to do a lookup.
每次我们调用dispatch函数的时候我们都是创建了临时的字典和一系列的lambda表达式,从性能方面看不是理想的。我们不如把字典创建为一个定量值,然后每一次使用的时候就去引用一下。
Second, if we really wanted to do some simple arithmetic like x + y, then we’d be better off using Python’s built-in operator module instead of the lambda functions used in the example. The operator module provides implementations for all of Python’s operators, for example operator.mul, operator.div, and so on. This is a minor point, though. I intentionally used lambdas in this example to make it more generic. This should help you apply the pattern in other situations as well.
我们如果需要使用x + y 这样的表达式我们最好使用内置库里面的一些操作方法,比如operator中的add mul div等等方法。
Well, now you’ve got another tool in your bag of tricks that you can use to simplify some of your if-chains should they get unwieldy. Just remember—this technique won’t apply in every situation and some- times you’ll be better off with a plain if-statement.
Key Takeaways
- Python doesn’t have a switch/case statement. But in some cases you can avoid long if-chains with a dictionary-based dispatch table.
- Once again Python’s first-class functions prove to be a powerful tool. But with great power comes great responsibility.