简体   繁体   English

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

[英]How can I simplify repetitive if-elif statements in my grading system function?

The goal is to build a program to convert scores from a '0 to 1' system to an 'F to A' system:目标是构建一个程序,将分数从“0 到 1”系统转换为“F 到 A”系统:

  • If score >= 0.9 would print 'A'如果score >= 0.9将打印 'A'
  • If score >= 0.8 would print 'B'如果score >= 0.8将打印 'B'
  • 0.7, C 0.7, C
  • 0.6, D 0.6, 日
  • And any value below that point, print F低于该点的任何值,打印 F

This is the way to build it and it works on the program, but it's somewhat repetitive:这是构建它的方法并且它适用于程序,但它有点重复:

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')

I would like to know if there is a way to build a function so that the compound statements wouldn't be as repetitive.我想知道是否有办法构建一个 function,这样复合语句就不会那么重复了。

I'm a total beginner, but would something in the lines of:我是一个完全的初学者,但会是这样的:

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

be possible?可能吗?

The intention here is that later we can call it by only passing the scr, numbergrade and letter grade as arguments:这里的目的是稍后我们可以通过只传递 scr、numbergrade 和 letter grade 作为 arguments 来调用它:

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')

If it would be possible to pass fewer arguments, it would be even better.如果能少传arguments就更好了。

You can use the bisect module to do a numeric table lookup:您可以使用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']

You can do something along these lines:您可以按照以下方式做一些事情:

# 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'

This uses next with a default argument on a generator over the score-grade pairs created by zip .这将next与生成器上的默认参数一起用于zip创建的分数-等级对。 It is virtually the exact equivalent of your loop approach.它实际上与您的循环方法完全相同。

You could assign each grade a threshold value:您可以为每个等级分配一个阈值:

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"

In this specific case you don't need external modules or generators.在这种特定情况下,您不需要外部模块或生成器。 Some basic math is enough (and faster)!一些基本的数学就足够了(而且更快)!

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"

You could use numpy.searchsorted , which additionally gives you this nice option of processing multiple scores in a single call:您可以使用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']

You can use np.select from numpy library for multiple conditions:您可以将np.select库中的 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')

I've got a simple idea to solve this:我有一个简单的想法来解决这个问题:

def convert_grade(numgrd):
    number = min(9, int(numgrd * 10))
    number = number if number >= 6 else 4
    return chr(74 - number)

Now,现在,

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

You provided a simple case.你提供了一个简单的案例。 However if your logic is getting more complicated, you may need a rules engine to handle the chaos.但是,如果您的逻辑变得越来越复杂,您可能需要一个规则引擎来处理混乱。

You can try Sauron Rule engine or find some Python rules engines from PYPI.您可以尝试Sauron 规则引擎或从 PYPI 找到一些 Python 规则引擎。

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

