Using dictionaries rather than complex if-elif-else clauses

Lately, I’ve been using dictionaries as a dispatching mechanism. It seems especially elegant when I face some fairly elaborate switching logic.

For example, instead of:
if a == 1 and b == 1:
log("everything worked!")
commit()


elif a == 1 and b == 0:
log("a good, b bad")
report_that_b_failed()


else:
log("a failed")
report_that_a_failed()

Do this:


d = {
(1, 1): commit,
(1, 0): report_that_b_failed,
(0, 0): report_that_a_failed
}


k = (a, b)
f = d[k]
f()

This approach is also really useful when you face the need to change logic based on runtime values. You can imagine that d might be built after parsing an XML file.

29 thoughts on “Using dictionaries rather than complex if-elif-else clauses

  1. What happens if a = 0, b = 1?
    The two you compared are not exact, right?

    That said, I like your approach.

  2. Nice, but I’d shorten the call site to d[(a,b)](), especially if it were used often.

  3. ok, but use vars longer than one char and please do this instead of pointless temp vars.

    d[(a,b)]()

  4. Sound cool… 😉
    Anyway, I’ll add:

    if not f:
    default()
    else:
    f()

    just to emulate a default behavior, which should be the correct failover if d has not the given (a,b) among its key. Correct? (I’m a half-newbie, so I apologize if I wrote stupid things…)

  5. This might be a bit dense, but what happens in the second example when you hit upon a (0,1), because the else block from the first can possibly map to (0,0) and (0,1) in the second example (assuming that a and b can only be 0 or 1), or have you accounted for this fact and that (0,1) can’t be reached…

    Also you’d need to put a comment or two in there so anyone that maintains it isn’t going to hit their head against the desk after the first casual glance – they’d be very used to seeing ‘if’ conditional blocks, right?

    #In this dictionary the keys represent various states of a and b that we can encounter.
    d{…}

    #Check a and b, and if the key (a,b) is in d, call the function that maps to that key.
    f()

    Good idea to cut code bloat though!

  6. What happens when a == 0 and b == 1? The logic in the two cases are not equivalent.

  7. You will need all combinations in the dict, so this will quickly explode for increasing numbers of variables. You need (0,1): report_that_a_failed BTW
    Or I suppose you could remove (0,0) from the dict and then:

    f=d.get(k, report_that_a_failed)

  8. Don’t forget to use dict.get instead of dict[] so you can provide a catch-all case

    k = (a, b)
    f = d.get(k, report_case_not_found)
    f()

  9. There’s a runtime complexity price. imo, there’s a readability price too.

  10. This is terrible practice IMO. What if a==0 and b==1? Then your examples are not equivalent: you have to add another key to the dict or check if the key is in the dict or add a try/except wrapper around your dict lookup or use a default dict. The fact that there is so many ways to do it should clue you in that it breaks from Python philosophy (where there should only be one way).

    You’ve also lost your log statements preceding the report statements since you are limited to a single statement unless you wrap all your actions into functions or terse lambdas.

    I also don’t like debugging problems in execution path with data this way because it seperates the control structure from its result which makes looking up potential problems a bit of a pain — I promise you will know what I mean if you use this extensively.

    I think these are the kinds of shorcuts we end up paying for in bugs.

  11. That’s sort-of a weak version of pattern matching dispatch as in Haskell or Erlang. In Erlang that’s basically the way you do everything, because it’s idiomatic and easy.

  12. Mike, thanks for the comment! Yeah, ever since I read about common lisp multimethods and prolog/haskell/erlang pattern matching, I’ve been craving those features when I can’t have them.

    Anyhow, this approach does allow easy programmatic manipulation, which I don’t know how to do in erlang. In other words, based on some inputs, I can rearrange the dictionary so keys point to different functions. I don’t know how I would do that in prolog.

  13. This sort of thing gives me a pang of longing for switch/case syntax in Python. I know dictionaries, etc, can accomplish the same thing, but I’ve already trained my brain for case/switch stuff. Doing without it has always been a little awkward.

  14. Hi Aaron — dictionaries are better than case-switch statements because the dictionary can be built based on some other data. For example, I could construct a dictionary from database information, or user inputs, the position of the stars, etc.

    A case-switch statement is static. Once it is written, it can’t be rewritten programmatically.

    Thanks for the comment!

  15. No one has mentioned this yet, but what you have done above is called a dispatch table and is a pretty common computer science concept. See the Wikipedia article for more examples (I can’t seem to post the link to it).

  16. @Matt & Johan,

    I get the whole dispatch thing, and I agree that it’s darn handy to have. But I ALSO want the rinky-dink switch/case syntax for simple cases. That way I can keep things readable when the conditionals are fairly static and simple.

    If I end up needing more sophisticated dispatch stuff, then I bust out my dictionary to do heavy lifting, sacrificing readability for power.

  17. I do this all the time. Also, don’t forget that variable names and inline literals can clarify things sometimes. Consider:

    case=(a,b)
    action = {
    (1, 1): commit,
    (1, 0): report_that_b_failed,
    (0, 0): report_that_a_failed
    }[case]
    action()

    I often do this for subcommand dispatch in commandline programs; a side effect is an easy way to list the possible options :

    Commands = { ‘folders’: folders,
    ‘folder’: folder,
    ‘debug’: debug,
    ‘pick’: pick,
    ‘refile’: refile,
    }

    cmdfunc = Commands.get(cmd,None)
    if cmdfunc:
    try:
    cmdfunc(cmdargs)
    except UsageError:
    print cmdfunc.__doc__
    sys.exit(1)
    config.write()
    state.write()
    else:
    print “Unknown command %s. Valid ones: %s ” % (sys.argv[1], ‘, ‘.join(_sort(Commands.keys())))

    The above is a snippet of real code from a commandline mailreader I wrote and use.

  18. Great question.  In my code, that combo might raise a KeyError.  I suppose I could use .get() and If don't have a callable defined for that key, then I could raise a more particular error.

Comments are closed.