Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python efficient way of writing switch case with comparison

I normally implement switch/case for equal comparison using a dictionary.

dict = {0:'zero', 1:'one', 2:'two'};
a=1; res = dict[a]

instead of

if a == 0:
  res = 'zero'
elif a == 1:
  res = 'one'
elif a == 2:
  res = 'two'

Is there a strategy to implement similar approach for non-equal comparison?

if score <= 10:
  cat = 'A'
elif score > 10 and score <= 30:
  cat = 'B'
elif score > 30 and score <= 50:
  cat = 'C'
elif score > 50 and score <= 90:
  cat = 'D'
else:
  cat = 'E'

I know that may be tricky with the <, <=, >, >=, but is there any strategy to generalize that or generate automatic statements from let's say a list

{[10]:'A', [10,30]:'B', [30,50]:'C',[50,90]:'D',[90]:'E'}

and some flag to say if it's < or <=.

like image 773
Kenny Avatar asked Mar 22 '19 23:03

Kenny


4 Answers

A dictionary can hold a lot of values. If your ranges aren't too broad, you could make a dictionary that is similar to the one you had for the equality conditions by expanding each range programmatically:

from collections import defaultdict

ranges   = {(0,10):'A', (10,30):'B', (30,50):'C',(50,90):'D'}
valueMap = defaultdict(lambda:'E')
for r,letter in ranges.items():
    valueMap.update({ v:letter for v in range(*r) })

valueMap[701] # 'E'
valueMap[7] # 'A'

You could also just remove the redundant conditions from your if/elif statement and format it a little differently. That would almost look like a case statement:

if   score < 10 : cat = 'A'
elif score < 30 : cat = 'B'
elif score < 50 : cat = 'C'
elif score < 90 : cat = 'D'
else            : cat = 'E'

To avoid repeating score <, you could define a case function and use it with the value:

score = 43
case = lambda x: score < x
if   case(10): cat = "A"
elif case(30): cat = "B"
elif case(50): cat = "C"
elif case(90): cat = "D"
else         : cat = "E"
print (cat) # 'C'

You could generalize this by creating a switch function that returns a "case" function that applies to the test value with a generic comparison pattern:

def switch(value):
    def case(check,lessThan=None):
        if lessThan is not None:
            return (check is None or check <= value) and value < lessThan
        if type(value) == type(check): return value == check
        if isinstance(value,type(case)): return check(value)
        return value in check
    return case

This generic version allows all sorts of combinations:

score = 35
case = switch(score)
if   case(0,10)         : cat = "A"
elif case([10,11,12,13,14,15,16,17,18,19]):
                          cat = "B"
elif score < 30         : cat = "B"
elif case(30) \
  or case(range(31,50)) : cat = 'C'
elif case(50,90)        : cat = 'D'
else                    : cat = "E"
print(cat) # 'C'

And there is yet another way using a lambda function when all you need to do is return a value:

score = 41
case  = lambda x,v: v if score<x else None
cat   = case(10,'A') or case(20,'B') or case(30,'C') or case(50,'D') or 'E'
print(cat) # "D"

This last one can also be expressed using a list comprehension and a mapping table:

mapping = [(10,'A'),(30,'B'),(50,'C'),(90,'D')]
scoreCat = lambda s: next( (L for x,L in mapping if s<x),"E" )

score = 37
cat = scoreCat(score)
print(cat) #"D"

More specifically to the question, a generalized solution can be created using a setup function that returns a mapping function in accordance with your parameters:

def rangeMap(*breaks,inclusive=False):
    default = breaks[-1] if len(breaks)&1 else None
    breaks  = list(zip(breaks[::2],breaks[1::2]))
    def mapValueLT(value):
        return next( (tag for tag,bound in breaks if value<bound), default)
    def mapValueLE(value):
        return next( (tag for tag,bound in breaks if value<=bound), default)
    return mapValueLE if inclusive else mapValueLT

scoreToCategory = rangeMap('A',10,'B',30,'C',50,'D',90,'E')

print(scoreToCategory(53)) # D
print(scoreToCategory(30)) # C

scoreToCategoryLE = rangeMap('A',10,'B',30,'C',50,'D',90,'E',inclusive=True)

print(scoreToCategoryLE(30)) # B

Note that with a little more work you can improve the performance of the returned function using the bisect module.

like image 142
Alain T. Avatar answered Oct 21 '22 12:10

Alain T.


The bisect module can be used for such categorization problem. In particular, the documentation offers an example which solves a problem very similar to yours.

Here is the same example adapted to your use case. The function returns two values: the letter grade and a bool flag which indicates if the match was exact.

from bisect import bisect_left

grades = "ABCDE"
breakpoints = [10, 30, 50, 90, 100]

def grade(score):
          index = bisect_left(breakpoints, score)
          exact = score == breakpoints[index]
          grade = grades[index]
          return grade, exact

grade(10) # 'A', True
grade(15) # 'B', False

In the above, I assumed that your last breakpoint was 100 for E. If you truly do not want an upper bound, notice that you can replace 100 by math.inf to keep the code working.

like image 26
Olivier Melançon Avatar answered Oct 21 '22 12:10

Olivier Melançon


For your particular case an efficient approach to convert a score to a grade in O(1) time complexity would be to use 100 minus the score divided by 10 as a string index to obtain the letter grade:

def get_grade(score):
    return 'EDDDDCCBBAA'[(100 - score) // 10]

so that:

print(get_grade(100))
print(get_grade(91))
print(get_grade(90))
print(get_grade(50))
print(get_grade(30))
print(get_grade(10))
print(get_grade(0))

outputs:

E
E
D
C
B
A
A
like image 21
blhsing Avatar answered Oct 21 '22 12:10

blhsing


Python 3.10 introduced match case (basically switch) and you can use it as

def check_number(no):
  match no:
    case 0:
      return 'zero
    case 1:
      return 'one'
    case 2:
      return 'two'
    case _:
      return "Invalid num"

This is something that I tried for an example. enter image description here

like image 44
Thinker Avatar answered Oct 21 '22 11:10

Thinker