ActiveState Powered by ActiveState

Recipe 410692: Readable switch construction without lambdas or dictionaries


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.

Python
 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.

Discussion

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

Comments

  1. 1. At 10:57 a.m. on 25 apr 2005, Zoran Isailovski said:

    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

  2. 2. At 12:24 p.m. on 25 apr 2005, Greg Jorgensen said:

    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. ;-)

  3. 3. At 3:18 p.m. on 25 apr 2005, Duncan McGreggor said:

    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.

  4. 4. At 11:27 p.m. on 25 apr 2005, hemanth sethuram said:

    Nice solution. Great solution. I liked it for its simplicity and elegance. The need for explicit "pass" is in line with all things Pythonic.

  5. 5. At 1:41 a.m. on 26 apr 2005, Pierre Quentel said:

    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

    import string
    c = 'A'
    for case in switch(c):
        if case(*string.lowercase):
            print "c is lower-case !"
            break
        if case(*string.uppercase):
            print "c is upper-case!"
            break
        if case(): # default
            print "I dunno what c was!"
    

    You would just have to change a line in the match method to

    elif self.value in args:
    
  6. 6. At 2:21 a.m. on 26 apr 2005, Brian Beck (the author) said:

    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.

  7. 7. At 8:08 a.m. on 26 apr 2005, Michael Chermside said:

    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:

    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.

    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:

    if v == 'one':
        print 1
    elif v == 'two':
        print 2
    elif v == 'ten':
        print 10
    else:
        print "something else!"
    

    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.

  8. 8. At 9:20 a.m. on 26 apr 2005, B Mahoney said:

    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.

  9. 9. At 9:26 a.m. on 26 apr 2005, Brian Beck (the author) said:

    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?

    class test(object):
        def __init__(self, value):
            self.value = value
    
        def __eq__(self, other):
            print 'calling __eq__'
            if hasattr(other, 'value'):
                return not cmp(self.value, other.value)
            else:
                return not cmp(self.value, other)
    
    t = test(10)
    if t == 0:
        print 'zero'
    elif t == 5:
        print 'five'
    elif t == 10:
        print 'ten'
    elif t == 15:
        print 'fifteen'
    else:
        print 'twenty'
    

    The output of the above is:

    calling __eq__
    calling __eq__
    calling __eq__
    ten
    

    So how is this not O(n) for cases? Or does this only happen if __eq__ is overridden?

  10. 10. At 9:32 a.m. on 26 apr 2005, Brian Beck (the author) said:

    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.

  11. 11. At 12:42 p.m. on 26 apr 2005, Michael Chermside said:

    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.

  12. 12. At 6:02 a.m. on 27 apr 2005, hemanth sethuram said:

    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.

  13. 13. At 8:01 a.m. on 27 apr 2005, Brian Beck (the author) said:

    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.

    The issue is discussed here -> http://c2.com/cgi-bin/wiki?SwitchStatement
    Also, question #6 here -> http://c2.com/cgi-bin/wiki?SwitchStatement
    

    In Python, it might be important to also consider the overhead in each approach, but I'm no expert in this area.

  14. 14. At 8:02 a.m. on 27 apr 2005, Brian Beck (the author) said:

    Oops, that second link should be:

    http://www.devx.com/amd/Article/21314
    
  15. 15. At 8:57 a.m. on 27 apr 2005, Zoran Isailovski said:

    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"

    b, a = a, b
    

    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.

  16. 16. At 3:35 p.m. on 4 may 2005, Markus Schramm said:

    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?

    def switch(value):
        truth = [False]
        def case(*args):
            #~ if not args: return True
            if value in args:
                truth[0] = True
            return truth[0]
        return [case]
    

    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.

  17. 17. At 10:40 a.m. on 16 may 2005, Fredrik Johansson said:

    I just timed both alternatives, and, despite your presumptions, it turns out that tuple unpacking is faster than using a temporary variable.

  18. 18. At 9:35 a.m. on 17 may 2005, Zoran Isailovski said:

    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.

  19. 19. At 6:40 p.m. on 19 may 2005, Zoran Isailovski said:

    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.

  20. 20. At 12:39 p.m. on 14 jun 2005, S C said:

    Seeing a switch: pythonic encapsulating and "brainy" chunking:

    Brian Beck's - switch - class encasulates the underlying logic so that:
    
     (1) The logic can be applied without regard to the name of what is being
     assessed.
     (2) Hence the block of code in the switch instance becomes modular.
     It can be readily moved/copied from module to module.
     It can be found and copied or re-written by python.  That's especially cool for dynamic modeling of changeable processes.
    

    [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'?

  21. 21. At 2:47 a.m. on 14 aug 2005, Jakub Stolarski said:

    Different results. Maybe I do it wrong, but on my FreeBSD 5.4 with Python 2.4.1 this code:

    import timeit
    a = 1
    b = 2
    t = timeit.Timer('a, b = b, a', 'from __main__ import a, b')
    print t.timeit()
    t = timeit.Timer('c = a; a = b; b = c', 'from __main__ import a, b')
    print t.timeit()
    
    Gives me:
    0.129501104355
    0.168084859848
    

Sign in to comment