Issue
So I'm working my way through Wentworth et al How to Think Like a Computer Scientist a Python 3 guidebook to try and teach myself more about programming. While it's a fantastic resource, it has very little to say about style and "best practice" for writing in Python 3.
I'm working through one of practice questions in the chapter on conditionals that asks me to write a function that returns a string 'grade' when an int or float 'mark' is inputted.
My direct question here is about the repetition in the conditionals in the function and the value the function returns. Is it possible to use a loop instead somehow to make it more concise instead of just writing elif
statements over and over? Also, the main grade
function is returning a null None
value; How I can make this function "fruitful" and not print None
when it's called?
Here's what I've written:
def grade(mark):
grds = ['First','Upper Second','Second','Third','F1 Supp.','F2','F3']
if mark >= 75.0:
print("Your grade is",grds[0])
elif mark < 75.0 and mark >= 70.0:
print("Your grade is",grds[1])
elif mark < 70.0 and mark >= 60.0:
print("Your grade is",grds[2])
elif mark < 60.0 and mark >= 50.0:
print("Your grade is",grds[3])
elif mark < 50.0 and mark >= 45.0:
print("Your grade is",grds[4])
elif mark < 45.0 and mark >= 40.0:
print("Your grade is",grds[5])
elif mark < 40.0:
print("Your grade is",grds[6])
def finalmark():
mark = float(input("Enter your mark"))
fnlmark = grade(mark)
return fnlmark
print(finalmark())
Solution
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.
Answered By - Martijn Pieters
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.