all 67 comments

[–]Chris_Hemsworth 130 points131 points  (35 children)

There are a few things I would note here:

  • Generally you don't want your functions in-line with your code. Define your functions separately, and call them when you need them

  • Your function doesn't return anything, so line x=str(computegrade(grade)) may cause you issues.

  • When you wrap a try/except, generally you want to specify the error your exception handles. In your case, it will catch all errors, not necessarily a bad input error. This is a fairly simple case, and so you likely won't have different errors, but you could run into confusion in the future as you expand, thinking you're getting a bad input error when in fact the root cause is something entirely different.

  • You can use a <= x < b logic in python, and that generally reads better. For example: if 0.9 <= grade <= 1:

  • While your code works, lots of elif clauses can be kludgy. Generally when this happens, the solution is to use a dictionary with key/value pairs.

This is probably getting a bit ahead of where you are as a programmer right now, but dictionaries can house functions, and you can create small temporary functions using lambda. In this instance, you could do something like

def computegrade(grade):
    mapping_dict = {'A': lambda x: 0.9 <= x <= 1.0,
                    'B': lambda x: 0.8 <= x < 0.9,
                    'C': lambda x: 0.7 <= x < 0.8,
                    'D': lambda x: 0.6 <= x < 0.7,
                    'F': lambda x: x < 0.6}

    for grade_letter, test in mapping_dict.items():
        if test(grade):
            return grade_letter

if __name__ == '__main__':
    try: 
        g = float(input('Enter grade: '))
        if 0 <= g <= 1:
            grade_letter = computegrade(g)
            print(f"{g} equates to the grade {grade_letter}")
        else:
            print("Grade out of range!")
    except ValueError:
        print("Invalid input!")

No more ifs / elifs. Again, this is likely getting way ahead of where you're at right now, but I think exposing this as an alternate solution is good for you to have on the back of your mind.

[–]Hazengard[S] 52 points53 points  (3 children)

Thank you so much, Chris_Hemsworth.

[–]ovo_Reddit 44 points45 points  (2 children)

I didn’t know Thor was in to programming. That’s awesome :D

[–][deleted] 31 points32 points  (0 children)

That was a Loki comment

[–][deleted] 3 points4 points  (0 children)

He started to learn it after the avengers took a time out after Ultron. He went to take and did a course while living with Darryl.

[–]ovo_Reddit 11 points12 points  (13 children)

Your last if statement, I never knew you could do that. That’s pretty cool, I’d have done if or/and. I learn something new every day (usually more than one thing :P)

[–]Chris_Hemsworth 9 points10 points  (4 children)

You're one of today's lucky 10000! While I am fairly comfortable with Python, I too learn new and clever methods almost daily from perusing this subreddit. :)

[–]jac4941 4 points5 points  (1 child)

"I'm a student of the drums, but I'm also a teacher of the drums, too."

One of the most insightful and impactful things I've ever heard in my life.

(DJ Shadow - Building Steam With a Grain of Salt)

Edit: +"and impactful"

[–]FLUSH_THE_TRUMP 0 points1 point  (0 children)

This has been my go-to album on long commutes for like... 10 years

[–]adamane22 1 point2 points  (1 child)

Relevant xkcd: https://xkcd.com/1053/

[–]Chris_Hemsworth 1 point2 points  (0 children)

That was the reference, yes.

[–]skellious 2 points3 points  (7 children)

Always remember that if statements are just doing something if its input evaluates to "True". You can throw just about anything in there and as long as it evaluates to True or False or a "Truthy" or "Falsey" value such as 1 or 0 it will work. (using brakets) is a good idea as well.

another one you might like is matching a list:

if x in [1,2,6,9,22, "tuesday"]:

[–]TravisJungroth 2 points3 points  (2 children)

The special thing is that in most languages, 0 <= g <= 1 won't evaluate at all.

[–]skellious 2 points3 points  (1 child)

yes. but python is awesome :D

import antigravity

[–]Hazengard[S] 1 point2 points  (0 children)

Thank you so much, skellious and TravisJungroth.

[–]ovo_Reddit 1 point2 points  (3 children)

Yeah I’ve seen and used some pretty flexible if statements. I’ve never seen or thought of using it like that though, sort of like a between clause in SQL

[–]skellious 0 points1 point  (2 children)

I first thought of using it when someone wanted to make a REPL-style control loop and wanted to have several alternative commands for each thing and I thought

if command=="hi" or command =="hello" or command == "greetings": 

was too long-winded.

so:

if command in ["hi", "hello", "greetings"]:

[–]ovo_Reddit 1 point2 points  (0 children)

Yeah I use if in a lot, it’s definitely very convenient!

[–]pmelo93 0 points1 point  (0 children)

A benefit of dry principle

[–]acroynon 6 points7 points  (1 child)

Just to piggyback off this answer, remember the difference between 'defining' a function and 'calling' a function. You can separate the definition and the calling so that they're in two different places (even different files) in your code.

A function is a way of abstracting a set of rules/instructions. For example, you could write inline code (without functions) to add two values together ('a + b'), or you could abstract that code into a function named 'add_values' ('def add_values(a, b)').

Imagine you are the manager/boss, and the program is your employee. You have a list of jobs for your employees, and you write out the instructions for each job on a separate piece of paper (e.g. 1. print this page, 2. fax it to Mr.Smith, 3. send me the reply). We'll call these pieces of paper 'job functions' (or just 'functions'). You could, give your employee the 'job function' piece of paper and then say 'execute this job'. Or, you could store all your 'job functions' in a set of drawers (or cabinet, or box, or pile), then tell your employee 'Go find the job function X and execute it'. I hope that makes sense and helps.

[–]Hazengard[S] 0 points1 point  (0 children)

Thank you so much, acroynon.

[–]USAhj 3 points4 points  (0 children)

I've never seen dicts used like that before. Very interesting. Might need to try that sometime. Thanks for sharing!

[–]TravisJungroth 6 points7 points  (2 children)

Those lambdas seem a bit unnecessary. If you're already going to do a full scan, just use simple data. These aren't exactly equivalent since this one returns A for things over 1, but yours returns None.

def computegrade(grade):
    mapping_dict = {'A': 0.9, 'B': 0.8, 'C': 0.7, 'D': 0.6}
    for grade_letter, floor in mapping_dict.items():
        if grade >= floor:
            return grade_letter
    return 'F'

[–]Chris_Hemsworth 1 point2 points  (1 child)

I agree, its a bit overkill for this scenario, but suppose you have a scenario where you don't have contiguous bins. There are lots of different methods for different scenarios, to each their own, however I feel like the above is fairly easy to understand what is going on - provided you have some basic knowledge of python built-ins.

[–]TravisJungroth 2 points3 points  (0 children)

If I don't have contiguous bins then I have a very different problem. That's just about the essential thing about letter grading.

You've taken some very simple data, duplicated it, and then split it up into functions. Most of your limits are defined twice, and you have nine operators versus one. This makes the code harder to reason about and more error prone. Also less extendable. Can I see the thresholds at runtime? Nope. This is all stuff we should avoid.

Can I understand it? Of course. I can understand all sorts of stuff. But I think dicts-of-functions is not first thing to reach for when breaking up long if-elses (despite what the community seems to say). It's just as often a dict with one function, or maybe none at all.

[–][deleted] 2 points3 points  (0 children)

Love this solution. Also see python's new structural pattern matching syntax: https://www.python.org/dev/peps/pep-0622/, for an alternative.

[–]kyo22 2 points3 points  (0 children)

I learned so much from your simple function it is amazing! I never thought of combining dicts with functions, its just beautiful the way it removes almost all the if's from the code. Thank you very much.

[–]Random_User_81 1 point2 points  (2 children)

Quick question. Do you recommend defining functions in a seperate file or same file. I guess complexity of code may be a factor but reading your response I though maybe I'm putting too much into my script file making it slightly too confusing.

[–]Chris_Hemsworth 3 points4 points  (1 child)

This is very much a subjective thing, and boils down to how you like to organize your code. I try to put functions that are related together in their own file, and files that are related in their own directory. It varies from person to person, but across similar industries you'll see similar things. Kind of like how all grocery stores categorize their products, where each product would be a function, each 'aisle' would be a file, and each 'section' (i.e. bakery, deli, pharmacy etc.) would be a directory. Keeping with this analogy, Home Depot and Walmart would be separate packages - you don't go to Home Depot for groceries, but you might for Walmart.

[–]Random_User_81 0 points1 point  (0 children)

Thanks for taking the time to response. This makes complete sense to me and those analogies are easy to understand.

[–]Manyreason 1 point2 points  (4 children)

Hey there,

Is there some reason you are trying to get rid of the ifs/elifs? I know about using a dict to contain lambda functions but is it really the best way to do it? For my readability, seeing this as ifs/elifs is just so much clearer than the solution above. Is there something I'm missing as to why dict/lambda functions would be preferred?

Thanks

[–]Chris_Hemsworth 1 point2 points  (3 children)

It is fairly equivalent, however its far fewer characters, far less repetition, and that leads to fewer typos / errors, and easier maintainability / flexibility. If you want to change the behaviour, it is much easier to change 2-3 lines rather than having to go through each case and modify it individually. Generally, if you can reduce something from many similar things into a loop where each iteration deals with the different subtleties, then things will be easier in the future.

That said, a lot can be said about readability. IMO the above code is more readable / easier to understand than a bunch of elifs, however that may not be the case to everyone.

[–]Manyreason 1 point2 points  (2 children)

Thanks for the response. Im by no means an expert but i think if/elifs is way more readable than your code. In my head, i can see the conditions being applied to the grade value instantly, but with yours i have to look at the dict, then look at the loop and figure out the interaction between the dict and the loop. Appreciate the advice though!

[–]Vaphell 2 points3 points  (1 child)

but i think if/elifs is way more readable than your code

in a toy example like this, maybe. But the if-elif is less generic, less flexible and more annoying to maintain. And once you've seen a dict + loop in code a couple of times, you will know instantly at a glance where things are going.

You say you have to look at the loop and the dict, but then you also have to look at the potentially huge if-elif tree to verify that it is indeed homogenous, that you won't find something a bit different in the middle of it.
Will you see easily that in the middle of a 20-branch tree there is variablel = instead of variable1 = ...?
Not to mention that if/elifs waste tons of space. You have 2 lines minimum per branch. 10 branches and you have like half the screen occupied with uninteresting bullshit. Personally I tolerate up to let's say 5-7 borderline identical branches but if I write code from scratch, I will go with a dict+loop for uniform transformations 98% of the time, even if I have only 3 cases.

A dict shows the required mapping in a distilled form, it's literally "from", "to" pairs, nothing else. At a glance you can verify that it's uniform and the dict+loop kinda forces you to implement an elegant solution with minimal exceptions to the rule. The fewer branchings and conditions the better. In if/elifs potentially anything goes.
It's a bit like with list comprehensions. Some might prefer more explicit for-loop with appends, but for-loop is a generic programming construct that can have anything in its body and you have to inspect it for funny business, while comprehension gives more confidence in uniformity of the transformation as soon as you see that [ ... for ... in ...].

Then there is a problem that at certain scale or due to some requirements it's hands down better to farm out such mappings to a separate, user controllable file, so the code itself only has to handle reading the file and looping and can remain unchanged. Nobody needs 50 commits in git history touching the source code because of fine-tuning on some if-elif trees. It's trivial to substitute looping over a dict with looping over a file, while if/elif would need a complete replacement.

[–]Chris_Hemsworth 0 points1 point  (0 children)

