if / elif / else blocks -- a better way?

45 views
Skip to first unread message

Peter J. Farrell

unread,
Dec 12, 2013, 5:22:49 PM12/12/13
to pym...@googlegroups.com
Doing some code cleanup on a project and wondering if there is a better
way to accomplish blocks like this:

if s_score <= 39:
s_range = 'low'
elif 40 < s_score < 69:
s_range = 'moderate'
else:
s_range = 'high'

There is a bunch of blocks like this and this seems klunky. The ranges
change depending what we're evaluating so just building a staticmethod
isn't always going to work. The reason we do this is to avoid similar
logic in our Django views -- much easier to just output {% s_range %}
instead of multiple blocks. This code is part of a larger
get_responses() method that dumps back a dictionary of data including
some ranges and scores.

As you all know, I'm only a year into Python at work -- any kung-fu
greatly appreciated.

--
Peter J. Farrell
Principal Technologist - Maestro Publishing, LLC
http://blog.maestropublishing.com
Identi.ca / Twitter: @maestrofjp

* Learn about VSRE. I prioritize emails with VSRE in the subject! http://vsre.info/
* Please do not send me Microsoft Office/Apple iWork documents. Send OpenDocument instead! http://fsf.org/campaigns/opendocument/

Kyle Marek-Spartz

unread,
Dec 12, 2013, 8:20:39 PM12/12/13
to pym...@googlegroups.com
What you want is pattern matching! There is an easy way to similate this with dictionaries. I'll send more in a bit.
--
Meetings Schedule / RVSP on our Meetup at http://python.mn
---
You received this message because you are subscribed to the Google Groups "PyMNtos" group.
To unsubscribe from this group and stop receiving emails from it, send an email to pymntos+u...@googlegroups.com.
For more options, visit https://groups.google.com/groups/opt_out.


--

Kyle Marek-Spartz

Kyle Marek-Spartz

unread,
Dec 12, 2013, 8:37:25 PM12/12/13
to pym...@googlegroups.com, Peter J. Farrell
The quickest, and easiest to explain would be:

    score_to_range = {
        0: ‘low’,
        1: ‘moderate’,
        2: ‘high’
    }

    score_to_range[int((score - 39) / 69.0)]

or something like that.

To dispatch functions on the type of their first argument (singular dispatch), I’ve been using the following.

    from collections import OrderedDict

    class pattern(OrderedDict):  
        def __call__(self, *args, **kwargs):
            return self[type(args[0])](*args, **kwargs)

    more = pattern({
        int: lambda x: x + 1,
        str: lambda x: x + 's'
    })

    more(1) == 2
    more('noun') == 'nouns'
    more[int](1) == 2
    # more[int]('noun') raises TypeError

I find this a little more flexible than http://www.python.org/dev/peps/pep-0443/

These could be merged into the following to simplify:

    class score_to_range_builder(dict):
        def __call__(self, score):
            return self[int((score - 39) / 69.0)]

    score_to_range = score_to_range_builder().update({
        0: ‘low’,
        1: ‘moderate’,
        2: ‘high’
    })

    score_to_range(52) == ‘moderate’

Inheriting from OrderedDict isn’t necessary in this case, but it is useful for the pattern class because if we put basestring and str in as keys, the order would matter.

I’ll work on a blog post about this soon.

-- 
Kyle Marek-Spartz

Kyle Marek-Spartz

unread,
Dec 12, 2013, 9:37:21 PM12/12/13
to pym...@googlegroups.com, Peter J. Farrell

Matt Anderson

unread,
Dec 13, 2013, 12:48:29 AM12/13/13
to pym...@googlegroups.com
I think classically this sort of lookup algorithm (especially "at scale") would be handled by a balanced binary tree; the lookup of what label goes with what score would be O(log(n)) in the number of labels, instead of O(n) as it is with a straightforward traversal of all possible values in a if/else block.  Now, at a small scale, who cares about efficiency?  But, at a large scale (many labels), for many lookups, it could matter.

