如何简化评分系统函数中重复的if-elif语句?

20
构建一个程序,将分数从“0到1”的系统转换为“F到A”的系统:
- 如果分数> = 0.9,则会打印“A” - 如果分数> = 0.8,则会打印“B” - 0.7级,C - 0.6级,D - 如果低于该点的任何值,则打印F
这是构建它的方法,并且在程序上可以正常工作,但有些重复。
if scr >= 0.9:
    print('A')
elif scr >= 0.8:
    print('B')
elif scr >= 0.7:
    print('C')
elif scr >= 0.6:
    print('D')
else:
    print('F')

我想知道是否有一种方法可以构建一个函数,使得复合语句不会那么重复。

我是一个完全的初学者,但是否有类似以下代码的解决方案:

def convertgrade(scr, numgrd, ltrgrd):
    if scr >= numgrd:
        return ltrgrd
    if scr < numgrd:
        return ltrgrd

可能吗?

这里的意图是以后我们可以仅通过传递scr、numbergrade和letter grade作为参数来调用它:

convertgrade(scr, 0.9, 'A')
convertgrade(scr, 0.8, 'B')
convertgrade(scr, 0.7, 'C')
convertgrade(scr, 0.6, 'D')
convertgrade(scr, 0.6, 'F')

如果能够传递更少的参数,那就更好了。


2
这个回答解决了你的问题吗?如何在Python中创建一个分级系统? - RoadRunner
14个回答

34
你可以使用模块进行数字表查找:bisect
from bisect import bisect 

def grade(score, breakpoints=(60, 70, 80, 90), grades='FDCBA'):
     i = bisect(breakpoints, score)
     return grades[i]

>>> [grade(score) for score in [33, 99, 77, 70, 89, 90, 100]]
['F', 'A', 'C', 'C', 'B', 'A', 'A']

2
我希望在使用“bisect”时能够额外增加+1,因为我发现它的使用太少了。 - norok2
4
@norok2 我认为一个只有4个元素的列表不是开始优化的地方。对于如此小的列表,线性扫描可能会更快。此外,使用带有可变默认参数而没有任何警告的情况下。 - user2390182
1
当然可以,而且考虑到问题的学习方面,我觉得它非常适合。 - norok2
2
这是来自 bisect 模块的示例。 - dawg
@schwobaseggl 即使对于这样小的列表,二分法也更快。在我的笔记本电脑上,二分法解决方案需要1.2微秒,而循环需要1.5微秒。 - Iftah
刚错过了5分钟的编辑窗口...编辑:我收回我的话:经过一些优化,我成功将循环方案处理时间降至1.18微秒。 - Iftah

12
您可以这样做:
# if used repeatedly, it's better to declare outside of function and reuse
# grades = list(zip('ABCD', (.9, .8, .7, .6)))

def grade(score):
    grades = zip('ABCD', (.9, .8, .7, .6))
    return next((grade for grade, limit in grades if score >= limit), 'F')

>>> grade(1)
'A'
>>> grade(0.85)
'B'
>>> grade(0.55)
'F'

这里使用了带有默认参数的 nextzip创建的得分-等级对生成器,几乎等同于您的循环方法。


6
你可以为每个等级分配一个阈值:
grades = {"A": 0.9, "B": 0.8, "C": 0.7, "D": 0.6, "E": 0.5}

def convert_grade(scr):
    for ltrgrd, numgrd in grades.items():
        if scr >= numgrd:
            return ltrgrd
    return "F"