I am not adding much to the party, except for some timing on the most noteworthy solutions:除了最值得注意的解决方案的一些时间安排外,我并没有为聚会添加太多内容:

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]
  • using basic algebra -- although this does not generalize to arbitrary breakpoints, while the above do (based on @RiccardoBucco's answer ):使用基本代数——尽管这不能概括为任意断点,而上面的是(基于@RiccardoBucco 的回答):
def grade_alg(score, grades="ABCDF"):
    return grades[-max(int(score * 10) - 5, 0) - 1]
  • using a chain of if - elif - else (essentially, OP's approach, which also does not generalize):使用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"

They all give the same result:他们都给出相同的结果:

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']

with the following timings (on n = 100000 repetitions into a list ):具有以下计时(在n = 100000次重复到list ):

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

indicating that the OP's approach is the fastest by far, and, among those that can be generalized to arbitrary thresholds, the bisect -based approach is the fastest in the current settings.表明 OP 的方法是迄今为止最快的,并且在那些可以推广到任意阈值的方法中,基于bisect的方法是当前设置中最快的。


As a function of the number of thresholds作为function的门槛数

Given that the linear search should be faster than binary search for very small inputs, it is interesting both to see where is the break-even point and to confirm that this application of binary search grows sub-linearly (logarithmically).考虑到对于非常小的输入,线性搜索应该比二分搜索更快,查看盈亏平衡点在哪里以及确认二分搜索的这种应用呈次线性(对数)增长是很有趣的。

To do so, a benchmark as a function of the number of thresholds is provided (excluding thos:为此,提供了阈值数量的基准 function(不包括:

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")

which can be plotted with the following:可以用以下内容绘制:

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')

to produce:生产:

bm

suggesting that the break-even point is around 5 , also confirming the linear growth of grade_gen() and grade_enu() , and the sub-linear growth of grade_bis() .表明盈亏平衡点在5左右,也证实了grade_gen()grade_enu()的线性增长,以及grade_bis() () 的次线性增长。


NumPy-based approaches基于 NumPy 的方法

Approaches that are capable of working with NumPy should be evaluated separately, as they take different inputs and are capable of processing arrays in a vectorized fashion.能够处理 NumPy 的方法应该单独评估,因为它们采用不同的输入并且能够以矢量化方式处理 arrays。

You could also use a recursive approach:您还可以使用递归方法:

grade_mapping = list(zip((0.9, 0.8, 0.7, 0.6, 0), 'ABCDF'))
def get_grade(score, index = 0):
    if score >= grade_mapping[index][0]:
        return(grade_mapping[index][1])
    else:
        return(get_grade(score, index = index + 1))

>>> print([get_grade(score) for score in [0, 0.59, 0.6, 0.69, 0.79, 0.89, 0.9, 1]])
['F', 'F', 'D', 'D', 'C', 'B', 'A', 'A']

Here are some more succinct and less understandable approaches:这里有一些更简洁但更难理解的方法:

The first solution requires the use of the floor function from the math library.第一个解决方案需要使用math库中的地板 function。

from math import floor
def grade(mark):
    return ["D", "C", "B", "A"][min(floor(10 * mark - 6), 3)] if mark >= 0.6 else "F"

And if for some reason importing the math library is bothering you.如果由于某种原因导入math库困扰你。 You could use a work around for the floor function:您可以使用 function 楼层的变通方法:

def grade(mark):
    return ["D", "C", "B", "A"][min(int(10 * mark - 6) // 1, 3)] if mark >= 0.6 else "F"

These are a bit complicated and I would advice against using them unless you understand what is going on.这些有点复杂,除非您了解发生了什么,否则我建议不要使用它们。 They are specific solutions that take advantage of the fact that the increments in grades are 0.1 meaning that using an increment other than 0.1 would probably not work using this technique.它们是特定的解决方案,利用了等级增量为 0.1 的事实,这意味着使用 0.1 以外的增量可能无法使用此技术。 It also doesn't have an easy interface for mapping marks to grades.它也没有用于将标记映射到成绩的简单界面。 A more general solution such as the one by dawg using bisect is probably more appropriate or schwobaseggl's very clean solution.一个更通用的解决方案,例如 dawg 使用 bisect 的解决方案可能更合适,或者 schwobaseggl 的非常干净的解决方案。 I'm not really sure why I'm posting this answer but it's just an attempt at solving the problem without any libraries (I'm not trying to say that using libraries is bad) in one line demonstrating the versatile nature of python.我不确定我为什么要发布这个答案,但这只是一种尝试在没有任何库的情况下解决问题(我并不是想说使用库是不好的)在一行中展示 python 的多功能性。

You can use a dict.你可以使用字典。

Code代码

def grade(score):
    """Return a letter grade."""
    grades = {100: "A", 90: "A", 80: "B", 70: "C", 60: "D"}
    return grades.get((score // 10) * 10, "F")

Demo演示

[grade(scr) for scr in [100, 33, 95, 61, 77, 90, 89]]

# ['A', 'F', 'A', 'D', 'C', 'A', 'B']

If scores are actually between 0 and 1, first multiply 100, then lookup the score.如果分数实际上在 0 和 1 之间,则先乘以 100,然后查找分数。

Hope following might help:if scr >= 0.9:print('A')elif 0.9 > scr >= 0.8:print('B')elif 0.8 > scr >= 0.7:Print('C')elif 0.7 scr >= 0.6:print('D')else:print('F')希望以下内容可能有所帮助:if scr >= 0.9:print('A')elif 0.9 > scr >= 0.8:print('B')elif 0.8 > scr >= 0.7:Print('C')elif 0.7 scr >= 0.6:打印('D')其他:打印('F')

The goal is to build a program to convert scores from a '0 to 1' system to an 'F to A' system:目标是构建一个程序,将分数从“0 到 1”系统转换为“F 到 A”系统:

  • If score >= 0.9 would print 'A'如果score >= 0.9将打印 'A'
  • If score >= 0.8 would print 'B'如果score >= 0.8将打印 'B'
  • 0.7, C 0.7、C
  • 0.6, D 0.6,D
  • And any value below that point, print F低于该点的任何值,打印 F

This is the way to build it and it works on the program, but it's somewhat repetitive:这是构建它的方法,它适用于程序,但它有些重复:

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')

I would like to know if there is a way to build a function so that the compound statements wouldn't be as repetitive.我想知道是否有办法构建 function 以便复合语句不会重复。

I'm a total beginner, but would something in the lines of:我是一个完全的初学者,但会是这样的:

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

be possible?有可能吗?

The intention here is that later we can call it by only passing the scr, numbergrade and letter grade as arguments:这里的意图是稍后我们可以通过只传递 scr、numbergrade 和 letter grade 为 arguments 来调用它:

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')

If it would be possible to pass fewer arguments, it would be even better.如果可以通过更少的arguments,那就更好了。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM