vary the number of elif statements in python bet list length in python

88 views Asked by At

I'm trying to create a script that uses the if/elif/else structure, but I'd like it to expand and contract the elifs with the number of elements in a list.

The attempt below contains 5 elements (a-e), and gets sorted through five conditionals (if, elif, elif, elif, else).

Is there an elegant way to make this script work if I were to input the set (a-z), without needing to make several more elif?

Values = (0.1,0.12,0.3,0.8,0.802)
Categories = ("a","b","c","d","e")
MySizeType = []

for i in Values:
    if i < .2:
        MySizeType.append(Categories[0])
    elif i < .4:
        MySizeType.append(Categories[1])
    elif i < .6:
        MySizeType.append(Categories[2])
    elif i < .8:
        MySizeType.append(Categories[3])
    else:
        MySizeType.append(Categories[4])
5

There are 5 answers

5
larsks On

Expanding on my comment:

You can't dynamically build an if/elif structure, but you can accomplish the same logic with a loop and a dictionary that maps threshold values to categories. Something like this:

Values = (0.1, 0.12, 0.3, 0.8, 0.802)

# NB: Here we are taking advantage of the fact that in modern Python (3.7+),
# dictionaries preserve insert order. This was not the case for earlier
# versions of Python.
Categories = {
    0.2: "a",
    0.4: "b",
    0.6: "c",
    0.8: "d",
    "default": "e",
}

MySizeType = []

for i in Values:
    for threshold, category in Categories.items():
        if threshold == "default":
            continue

        if i < threshold:
            MySizeType.append(category)
            break
    else:
        MySizeType.append(Categories["default"])

print(MySizeType)

This will output...

['a', 'a', 'b', 'e', 'e']

...which appears correct for your sample data.


With a minor change to the data structures:

Categories = {
    0.2: "a",
    0.4: "b",
    0.6: "c",
    0.8: "d",
}
DefaultCategory = "e"

You could reduce that loop down to a single expression:

MySizeType = [
    next((cat[1] for cat in Categories.items() if v < cat[0]), DefaultCategory)
    for v in Values
]

@KellyBundy points out that my initial statement was not entirely accurate. Here's a solution in the spirit of their comment that uses Jinja2 to dynamically generate a chain if elsif statements:

import jinja2

Values = (0.1, 0.12, 0.3, 0.8, 0.802)
Categories = {
    0.2: "a",
    0.4: "b",
    0.6: "c",
    0.8: "d",
}
DefaultCategory = "e"
MySizeType = []

code = jinja2.Template(
    """
if False:
    pass
{% for threshold,category in categories.items() %}
elif i < {{threshold}}:
    MySizeType.append("{{category}}")
{% endfor %}
else:
    MySizeType.append(DefaultCategory)
"""
).render(categories=Categories)

blob = compile(code, "__generated__", "single")
for i in Values:
    exec(blob, globals(), locals())

print(MySizeType)

The output is the same as the earlier code.

For the record, don't do this.

1
Michael Cao On

In this case, you can calculate the category. The formula I used assumed numbers are in range [0, 1), but you can adjust the formula to accomodate for negative numbers or numbers greater than or equal to 1.

Values = (0.1,0.12,0.3,0.8,0.802)
Categories = ("a","b","c","d","e")
MySizeType = []

for i in Values:
    category = int(i // 0.2)
    MySizeType.append(Categories[category])
0
Daweo On

I suggest trying using bisect which is part of standard library. For given sorted list it finds index (position) where to put certain element so it would remain sorted. Consider following simple example

import bisect
category = ["a", "b", "c", "d"]
value = [0.1, 0.3, 0.9]
for i in [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]:
    pos = bisect.bisect_left(value, i)
    cat = category[pos]
    print("Element", i, "belongs to category", cat)

gives output

Element 0.0 belongs to category a
Element 0.1 belongs to category a
Element 0.2 belongs to category b
Element 0.3 belongs to category b
Element 0.4 belongs to category c
Element 0.5 belongs to category c
Element 0.6 belongs to category c
Element 0.7 belongs to category c
Element 0.8 belongs to category c
Element 0.9 belongs to category c
Element 1.0 belongs to category d

Observe that categories has one more element that value, as 3 cuts result in 4 parts. Keep in mind that value list must be sorted. Depending on how you would treat values equal to threshold you might choose between bisect.bisect_left and bisect.bisect_right.

1
tdelaney On

Categories is indexed by integer values. Assuming you know the distribution of valid values, you could normalize those values to the category index. If the value range is [0, 1), you could multiply by the length of the category array and its floor integer value is the index.

Values = (0.1,0.12,0.3,0.8,0.802)
Categories = ("a","b","c","d","e","f","g","h","i") # got bored...
MySizeType = [Categories[int(value * len(Categories))]
        for value in Values]
print(f"For Categories {Categories}:")
for v,s in zip(Values, MySizeType):
    print(f"{v} --> {s}")

Output

For Categories ('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'):
0.1 --> a
0.12 --> b
0.3 --> c
0.8 --> h
0.802 --> h
0
Alain T. On

To avoid the proliferation of elif you should place the thresholds of your categories in a list. This will allow you to find the index of the appropriate category by searching for the first threshold value that is larger or equal to each input value.

You can use the next() function to perform this search sequentially or, if you have a very large list of categories, a binary search (using bisect):

Values = (0.1, 0.12, 0.3, 0.8, 0.802)

Categories = ("a","b","c","d","e")
breaks     = [.2, .4, .6, .8, 1.0 ] # threshold values for category

# SEQUENTIAL SEARCH:

MySizeType = [ Categories[next(i for i,b in enumerate(breaks) if b>=v)]
               for v in Values ]

print(MySizeType)
['a', 'a', 'b', 'd', 'e']


# BINARY SEARCH:
                          
from bisect import bisect_right
MySizeType = [ Categories[bisect_right(breaks,v)] for v in Values ]

print(MySizeType)
['a', 'a', 'b', 'd', 'e']