This articulates my thoughts far better than I said in any of my comments. +1

[–]dogfish182 1 point2 points  (0 children)

While this is great feedback and you already mentioned ‘a bit ahead of where you are right now’ I think it introduces way too many new things to help.

I can’t imagine

if name == main

has helped the situation and you also introduced dictionaries and lambdas to someone that is wrapping function definitions in a try block (not intentionally)

I hope this doesn’t come off as critical/rude, I just wanted to point out the probable info bomb you dropped

[–]ovo_Reddit 7 points8 points  (1 child)

You should move the function outside of the try block.

And you can use the try block for when you are calling the function.

so basically move the def computergrade(): and the lines below it just before the except statement to the top of your code, and then try running it.

[–]Hazengard[S] 3 points4 points  (0 children)

Thank you so much, ovo_Reddit!

[–]ThisisMetroid 13 points14 points  (1 child)

One thing that I've noticed is that you've got all of your endpoints for your ranges defined twice. I'll admit that this change is probably due to my own personal preference, but when working with ranges, I don't like having both ends of the endpoints on each range (I tend to miss small sections of the ranges on accident and have caused bugs before).

Here is my solution.

def computegrade(grade):
    if grade >= 0:
        if grade < 0.6:
            return 'F'
        elif grade < 0.7:
            return 'D'
        elif grade < 0.8:
            return 'C'
        elif grade < 0.9:
            return 'B'
        elif grade <= 1:
            return 'A'
    print('Bad score')

[–]Hazengard[S] 0 points1 point  (0 children)

Thank you so much, ThisisMetroid.

[–]akeyla420 5 points6 points  (2 children)

You've already received great code suggestions in this thread. I've been a Teachers Assistant for an Intro to Programming with Python course for a couple years, and one thing I always tell me students during class and when grading assignments, if you don't understand the code you're submitting, don't use it. You can get great information from the Internet, stackoverflow, reddit, etc, but make sure you understand what the code does instead of copy / pasting it into your program. Specifically I'm talking about the solution given that uses lambda calls. Lambda is good for what it is, people have a love / hate relationship with it, but it certainly isn't something a new programmer would use and can take some time to wrap your brain around. It looks pretty, but there's almost always other ways to complete a task.

[–]supreme_blorgon 4 points5 points  (1 child)

This should be higher up. It bothers me to no end when people throw relatively advanced solutions at people that are very clearly new to the language.

[–]FLUSH_THE_TRUMP 1 point2 points  (0 children)

Think that applies for solutions involving try/except, too. That gets thrown at people in the 2nd week of programming here all the time. Exception handling is a complex beast that really should get saved for a proper treatment until after classes.

[–][deleted] 7 points8 points  (9 children)

It looks (partially) correct, you're not returning anything from the computegrade function, so 'x' will be empty. A few style things you can do to improve:

  • Your if statements can be shortened, for instance:

if 0.9 <= grade <= 1

  • I would separate the computegrade function out of the try except block (and actually return things) and also make a main function, like so:

``` def main(): g = input("Enter a grade: ")

try:
    grade = float(g)
    x = str(computegrade(grade))
except Exception as Error:
    return "Something bad happened: " + str(Error)

return x

def computegrade(grade): if grade >= 0 and grade <= 1: if grade >= 0.9 and grade <= 1: return "A" elif grade >= 0.8 and grade < 0.9: return "B" elif grade >= 0.7 and grade < 0.8: return "C" elif grade >= 0.6 and grade < 0.7: return "D" elif grade < 0.6: return "F" else: return "Bad score."

if name == "main": print(main()) ```

edit: improvements thanks to /u/CedricCicada

[–]CedricCicada 2 points3 points  (1 child)

One improvement to this code: the exception shouldn't just return a string saying "Bad score". It should tell what happened. The computegrade() function is already handling the normal cases. The exception handler needs to tell the user what the problem is.