2
请注意,如果您使用的是Python 3.6或更低版本,则应该执行sorted(grades.items()),因为字典不能保证排序。 - wjandrea
这在所有的Python版本中都不能可靠地工作。请注意,字典的顺序不能保证。此外,dict是一种不必要的重型数据结构,因为它是顺序而不是键进行查找的。 - user2390182
3
虽然它并不是最有效率的,但可以说它是最易读的,因为所有标记都靠近它们的阈值写出来。我更倾向于建议用一对元组代替字典。 - norok2
对于这个特定的任务,是的,一个元组列表比字典更好,但如果所有这些代码都要放在一个模块中,那么字典将允许您查找字母等级->阈值。 - wjandrea
1
如果需要的话,您需要交换键和值才能允许类似grades[int(score*10)/10.0]这样的操作,但是由于浮点数在字典键中表现不佳,因此应该使用Decimal - user2390182
1
你可以在字典中加入"F": 0.0来简化这个过程。 - wjandrea

5
在这种特定情况下,您不需要外部模块或生成器。一些基本的数学就足够了(而且更快)!
grades = ["A", "B", "C", "D", "F"]

def convert_score(score):
    return grades[-max(int(score * 10) - 5, 0) - 1]

# Examples:
print(convert_grade(0.61)) # "D"
print(convert_grade(0.37)) # "F"
print(convert_grade(0.94)) # "A"


2
你可以使用numpy.searchsorted,它还提供了在一次调用中处理多个分数的选项:
import numpy as np

grades = np.array(['F', 'D', 'C', 'B', 'A'])
thresholds = np.arange(0.6, 1, 0.1)

scores = np.array([0.75, 0.83, 0.34, 0.9])
grades[np.searchsorted(thresholds, scores)]  # output: ['C', 'B', 'F', 'A']

2
我有一个简单的想法来解决这个问题:
def convert_grade(numgrd):
    number = min(9, int(numgrd * 10))
    number = number if number >= 6 else 4
    return chr(74 - number)

现在,
print(convert_grade(.95))  # --> A 
print(convert_grade(.9))  # --> A
print(convert_grade(.4))  # --> F
print(convert_grade(.2))  # --> F

2
您可以使用 numpy 库中的 np.select 函数来处理多个条件:
>> x = np.array([0.9,0.8,0.7,0.6,0.5])

>> conditions  = [ x >= 0.9,  x >= 0.8, x >= 0.7, x >= 0.6]
>> choices     = ['A','B','C','D']

>> np.select(conditions, choices, default='F')
>> array(['A', 'B', 'C', 'D', 'F'], dtype='<U1')

1
你提供了一个简单的案例。但是,如果你的逻辑变得更加复杂,你可能需要一个规则引擎来处理混乱。
你可以尝试Sauron规则引擎或从PYPI找到一些Python规则引擎。

1

除了一些最显著的解决方案的时间安排外,我没有为这个聚会做出太多贡献:

import bisect


def grade_bis(score, thresholds=(0.9, 0.8, 0.7, 0.6), grades="ABCDF"):
    i = bisect.bisect(thresholds[::-1], score)
    return grades[-i - 1]
def grade_gen(score, thresholds=(0.9, 0.8, 0.7, 0.6), grades="ABCDF"):
    return next((
        grade
        for grade, threshold in zip(grades, thresholds)
        if score >= threshold), grades[-1])
def grade_enu(score, thresholds=(0.9, 0.8, 0.7, 0.6), grades="ABCDF"):
    for i, threshold in enumerate(thresholds):
        if score >= threshold:
            return grades[i]
    return grades[-1]
  • 使用基本代数 -- 虽然这不能推广到任意的断点,而上面的方法可以(参考@RiccardoBucco的答案):
def grade_alg(score, grades="ABCDF"):
    return grades[-max(int(score * 10) - 5, 0) - 1]

使用一系列的if-elif-else语句(本质上是OP的方法,也不具有普适性):
def grade_iff(score):
    if score >= 0.9:
        return "A"
    elif score >= 0.8:
        return "B"
    elif score >= 0.7:
        return "C"
    elif score >= 0.6:
        return "D"
    else:
        return "F"

它们都会给出相同的结果:

import random
random.seed(2)
scores = [round(random.random(), 2) for _ in range(10)]
print(scores)
# [0.96, 0.95, 0.06, 0.08, 0.84, 0.74, 0.67, 0.31, 0.61, 0.61]

