6

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 <=.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Kenny
  • 1,902
  • 6
  • 32
  • 61
  • Surely we must have a duplicate for these kinds of questions by now? Someone? – Aran-Fey Mar 22 '19 at 23:41
  • 2
    Python does not have a switch statement. Just use if,elif,else – juanpa.arrivillaga Mar 22 '19 at 23:42
  • 1
    Argh, of course this question here is also unsuited to be a good canonical... it just *had* to ask for a way to tell apart < and <=... – Aran-Fey Mar 22 '19 at 23:48
  • Related: *[Replacements for switch statement in Python?](https://stackoverflow.com/questions/60208/replacements-for-switch-statement-in-python/60211#60211)* [sic]. It also has the switch statement introduced with Python 3.10 (2021). – Peter Mortensen Oct 05 '21 at 11:24

7 Answers7

6

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.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Alain T.
  • 40,517
  • 4
  • 31
  • 51
5

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

wjandrea
  • 28,235
  • 9
  • 60
  • 81
Arun AK
  • 4,353
  • 2
  • 23
  • 46
3

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.

Olivier Melançon
  • 21,584
  • 4
  • 41
  • 73
2

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
blhsing
  • 91,368
  • 6
  • 71
  • 106
1

Yes, there is a strategy, but not quite as clean as human thought patterns. First some notes:

  • There are other questions dealing with "Python switch"; I'll assume that you consulted them already and eliminated those solutions from consideration.
  • The structure you posted is not a list; it's an invalid attempt at a dict. Keys must be hashable; the lists you give are not valid keys.
  • You have two separate types of comparison here: exact match to the lower bound and range containment.

That said, I'll keep the concept of a look-up table, but we'll drop it to a low common denominator to make it easy to understand and alter for other considerations.

low = [10, 30, 50, 90]
grade = "ABCDE"

for idx, bkpt in enumerate(low):
    if score <= bkpt:
        exact = (score == bkpt)
        break

cat = grade[idx]

exact is the flag you requested.

wovano
  • 4,543
  • 5
  • 22
  • 49
Prune
  • 76,765
  • 14
  • 60
  • 81
0

Python had (was added in python 3.10, hat-tip Peter Mortensen) no case statement. Just write a long series of elifs.

if score <= 10:
  return 'A'
if score <= 30:
  return 'B'
if score <= 50:
  return 'C'
if score <= 90:
  return 'D'
return 'E'

Stuff, like look it up in a dictionary, sounds great, but really, it's too slow. elifs beat them all.

Tatarize
  • 10,238
  • 4
  • 58
  • 64
  • 1
    A switch statement was introduced [with Python 3.10 (2021)](https://stackoverflow.com/questions/60208/replacements-for-switch-statement-in-python/60211#60211). – Peter Mortensen Oct 05 '21 at 11:09
-1
low = [10,30,50,70,90]
gradE = "FEDCBA"

def grade(score):
    for i,b in enumerate(low):
        #if score < b:   # 0--9F,10-29E,30-49D,50-69C,70-89B,90-100A Easy
        if score <= b:   # 0-10F,11-30E,31-50D,51-70C,71-90B,91-100A Taff
            return gradE[i]
    else:return gradE[-1]

for score in range(0,101):
    print(score,grade(score))