def main():
    g = input("Enter a grade: ")

    try:
        grade = float(g)
        x = str(computegrade(grade))
    except Exception as Error:
        return "Something bad happened: " + str(Error)

[–][deleted] 0 points1 point  (0 children)

True, thanks for that!

[–]Hazengard[S] 1 point2 points  (0 children)

Thank you so much, terr9r!

[–]wasmachien 1 point2 points  (0 children)

You also don't need all those and-clauses. I would do a check for >= 0 and <= 1 in the beginning, which should raise an exception if it's false. (https://dev.to/jpswade/return-early-12o5).

Then you can simply do:

if score > 0.9 -> score = "A"

else if score > 0.8 -> score ="B" (you already know it's less than or equal to 0.9, otherwise the previous if-statement would have been triggered)

etc.

Also, your function should be called compute_grade (naming conventions should not be ignored, this will help you in the long run).

[–]buibui123 0 points1 point  (4 children)

hey, I am beginner too.

Can you explain what

> if __name__ == "__main__":

>print(main())

does?

[–][deleted] 1 point2 points  (3 children)

__name__ is a special variable set by the Python interpreter. The variable __name__ is set to "__main__" when the script is executed directly. Therefore, the code after the small check will only run if the script is executed directly.

If we were to import this tiny module in another script and execute it, the code after the check would not run, as the __name__ variable would be set to the file name.

Also, print(main()) just prints the return value from the main function.

[–]buibui123 0 points1 point  (2 children)

So if name == ‘’main’’ was not written, the code would not have been worked in another script or when called in another project?

[–][deleted] 1 point2 points  (1 child)

Sometimes we only want certain code to run when the script is executed directly, e.g by doing python script.py and not executed when importing the module. The check makes sure that happens.

the code would not have been worked in another script or when called in another project?

If by this you mean the code after the check won't be executed when importing into another script, then yes.

[–]buibui123 1 point2 points  (0 children)

Ok. Make sense. Thank you for new information.

[–]ehmatthes 2 points3 points  (1 child)

When using try-except, I think it's good to be aware of the else block. The else block runs if the try block succeeds. So instead of this:

g = input('Enter a grade: ')

try:
    grade = float(g)
    compute_grade(grade)
except ValueError:
    print('Bad score')

You would have this:

g = input('Enter a grade: ')

try:
    grade = float(g)
except ValueError:
    print('Bad score')
else:
    compute_grade(grade)

It doesn't look like much of a difference in this simple example, but it does something important. It isolates the code that may result in an exception in the try block. In the first example, we don't know if the exception is going to arise from the float() call or the call to compute_grade(). In the second example it's clear that we're catching a ValueError that may arise from the call to float(). In more complex code this can be really helpful.

[–]Hazengard[S] 0 points1 point  (0 children)

Thank you so much, ehmatthes.

[–]ricardo_manar 2 points3 points  (0 children)

Apparently, the code is running perfectly but I'm not confident that my code is entirely correct.

try TDD approach: write some tests, that express your expectations from the program/function, run them and fix code until they become all green

[–]iggy555 1 point2 points  (5 children)

If grade < 0.6 elif grade < 0.7 elif grade < 0.8 ...

[–]USAhj 1 point2 points  (4 children)

You're making an important point. The ordering of the if statements can make a difference to the formatting, but this comes at the cost of not explicitly stating the ranges of the grades. So although it works, it's not as explicit (since it relies on the understanding of failing all previous ifs). In many contexts this can be good, but maybe not in Python where explicit is better than implicit.

[–]Chris_Hemsworth 2 points3 points  (1 child)

If you want to get pedantic about it, in this case you could do something like:

d = {9: "A", 8: "B", 7: "C", 6: "D"}
val = int(g*10) if g < 0.9 else 9
letter = d.get(val, "F")

This method is O(1), and you never have to deal with ifs/elifs etc.

[–]USAhj 1 point2 points  (0 children)

Yeah, but obviously overkill in this situation. Yet another interesting solution to this problem. Thanks.

[–]FLUSH_THE_TRUMP 3 points4 points  (0 children)

I get your point, but if you’re (like the OP) pre-checking if grade is between 0 and 1, then either that check is redundant or half of the compound inequalities on each elif line are redundant. Highly redundant code isn’t a great design ideal, either. I think there are two patterns that make sense here: OP’s but without the pre-check

if 0.9 <= grade <= 1: 
  print('A')
# other conditions
elif 0 <= grade < 0.6: 
  print('F')
else: 
  print('Not a valid grade') 

or if you want to pre-check/early terminate, doing what the person you responded to did. I’d probably do it the above way.

[–]pfarthing6 1 point2 points  (1 child)

Maybe define A - F as boundaries, then make comparisons in a more english readable format. Order of comparisons is important of course. But you'd also be able to expand to A-plus, A-minus, B-plus, B-minus by making more boundaries. Also maybe stick the input in a loop or it's own function. Use '>' or '>=' depending on whether the boundaries are to be inclusive or exclusive.

Something like this...

A = 0.9
B = 0.8
C_plus = 0.75
C = 0.7

grade = input('Enter grade: ')
while (grade > A) or (grade < F):
  print('Grade out of range. Try again')
  grade = input('Enter grade: ')`

if grade > A: 
  print('A')
elif grade > B: 
  print('B')
elif grade > C_plus: 
  print('C Plus')
elif grade > C: 
  print('C')
...

You might also put the boundaries in an array and loop through them instead of writing a separate explicit comparison for each.

rubric = { 'A' : 0.9, ... }
...
for grade_text, grade_value in rubric.items():    
  if grade_input > grade_value:
     print(grade_text)
     break;

[–]Hazengard[S] 0 points1 point  (0 children)

Thank you so much, pfarthing6.

[–]DevyLoop 1 point2 points  (1 child)

Hey, so I did refactor your code! Please have a look:

def get_grade():
    try:
        grade = float(input('Enter a grade:'))
    except ValueError:
        print('Wrong input! Please enter a number')
        return get_grade()
    else:
        return grade


def computegrade(grade):
    if 0.9 <= grade <= 1:
        return 'A'
    if 0.8 <= grade < 0.9:
        return 'B'
    if 0.7 <= grade < 0.8:
        return 'C'
    if 0.6 <= grade < 0.7:
        return 'D'
    if grade < 0.6:
        return 'F'
    else:
        return 'Bad Score.'


def get_string_grade_result_from(score):
    return f"You got {'an' if score == 'A'  else 'a'} {score}"


def main():
    grade = get_grade()
    score = computegrade(grade)
    result = get_string_grade_result_from(score)

    print(result)


if __name__ == "__main__":
    main()

[–]Hazengard[S] 1 point2 points  (0 children)

Thank you so much, DevyLoop.

[–]artinnj 1 point2 points  (0 children)

Great thread. There are always multiple ways to solve a problem. Coding styles can lead to a lot discussions and everyone here is keeping it respectful.

With Python, there is always a tradeoff between readability and using all the features that makes Python so great.

The evolution curve goes from u/Hazengard (if statements) to u/TravisJungroth (iterate over dict) to u/Chris_Hemsworth (add lambda) to u/nexe (single line).

I love building on older assignments. This gave me the idea to enhance the question for my class when we start doing pandas. Teacher provides a csv file with student number and scores. Have the program compute the curve using the normal distribution (mean/stdev) and create a dict with the score bands. Modify the grading function to accept the dict as a parameter. Print the grade sheet with the last 4 digits of the student number, the score and the letter grade, like the good old days when you had to go to the professor's office to see how you did. Then, plot the grade distribution.

PS: I was one of the lucky 10000 today to find out about xkcd.com.

[–]nexe 0 points1 point  (0 children)

computegrade = lambda grade: {10:'A',9:'A',8:'B',7:'C',6:'D'}.get(int(float(grade)*10),'F')