Home > Software design >  Refactor a if-elif-block into a pythonic dictionary
Refactor a if-elif-block into a pythonic dictionary

Time:12-29

I have a big if-elif-else block in my code like this

if counted == 2:
    score = None

elif counted == 1:
    score = (sum(values) - 3) / 6 * 100

elif counted == 0:
    score = (sum(values) - 4) / 8 * 100

else:
    raise Exception('Should not be reached!')

With python I assume there is a way to solve that with a dict using the counted values as keys. But what comes as items into that dict? The new case expression is not an option here. IMHO this wouldn't be pythonic no matter that it's now part of Python standard.

Approach A

# Approach A
mydict = {
    0: ((sum(values) - 4) / 8 * 100),
    1: ((sum(values) - 3) / 6 * 100),
    2: None
}

print(mydict)
print(mydict[counted])

The problem here is that all items are executed.

{0: 4625.0, 1: 6183.333333333334, 2: None}

Approach B

mydict = {
    0: (lambda _: (sum(values) - 4) / 8 * 100),
    1: (lambda _: (sum(values) - 3) / 6 * 100),
    2: (lambda _: None)
}

print(mydict)
print(mydict[counted](None))

Here I have the fake argument I don't need. But it seems to be mandatory when creating a lambda.

{0: <function <lambda> at 0x7ff8f3dce040>, 1: <function <lambda> at 0x7ff8f3b6a670>, 2: <function <lambda> at 0x7ff8f3b6a700>}

Is there a another way?

Full MWE

#!/usr/bin/env python3
import random

values = random.choices(range(100), k=10)
counted = random.choice(range(3))

print(f'counted={counted}')

if counted == 2:
    score = None

elif counted == 1:
    score = (sum(values) - 3) / 6 * 100

elif counted == 0:
    score = (sum(values) - 4) / 8 * 100

else:
    raise Exception('Should not be reached!')

print(f'score={score}')

# Approach A
mydict = {
    0: ((sum(values) - 4) / 8 * 100),
    1: ((sum(values) - 3) / 6 * 100),
    2: None
}

print(mydict)
print(mydict[counted])

# Approach B
mydict = {
    0: (lambda _: (sum(values) - 4) / 8 * 100),
    1: (lambda _: (sum(values) - 3) / 6 * 100),
    2: (lambda _: None)
}

print(mydict)
print(mydict[counted](None))

CodePudding user response:

First of all, you don't need any arguments for lambda:

mydict = {
    0: (lambda: (sum(values) - 4) / 8 * 100),
    1: (lambda: (sum(values) - 3) / 6 * 100),
    2: (lambda: None)
}

print(mydict)
print(mydict[counted]())

That being said, arguably the most Pythonic version would be version 0, if/else chain. It is most readable, calculates only what is necessary and doesn't overcomplicate anything.

CodePudding user response:

Relying on globals is usually a bad idea:

mydict = {
    0: (lambda values: (sum(values) - 4) / 8 * 100),
    1: (lambda values: (sum(values) - 3) / 6 * 100),
    2: (lambda _: None)
}

print(mydict)
print(mydict[counted](values))

CodePudding user response:

Using a dictionary with lambda values merely serves to obfuscate the code. Furthermore it's difficult to understand for lesser beings than the OP. It also doesn't perform very well.

Here's a consolidation of 3 possible approaches with output that demonstrates how they each perform:

from timeit import timeit

values = [1, 2, 3, 4,]

mydict = {
    0: (lambda _: (sum(_) - 4) / 8 * 100),
    1: (lambda _: (sum(_) - 3) / 6 * 100),
    2: (lambda _: None)
}

def v1(): # dictionary / lambda approach
    for i in range(3):
        score = mydict[i](values)
    return score

def v2(): # match / case approach
    for i in range(3):
        match i:
            case 0:
                score = (sum(values) - 4) / 8 * 100
            case 1:
                score = (sum(values) - 3) / 6 * 100
            case _:
                score = None
    return score

def v3(): # simple if/else
    for i in range(3):
        if i == 0:
            score = (sum(values) - 4) / 8 * 100
        elif i == 1:
            score = (sum(values) - 3) / 6 * 100
        else:
            score = None
    return score


for func in v1, v2, v3:
    print(func.__name__, timeit(func))

Output:

v1 0.5237932950003596
v2 0.4183016259994474
v3 0.40317739200327196

Conclusion:

Keep it simple and clear and you'll very likely gain in terms of performance and maintainability

CodePudding user response:

The lambdas without the arguments works. Just needs the :

import random


values = random.choices(range(100), k=10)

mydict = {
    0: lambda: (sum(values) - 4) / 8 * 100,
    1: lambda: (sum(values) - 3) / 6 * 100,
    2: lambda: None
}

counted = random.choice(range(3))
mydict[counted]()
  • Related