Switch-case statement in Python revisited

This post is part of the Powerful Python series where I talk about features of the Python language that make the programmer’s job easier. The Powerful Python page contains links to more articles as well as a list of future articles.

About nine months ago I wrote a post talking about how you might go about implementing a switch case statement in the Python programming language. Python lacks a native switch-case like construct, but there are multiple ways to fake a similar effect. The most naive way would be multiple if-else blocks. A more Pythonic way would be to use Python’s dictionaries and and first class functions. A simple example in C, and the two Python ways is shown below.

switch(n) {
  case 0:
    printf("You typed zero.\n");
    break;
  case 1:
  case 9:
    printf("n is a perfect square\n");
    break;
  case 2:
    printf("n is an even number\n");
  case 3:
  case 5:
  case 7:
    printf("n is a prime number\n");
    break;
  case 4:
    printf("n is a perfect square\n");
  case 6:
  case 8:
    printf("n is an even number\n");
    break;
  default:
    printf("Only single-digit numbers are allowed\n");
  break;
}

if n == 0:
    print "You typed zero.\n"
elif n== 1 or n == 9 or n == 4:
    print "n is a perfect square\n"
elif n == 2:
    print "n is an even number\n"
elif  n== 3 or n == 5 or n == 7:
    print "n is a prime number\n"

options = {0 : zero,
                1 : sqr,
                4 : sqr,
                9 : sqr,
                2 : even,
                3 : prime,
                5 : prime,
                7 : prime,
}

def zero():
    print "You typed zero.\n"

def sqr():
    print "n is a perfect square\n"

def even():
    print "n is an even number\n"

def prime():
    print "n is a prime number\n"

The Fine Print

However, as the comments in the original post show, neither of the Python examples are a very good solution. They lack the versatility and power of the original C form, both in terms of syntax and semantics. Syntactically, neither of the forms do a good job of conveying the intent of the written code. The if-else form does an acceptable job and may be fewer lines, but it clutters the code with keywords and multiple equality checks and boolean combinations. The dictionary form is even worse at conveying the intention of the code.

Semantically, the two forms don’t stand up to the C form either. Multiple if-elses are probably as close as you can get, but it doesn’t work quite the same way. In particular, the ‘fall through’ semantics of a switch-case statement, where consecutive blocks are executed if there is no break statement, is a bit clumsy to duplicate. Repetitive code is almost always bad and trying to mimic the semantics of switch-cases for non-simple examples (including the above one) inevitably requires some repetition. Using dictionaries is simply a semantic mess. Creating a list and coming up with function names is really too much trouble for the simple task at hand. List comprehensions and dictionary comprehensions are powerful tools, but they simply have no place in something like a switch-case statement.

The Real Problem

This is one of the cases (pun unintended) where though you can use existing language features to get what you want (or something close), you would really like to have in-built language support. Python is a pretty well designed language as far as languages go, but it has it’s share of quirks. Personally I don’t consider a lack of switch case a particularly damaging lack, though there have been times where I wished there was one. In fact, a little Googling shows that there was a Python Enhancement Proposal submitted a few years ago but it was rejected due to lack of popular support.

There is an excellent Stack Overflow question on what the switch/case substitutes are and how to choose between them. The first answer to that question captures the essence of the problem at hand. It’s not a matter of how the alternative should be implemented, but rather what the alternative should mean. The if-else and dictionary lookups are generally useful if you have a simple choice to make in code which is mainly procedural. However if your code base is very object oriented, then you are probably better off with something like polymorphism to choose between alternatives. The solution should fit the problem, not the other way around.

The final thing I would like to say on this matter doesn’t involve Python at all. Rather it’s about Common Lisp, which I’ve been teaching myself for the last few weeks. The Lisp family of languages is particular famous (or infamous, depending on your point of view) because of the general lack of concrete syntax. Lisp code is written directly in the form of S-expressions. Essentially you write out the abstract syntax tree that in other languages is generated by the parsing the source code. Because the programmer has access to this representation, it’s possible to write code that will generate these S-expressions at compile time. In essence, this allows Lisp to be a programmable programming language. This is important because you as the programmer can basically add your own extensions to the language without waiting for a committee or community to approve it. In our case, if Common Lisp didn’t come with a switch-case statement and you really needed one, you could roll your own. In fact, it has been done. That’s not to say that rolling your language features is easy or something you should do on a daily basis, but in languages that allow it, it can be a very powerful tool if used right.

