Python's lack of a 'switch' statement has garnered much discussion and even a PEP. The most popular substitute uses dictionaries to map cases to functions, which requires lots of defs or lambdas. While the approach shown here may be O(n) for cases, it aims to duplicate C's original 'switch' functionality and structure with reasonable accuracy.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | # This class provides the functionality we want. You only need to look at
# this if you want to know how this works. It only needs to be defined
# once, no need to muck around with its internals.
class switch(object):
def __init__(self, value):
self.value = value
self.fall = False
def __iter__(self):
"""Return the match method once, then stop"""
yield self.match
raise StopIteration
def match(self, *args):
"""Indicate whether or not to enter a case suite"""
if self.fall or not args:
return True
elif self.value in args: # changed for v1.5, see below
self.fall = True
return True
else:
return False
# The following example is pretty much the exact use-case of a dictionary,
# but is included for its simplicity. Note that you can include statements
# in each suite.
v = 'ten'
for case in switch(v):
if case('one'):
print 1
break
if case('two'):
print 2
break
if case('ten'):
print 10
break
if case('eleven'):
print 11
break
if case(): # default, could also just omit condition or 'if True'
print "something else!"
# No need to break here, it'll stop anyway
# break is used here to look as much like the real thing as possible, but
# elif is generally just as good and more concise.
# Empty suites are considered syntax errors, so intentional fall-throughs
# should contain 'pass'
c = 'z'
for case in switch(c):
if case('a'): pass # only necessary if the rest of the suite is empty
if case('b'): pass
# ...
if case('y'): pass
if case('z'):
print "c is lowercase!"
break
if case('A'): pass
# ...
if case('Z'):
print "c is uppercase!"
break
if case(): # default
print "I dunno what c was!"
# As suggested by Pierre Quentel, you can even expand upon the
# functionality of the classic 'case' statement by matching multiple
# cases in a single shot. This greatly benefits operations such as the
# uppercase/lowercase example above:
import string
c = 'A'
for case in switch(c):
if case(*string.lowercase): # note the * for unpacking as arguments
print "c is lowercase!"
break
if case(*string.uppercase):
print "c is uppercase!"
break
if case('!', '?', '.'): # normal argument passing style also applies
print "c is a sentence terminator!"
break
if case(): # default
print "I dunno what c was!"
# Since Pierre's suggestion is backward-compatible with the original recipe,
# I have made the necessary modification to allow for the above usage.
|
While the use of 'for ... in' may not be semantically accurate here (it will only ever execute the suite once), the fact that we are trying multiple cases (even if they are all within the same iteration) at least gives it the illusion of making some sense.
For better or worse, changing the switched variable within the suite will have no effect on the proceeding cases. This is a diversion from C functionality.
As noted above, case() (with no arguments) is used in place of 'default'. Another option would be to just omit the last conditional or use 'if True:'. To make that part look even more like the classic idiom, you could: --> change "yield self.match" in __iter__ to "yield self.match, True" --> change "for case in" to "for case, default in" --> change "if case():" to "if default:"
...but, in my opinion, this slight adjustment is just a trade-off of one area of readability for another.
As far as I can tell, people generally use switch for its conciseness and readability, and not for its better lookup time, if that is indeed true.
Enjoy!
Alan Haffner's dictionary-based recipe: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/181064
Runsun Pan's dictionary-based recipe: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/269708
PEP 275 - Switching on Multiple Values: http://www.python.org/peps/pep-0275.html
A very inventive approach!!! This is what I'd call an inspired solution. Of all switch-case substitutes, I probably like this one most.
Just one marginal note: the "raise StopIteration" at the end of "__iter__" is probably superfluous. StopIteration is raised as soon as a generatator returns normally.
Perhaps you'd like to see my "Exception-based Switch-Case" recipe at
http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/410695
and give me some comment on it?
Best Regards
Zoran
re: Readable switch construction... Well-done and readable solution.
I think we're getting close to being able to implement Duff's Device (http://en.wikipedia.org/wiki/Duff%27s_device) in Python. ;-)
Most Elegant. In python, I really haven't missed switch(), but I am really impressed with the simplicity and beauty of this implementation. Very nice.
Nice solution. Great solution. I liked it for its simplicity and elegance. The need for explicit "pass" is in line with all things Pythonic.
Multiple cases ? I agree with the previous comments, it's nice, concise and elegant. Have you considered an extension to multiple cases ? I mean something like
You would just have to change a line in the match method to
Multiple cases ? Ooh, nice. I didn't consider it, as I was aiming to duplicate the actual functionality of 'switch', but this possibility definitely enhances it, in my opinion. I'm going to mention your suggestion in the recipe, if you don't mind.
I hate to knock such a beautiful effort... ... but I think I have to disagree with the consensus here: I think this is a poor idea.
Brian explains:
But I disagree. Switch IS O(1) in number of branches, and that is an extremely important feature of it. Dictionary dispatch is the correct solution in Python when "a switch statement" is needed because it is O(1), and IMHO when people say "a switch statement" they are implying O(1).
If readability is the only criterion, then I personally believe that you can't beat this:
So, while I admire the cleverness that went into creating a syntax that closely mimics that of C and its offspring, I would advise AGAINST ever using this recipe. It's not that the recipe is poor, just that what it attempts to do (imitate C syntax) is not, IMHO, a wise idea in Python.
It's not about Python, it's about a class of "switch" You could extend this thing a long way and it will become more internally consistent/elegant, but I don't think it make things easier for a Python programmer. It is good for someone who needs to program a switch in some customized language running under Python. For example, case could be smart enough to know when it tests true, so it raises an error when the programmer fails to choose a course of action in the code block and falls onto the next case test. That would be useful for some mini-language. Speaking as someone who often forgot the 'break' statement when first doing C
But as a Python programmer, why would I write code that requires I remember to hardcode a 'break' or other action within each code block for any reason other than imitating C programming?
This class could be expanded to be a smart tool for novice programmers, but as a Python programmer I would just leverage existing constructs, such as dictionaries.
Re: I hate to knock such a beautiful effort... Hold on now... you recommend against using this just because it's O(n), but then recommend using if, elif, else?
The output of the above is:
So how is this not O(n) for cases? Or does this only happen if __eq__ is overridden?
Re: It's not about Python, it's about a class of "switch" If you don't want to manually break, just change subsequent 'if' to 'elif'. It still saves you from typing a similar conditional over and over.
My Response. No, I recomend against using it because it is O(n) and if you want an O(n) solution, then you should use if-elif-else. In C (and its decendents) one uses switch() despite its ugliness because it is O(1), but this recipe lacks that advantage.
switch is not O(1) in C. I remember reading it somewhere that the switch..case in C is only a syntactic sugar for if.. else if and not a O(1). What most of the modern C compilers do when you profile the code and reuse the profile information is that they put the most likely condition to succeed at the top of the if..else if.. structure so that on the average you get an O(1) performance.
switch O(?). Okay, after doing some reading in this direction, I've concluded that C's switch can be O(1), but it is not guaranteed.
In Python, it might be important to also consider the overhead in each approach, but I'm no expert in this area.
Oops, that second link should be:
Is there really a point in this? I don't think that the cookbook is reserved for speedy recipes only. For example, the recipe "Swapping Values Without Using a Temporary Variable"
involves packing and unpacking a tuple and probably runs slower then code that does use a temporary variable. Does this disqualify the recipe? No.
As I understand the cookbook, it is about how things can be done with python. A good recipe will point out the pro's and con's for/against using it, and it's up to the cook what he/she will do about it. You want a speedy dish? Use dictionaries. You want a dish that looks like C but speed is less important to you? Use the recipe here.
At the end, a cookbook conveys freedom of choice, and more recipes mean more freedom of choice.
Another implementation. I think the idea is really interesting, interesting enough to propose another implementation. This one feels more natural to me (and is also much shorter).
What do you think?
It has exactly the same behavior, but doesn't special case the call without args (just commented out here). The reasons: (1) It has been the only case that returns True, but does not start falling through further calls. (2) Anyway in C or Java there is no direct equivalent for it. Case must always have an argument. The default case has its own keywork ("default"). (3) You did already mention it: Its not necessary. Just omit the last conditional and write the default code near the end of the for loop. A conditional 'if True:' near the end, or an 'else:' clause for the for-loop have the same effect.
I just timed both alternatives, and, despite your presumptions, it turns out that tuple unpacking is faster than using a temporary variable.
In what python version? On what platform? ...
Unpacking and re-packing used to be the slower variants in early versions of python, but even back then a, b = c, d was attractive for its conciseness, if not for the speed.
Anyway, timing is not the point here. Or would you change your code with every new version of python just because some constructs are now more optimized then the others used sofar?
I wonder how some people seem to be obsessed with speed at every bit, but then choose python as the implementation language. But I do admire the time and energy spent to measure even such negligible issues like the above.
Yet I couln't withstand the temptation and timed it myself. It turns out that a, b = b, a is about 7% slower on Python 2.2 and WinXP and about 11% slower on Python 2.4 and Win2000. My presumptions seem to still hold.
Seeing a switch: pythonic encapsulating and "brainy" chunking:
[How pythonic is it?]....
Encapsulating is highly pythonic. It gives us the local namespaces of function, class, and module, as well as more subtle **kwargs and *args groupings of parameters. In terms of the instance at hand, here we see encapsulating both in the switch class's function suite, and in the *args addition Pierre Quentel suggested for groupings of workalike parameters.
[don cogsci hat]....
In cognitive science terms, this encapsulating of a [supposedly unique-]choice function is akin to "chunking". Chunking is a well-proven psychological "trick": Chunking several items into one allows more stuff to be fitted into a limited-capacity memory or buffer (cf. fixed-length namespace). But here, instead of grouping items the abstraction is a grouping of tests. This structure of sequenced test criteria thus implements choice_function-chunking.
[switch back ;)] ...
Is this a case of 'Python fits the brain'?
Different results. Maybe I do it wrong, but on my FreeBSD 5.4 with Python 2.4.1 this code:
The Beck switch is ingenious.
I don't use switch statements. In C I index into arrays of functions. Swapping that array with another can change the code's personality. In python I use dictionaries of functions.
very nice solution indeed.
I love the simplicity of this construct - especially how readable it makes the code.
However, I'm struggling to see what the self.fall variable is for. It's probably something really obvious that I'm overlooking, but to me it seems superfluous. Could you comment, please?
Many thanks! :)
With the first syntax, you can make a fairly semantic 'default' case using
for
/else
:I think the biggest gotcha there is the slightly unexpected indentation.
Great Job.....Nice Implementation