funcs = grade_bis, grade_gen, grade_enu, grade_alg
for func in funcs:
    print(f"{func.__name__:>12}", list(map(func, scores)))
#    grade_bis ['A', 'A', 'F', 'F', 'B', 'C', 'D', 'F', 'D', 'D']
#    grade_gen ['A', 'A', 'F', 'F', 'B', 'C', 'D', 'F', 'D', 'D']
#    grade_enu ['A', 'A', 'F', 'F', 'B', 'C', 'D', 'F', 'D', 'D']
#    grade_alg ['A', 'A', 'F', 'F', 'B', 'C', 'D', 'F', 'D', 'D']
#    grade_iff ['A', 'A', 'F', 'F', 'B', 'C', 'D', 'F', 'D', 'D']

以下是计时情况(在重复n=100000次的情况下,将结果存储到一个列表中):
n = 100000
scores = [random.random() for _ in range(n)]
base = list(map(funcs[0], scores))
for func in funcs:
    res = list(map(func, scores))
    is_good = base == res
    print(f"{func.__name__:>12}  {is_good}  ", end="")
    %timeit -n 4 -r 4 list(map(func, scores))
#    grade_bis  True  4 loops, best of 4: 46.1 ms per loop
#    grade_gen  True  4 loops, best of 4: 96.6 ms per loop
#    grade_enu  True  4 loops, best of 4: 54.4 ms per loop
#    grade_alg  True  4 loops, best of 4: 47.3 ms per loop
#    grade_iff  True  4 loops, best of 4: 17.1 ms per loop

表明OP的方法迄今为止是最快的,而在可以推广到任意阈值的方法中,基于bisect的方法在当前设置中是最快的。


随着阈值数量的增加

考虑到对于非常小的输入,线性搜索应该比二分搜索更快,因此有趣的是看到何时达到了平衡点,并确认二分搜索的应用呈现出次线性增长(对数级别)。

为了做到这一点,提供了一个作为阈值数量函数的基准测试(不包括那些:

import string


n = 1000
m = len(string.ascii_uppercase)
scores = [random.random() for _ in range(n)]

timings = {}
for i in range(2, m + 1):
    breakpoints = [round(1 - x / i, 2) for x in range(1, i)]
    grades = string.ascii_uppercase[:i]
    print(grades)
    timings[i] = []
    base = [funcs[0](score, breakpoints, grades) for score in scores]
    for func in funcs[:-2]:
        res = [func(score, breakpoints, grades) for score in scores]
        is_good = base == res
        timed = %timeit -r 16 -n 16 -q -o [func(score, breakpoints, grades) for score in scores]
        timing = timed.best * 1e3
        timings[i].append(timing if is_good else None)
        print(f"{func.__name__:>24}  {is_good}  {timing:10.3f} ms")

可以使用以下方法绘制:

import pandas as pd
import matplotlib.pyplot as plt


df = pd.DataFrame(data=timings, index=[func.__name__ for func in funcs[:-2]]).transpose()
df.plot(marker='o', xlabel='Input size / #', ylabel='Best timing / µs', figsize=(6, 4))
fig = plt.gcf()
fig.patch.set_facecolor('white')

生产:

bm

表明盈亏平衡点约为5,也证实了grade_gen()grade_enu()的线性增长,以及grade_bis()的次线性增长。


基于NumPy的方法

能够使用NumPy处理的方法应该单独进行评估,因为它们接受不同的输入并能够以向量化的方式处理数组。


1
感谢您在这方面提供了一些数字! - StefOverflow

1
>>> grade = lambda score:'FFFFFFDCBAA'[int(score*100)//10]
>>> grade(0.8)
'B'

2
虽然这段代码可能回答了问题,但最好包含一些上下文,解释它的工作原理以及何时使用它。仅有代码的答案从长远来看并不有用。 - Mustafa

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接