Rather than using print()
in the grade()
function, return your result and have the caller print the resulting mark. The grade()
function should only be used to return a grade:
def grade(mark):
grds = ['First','Upper Second','Second','Third','F1 Supp.','F2','F3']
if mark >= 75.0:
return grds[0]
# .. etc
def finalmark():
mark = float(input("Enter your mark"))
fnlmark = grade(mark)
print("Your grade is", fnlmark)
finalmark()
Note that finalmark()
is responsible for printing now; that's the best place for it, as that same function also is responsible for printing the question on the screen and taking user input. Like your version, finalmark()
returns None
(because that's the default), and I removed the print()
from around the finalmark()
call to avoid printing that return value. There's no point in printing it, finalmark()
will never return anything other than None
.
You can also remove half of your tests; only the first matching if
or elif
branch is picked, the rest are skipped. So you can remove tests for what a previous branch already covered:
def grade(mark):
grds = ['First','Upper Second','Second','Third','F1 Supp.','F2','F3']
if mark >= 75.0:
return grds[0]
elif mark >= 70.0:
return grds[1]
elif mark >= 60.0:
return grds[2]
elif mark >= 50.0:
return grds[3]
elif mark >= 45.0:
return grds[4]
elif mark >= 40.0:
return grds[5]
else:
return grds[6]
If the first if mark >= 75.0:
test did not match, then there is no need to test for mark < 75.0
anymore, because we have tested for the inverse. Testing for mark >= 70.0
is enough for the next grade. If that fails to match, we know the mark is definitely smaller than 70, so the next test only needs to test if it is larger than 60.0
, etc.
Now a pattern emerges that you could build a loop on. You test for a lower bound, and if it matches, you know which index to return. Build a separate list to store the lower bounds:
def grade(mark):
grds = ['First','Upper Second','Second','Third','F1 Supp.','F2','F3']
bounds = [75.0, 70.0, 60.0, 50.0, 45.0, 40.0]
for grade, bound in zip(grds, bounds):
if mark >= bound:
return grade
# there is no lower bound for F3; if the loop didn't find a match,
# we end up here and can assume the lowest grade.
return grds[6]
I used the zip()
function here to pair up the grade names and the bounds, pairwise. You could also have used the enumerate()
function to generate an index along with each grade name, or a for index in range(len(grds)):
loop, but I find zip()
to work cleaner here.
Next, we can start being clever with the algorithm. The above still tests each grade, from high to low, one by one. That can take up to N steps, for N grades. That's a linear algorithm, it takes as many steps as there are inputs.
But the grades are sorted, so we could use bisection here; jump to the middle and see if the mark is lower or higher than the current bound. Then pick either half, and test again, until you find a best match. Bisection takes at most Log(N) steps. Python has a very fast implementation included; it assumes values in increasing order, so reverse the grades and boundaries:
import bisect
def grade(mark):
grds = ['F3', 'F2', 'F1 Supp.', 'Third', 'Second', 'Upper Second', 'First']
bounds = [40.0, 45.0, 50.0, 60.0, 70.0, 75.0]
return grds[bisect.bisect_right(bounds, mark)]
bisect.bisect_right()
bisects into bounds
to find the 'insertion point' for mark
, which will be to the right of the same value in the list. So 35.0
would be inserted at 0
, 50.0
at 3
(as it is equal or higher), 74.0
at 5
and anything at 75.0
or higher at 6
. And those happen to be the exact indices for the matching grades.