Welcome, guest | Sign In | My Account | Store | Cart
14

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, 88 lines
 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

26 comments

Zoran Isailovski 11 years, 3 months ago  # | flag

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

Greg Jorgensen 11 years, 3 months ago  # | flag

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

Duncan McGreggor 11 years, 3 months ago  # | flag

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.

hemanth sethuram 11 years, 3 months ago  # | flag

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

Pierre Quentel 11 years, 3 months ago  # | flag

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:
Brian Beck (author) 11 years, 3 months ago  # | flag

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.

Michael Chermside 11 years, 3 months ago  # | flag

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.

B Mahoney 11 years, 3 months ago  # | flag

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.

Brian Beck (author) 11 years, 3 months ago  # | flag

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?

Brian Beck (author) 11 years, 3 months ago  # | flag

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.

Michael Chermside 11 years, 3 months ago  # | flag

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.

hemanth sethuram 11 years, 3 months ago  # | flag

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.

Brian Beck (author) 11 years, 3 months ago  # | flag

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.

Brian Beck (author) 11 years, 3 months ago  # | flag

Oops, that second link should be:

http://www.devx.com/amd/Article/21314
Zoran Isailovski 11 years, 3 months ago  # | flag

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.

Markus Schramm 11 years, 2 months ago  # | flag

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.

Fredrik Johansson 11 years, 2 months ago  # | flag

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

Zoran Isailovski 11 years, 2 months ago  # | flag

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.

Zoran Isailovski 11 years, 2 months ago  # | flag

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.

S C 11 years, 1 month ago  # | flag

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

Jakub Stolarski 10 years, 11 months ago  # | flag

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
David Lambert 7 years, 9 months ago  # | flag

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.

Victor Noagbodji 3 years, 6 months ago  # | flag

very nice solution indeed.

John Landells 1 year, 12 months ago  # | flag

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! :)

Andrew Taylor 1 year, 6 months ago  # | flag

With the first syntax, you can make a fairly semantic 'default' case using for/else:

for case in switch(x):
  if case(1):
    print 1
    break
  if case(2):
    print 2
    break
else:
  print "something else!"

I think the biggest gotcha there is the slightly unexpected indentation.

Yehonatan Peleg 8 months ago  # | flag

Great Job.....Nice Implementation

Add a comment

Sign in to comment

Created by Brian Beck on Mon, 25 Apr 2005 (PSF)
Python recipes (4482)
Brian Beck's recipes (5)

Required Modules

Other Information and Tasks