Trees aren't exactly build into the standard library, if the sort of lookup you want to do generally holds to this form, the bisect module can do this efficiently (and perhaps elegantly).  There's actually an example of something very like this in the standard library docs; see the grade example here:


Using a pair of lists like this is, algorithmically, equivalent to using a binary tree for lookup.

Mocking something up; code:

import bisect

def score(value):
    breakpoints = [39, 69]
    grades = ['low', 'moderate', 'high']
    return grades[bisect.bisect_left(breakpoints, value)]

for x in [30, 39, 40, 60, 69, 70, 100]:
    print x, score(x)

And the output:

>>> %run /tmp/b.py
30 low
39 low
40 moderate
60 moderate
69 moderate
70 high
100 high

Matt Anderson



David Radcliffe

unread,
Dec 12, 2013, 11:15:27 PM12/12/13
to pym...@googlegroups.com
Maybe define a helper function like this?

def cases(value, *lst):
    k = 0
    length = len(lst)
    while k < length:
        if k == length - 1:
            return lst[k]
        if value <= lst[k]:
            return lst[k+1]
        k += 2

for val in [10, 20, 30, 40, 50, 60, 70, 80]:
    print "The value %d is %s" % (
        val, cases(val, 39, 'low', 68, 'medium', 'high'))



Benjamin

unread,
Dec 13, 2013, 3:08:04 AM12/13/13
to pym...@googlegroups.com
Batteries included:
http://docs.python.org/2/library/bisect.html#other-examples (Same link Matt gave)

However, if you can't generalize to that particular degree, I'd stick with the if elif elif, etc. because they're easy to read and understand. When you come back to it in a year, are you going to want to decipher a custom subclass of OrderedDict?

Andres Moreno

unread,
Dec 13, 2013, 11:00:56 AM12/13/13
to pym...@googlegroups.com
Nothing clever to add to what folks have already said . BUT (you knew this was coming), it seems to me that you might benefit from having a dedicated section to map all your data to the values you will use in your program.

If this is the case and you have a bunch of transformations, I would code a simple general function (if that makes sense) and stick all transformation code into a section. The logic would be:

RAW DATA -> COOKED DATA --> program --> OUTPUT

Sometimes the struggle is with the data, not with the program per se.

Cheers! afm

Peter J. Farrell

unread,
Dec 13, 2013, 5:13:23 PM12/13/13
to pym...@googlegroups.com
Matt Anderson said the following on 12/12/2013 11:48 PM:
> Trees aren't exactly build into the standard library, if the sort of
> lookup you want to do generally holds to this form, the bisect module
> can do this efficiently (and perhaps elegantly). There's actually an
> example of something very like this in the standard library docs; see
> the grade example here:
>
> http://docs.python.org/2/library/bisect.html#other-examples
>
> Using a pair of lists like this is, algorithmically, equivalent to
> using a binary tree for lookup.
Thanks everybody for their ideas! All very different.Right now in
certain places, out code is mostly a single if/elif/else block but I can
foresee some situations in the future where this is going to get more
complex.

Of all the suggestions, the bisect code is the easiest / cleanest method
IMO (other than if/elif/else) both for implementation, readability and
flexibility. Glad you pointed that out @Matt -- I didn't know about
this "included battery" in Python. @Benjamin, from what I can tell --
bisect is the official binary tree algorithm in Python.

Cheers,
Peter

Kyle Marek-Spartz

unread,
Dec 13, 2013, 5:15:22 PM12/13/13
to pym...@googlegroups.com
Yeah, the singular dispatch stuff I suggested is more for resolving circular dependencies than for this use.
--
Meetings Schedule / RVSP on our Meetup at http://python.mn
---
You received this message because you are subscribed to the Google Groups "PyMNtos" group.
To unsubscribe from this group and stop receiving emails from it, send an email to pymntos+u...@googlegroups.com.
For more options, visit https://groups.google.com/groups/opt_out.


--

Kyle Marek-Spartz
Reply all
Reply to author
Forward
0 new messages