To be fair, I think you could write code to generate code in any run it in any language that has text processing and dynamic loading, but it would probably be very tedious and error-prone. The Lisp S-expression form lets you do it a much more elegant and powerful fashion.

In Conclusion

While Python does not have a switch case statement (and will probably never have one) there are a lot of other language features you can use to get the job done. It’s important to remember that you shouldn’t just be trying to recreate the semantics of switch-case (as that can be very messy). As the original post shows, trying to clone the C implementation is a futile endeavor. You need to pay attention to what the problem really is and then pick a Python feature that solves the problem correctly and elegantly. And if you get the chance, do explore languages like Lisp where syntax is fluid. It will help you better understand the difference between what your code looks like it’s doing and what it actually is doing. Happy hacking.

About these ads

13 thoughts on “Switch-case statement in Python revisited

  1. Pingback: Switch-case statement in Python « The ByteBaker

  2. Your third example won’t work – you need to put the function definitions BEFORE the dict. It’s the correct way to do things, but so verbose.

    If Python allowed true anonymous functions, then you could do it with a dict:

    x = None
    dict(
    foo=lambda: x = ‘this’,
    bar=lambda: x = ‘that’,
    ).get(name, error_function)();

    but of course this won’t work because you can’t have multiple statements nor make assignments from within a lambda…

  3. It’s nitpicking, but the python if statements start to look a lot nicer (for this example) when you use `in`. I also took the liberty of removing the extra ‘\n’ since it isn’t necessary with 2.x’s print statement:

    if n == 0:
        print "You typed zero."
    elif n == 2:
        print "n is an even number"
    elif n in (1, 9, 4)
        print "n is a perfect square"
    elif  n in (3, 5, 7):
        print "n is a prime number"
    

    I find that a lot less repetitive and semantically cleaner for this example, since you’re only using the fall-throughs to group different numbers. For something that uses them more cleverly, like duffs device, your point still stands.

    • I like jmoiron’s point. His “elif” construction with “in” is pretty darn close both in appearance and functionality to a switch-case statement and will serve well in many cases.

  4. Pingback: Powerful Python « The ByteBaker

  5. Pingback: Revamping the ByteBaker series « The ByteBaker

  6. It’s also easy to put in the fallthrough behaviour, as well as more syntactically clear. Just get rid of the “el” of elif

    if n==0:
    print “You typed 0″
    if n in (0,2,4,6,8):
    print “n is even”
    if n in (1,3,5,7,9):
    print “n is odd”
    if n in (1,4,9):
    print “n is a perfect square”
    if n in (1,8):
    print “n is a perfect cube”
    if n in (2,3,5,7):
    print “n is prime”
    if(not(n in range(0,9))):
    print “not a valid entry”

    I think this is much more clear than the (more convoluted) switch/case example as well, because you can immediately see all the inputs that give each result. For the case statement you have to look around for all the break; statements before you understand what’s going on.

    In general it’s nice to have switch/case, and I think it’s perfect for simple things where your cases are disjunct (I came here to see if python had something similar, in fact). But I don’t think I would ever use it for something with complex fall-throughs. It’s neat and makes you feel smart but it makes code hard to read.

    • I don’t see that as an attractive solution. Firstly, the script will check every single if statement, which if the code is performance critical or a large number of cases exist it will be slow. Secondly, it continues to fallthrough whereas in C fallthrough will stop once a break is encountered. Since there’s no way to break from a series of if statements the emulation of fallthrough behaviour is only partial (unless you also want even more hacks and impliment some kind of goto). Thridly, the default case actually relies on checking a condition has been met rather than falling back on a default if no condition has been met. jmoiron’s example gives a better approximation to fallthrough behaviour (a final else clause can be used for defaults).

      • Wrapping the entire thing in a single-execution loop (e.g. a while loop which sets its condition to false on the first line) makes it quite easy to break out from it, though that might not be very “Pythonic”. A little more planning re: mutually exclusive options and you could reduce the number of checks that have to get made every time the code is run. However, at the end of the day, if you are concerned about performance, do it in C.

  7. I wrote mine the way I did because it was way more appropriate to this specific example than anything else. Even the original true switch/case example in c from the post doesn’t properly solve the problem. It doesn’t say 0 is even, it has to have two “the number you typed is even” statements (one for 2 and one for 4,6,8). Also, it tells you you typed a number with more than 1 digit when you may have just typed a letter or something.

    The fundamental problem that we’re trying to solve here is that we have a set of cases, and some subsets of that set, where for each subset we want a certain action to be performed. Now if all the subsets that you have actions for are nested or disjoint, then switch/case with fallthroughs works perfectly:
    case 1:
    case 2:
    print(“your number was less than 3″);
    case 3:
    case 4:
    print(“your number was less than 5″);
    break;
    case 6:
    case 7:
    print(“your number was greater than 5″);
    break;
    default:
    print(“not a valid input”);

    in this code we have the set (1,2,3,4), which produces “your number was less than 5″, the nested set (1,2) which produces “your number was less than 3″, the disjoint set (6,7) which produces “your number was greater than 5″, and the default case, (aka the disjoint set including any other input) which produces “not a valid input”. This works beautifully with switch/case because disjoint sets are separated by break;s and nested sets fall through to the relevant supersets. Cool.

    In the original case, our subsets and actions are:
    0 – tell the user the input was 0
    0,2,4,6,8 – tell the user the input was even
    1,3,5,7,9 – tell the user the input was odd
    1,4,9 – tell the user the input was a square
    2,3,5,7 – tell the user the input was prime

    Now the subsets aren’t all nested. The squares have evens and odds but none of the odds are even. Same with the primes. You can’t do this in a semantically appropriate way with switch/case. For example, you can’t have both case 2: and case 4: fall through to the same message about evens, because case 2: has to fall through to a message about primes but can’t fall through to a message about squares, and case 4: has to fall through to a message about squares but can’t fall through to a message about primes. Using switch/case in this case make for really messy code that’s hard to understand.

    Also, if you’re really concerned about the performance effect of checking a few more if statements, you shouldn’t be programming in python. Yes, there will be twice as many comparisons as there were in the c version (18 compared to 10), but I will happily trade that nanosecond or two at runtime to make a script easier to write and read and debug. You probably gain that time back from having less code to load into memory.

  8. #!/usr/bin/env python

    def case(*arg):
    return lambda x: ‘%d %s’ % (x, ‘ ‘.join(list(arg)))

    def fail(arg):
    def require(retval, check, msg):
    if not check:
    retval = msg if not retval else retval+’ and ‘+msg
    return retval

    retval = ”
    retval = require(retval, len(arg) == 1, ’1 char allowed’)
    retval = require(retval, ’0′ <= arg[0] <= '9', 'char must be digit')
    return retval

    switch = {
    0: case('zero'),
    1: case('square', 'cube', 'perfect', 'prime'),
    2: case('even', 'prime'),
    3: case('prime'),
    4: case('even', 'square'),
    5: case('prime'),
    6: case('even', 'perfect'),
    7: case('prime'),
    8: case('cube'),
    9: case('square'),
    }

    if __name__ == "__main__":
    from sys import argv
    argv.pop(0)
    for arg in argv:
    try:
    print ''.join([switch[value](value) for value in (int(arg),)])
    except:
    print arg, "is unacceptable because", fail(arg)

  9. What everyone seems to miss is that the original problem isn’t even well solved by a switch-case. A much better solution, in any language, would be:

    if i > 9: print(“Only single digit numbers are allowed!”)
    elif i == 0: print(“You typed zero.”)
    else:
    if i % 2 == 0: print(“i is an even number.”)
    else: print(“i is an odd number.”)
    if sqrt(i)**2 == i: print(“i is a perfect square!”)

    Take your 25 lines of crappy C code and shove it. I just wrote a far easier to read function in 6 lines.

  10. Pingback: Python: switch case | Harry Tran

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s