10

I am writing a basic program in Python that prompts the user to enter 5 test scores. The program will then convert each test score to a grade point (i.e. 4.0, 3.0, 2.0...), and then afterwards take the average of these numbers.

I've assigned each test score their own variable, and I'm feeding them into a for loop as follows:

for num in [score1, score2, score3, score4, score5]:
   if num >= 90
       print('Your test score is a 4.0')
   elif num < 90 and >= 80
   .
   .
   # and so on for each grade point.

Now, this does fine for displaying what each test score is equivalent to grade point wise. However, later in the function I need to calculate the average of each of these grade point values. So, I'd actually like to assign a grade point value to the specific variable passed through the for loop at that time. So, when score1 is passed through the for loop, and the appropriate grade point is determined, how can I actually assign that grade point to score1, and then later for score2 and so on as they are passed through the loop?

I hope that makes the question clear. It would seem silly that Python would not have this kind of capability, because if not you wouldn't be able to redefine any variable passed through a for loop if it is part of a list that is being passed through.


Not only are variables "in" the list (the list actually only stores values) updated, but the list isn't updated at all by this process. See Why doesn't assigning to the loop variable modify the original list? How can I assign back to the list in a loop? for that version of the question.

Karl Knechtel
  • 62,466
  • 11
  • 102
  • 153
Dustin Burns
  • 195
  • 2
  • 4
  • 9
  • 1
    *"It would seem silly that Python would not have this kind of capability, because if not you wouldn't be able to redefine any variable passed through a for loop if it is part of a list that is being passed through."* - That's how most programming languages work. To allow this capability would be bad because it would create something called side-effects, which make code obtuse. – ninjagecko Apr 01 '12 at 18:50

4 Answers4

19

"It would seem silly that Python would not have this kind of capability, because if not you wouldn't be able to redefine any variable passed through a for loop if it is part of a list that is being passed through." - That's how most programming languages work. To allow this capability would be bad because it would create something called side-effects, which make code obtuse.

Additionally this is a common programming pitfall because you should keep data out of variable names: see http://nedbatchelder.com/blog/201112/keep_data_out_of_your_variable_names.html (especially the list of similar questions; even if you aren't dealing with variables names, you are at least trying to deal with the variable namespace). The remedy is to work at "one level higher": a list or set in this case. This is why your original question is not reasonable. (Some versions of python will let you hack the locals() dictionary, but this is unsupported and undocumented behavior and very poor style.)


You can however force python to use side-effects like so:

scores = [99.1, 78.3, etc.]
for i,score in enumerate(scores):
    scores[i] = int(score)

the above will round scores down in the scores array. The right way to do this however (unless you are working with hundreds of millions of elements) is to recreate the scores array like so:

scores = [...]
roundedScores = [int(score) for score in scores]

If you have many things you want to do to a score:

scores = [..., ..., ...]

def processScores(scores):
    '''Grades on a curve, where top score = 100%'''
    theTopScore = max(scores)

    def processScore(score, topScore):
        return 100-topScore+score

    newScores = [processScore(s,theTopScore) for s in scores]
    return newScores

sidenote: If you're doing float calculations, you should from __future__ import division or use python3, or cast to float(...) explicitly.


If you really want to modify what is passed in, you can pass in a mutable object. The numbers you are passing in are instances of immutable objects, but if for example you had:

class Score(object):
    def __init__(self, points):
        self.points = points
    def __repr__(self):
        return 'Score({})'.format(self.points)

scores = [Score(i) for i in [99.1, 78.3, ...]]
for s in scores:
    s.points += 5  # adds 5 points to each score

This would still be a non-functional way to do things, and thus prone to all the issues that side-effects cause.

ninjagecko
  • 88,546
  • 24
  • 137
  • 145
  • 1
    I appreciate all the advice, and I think I get what you're saying about making the code obtuse. I find that interesting, I can see how that could cause some unexpected errors, though I'd be interested in hearing exactly why. On the other hand, I'm actually looking to redefine the variables instead of modifying them or causing global changes like a curve, which your post does a great job of outlining how to do. Thank you – Dustin Burns Apr 01 '12 at 19:26
  • 2
    @DustinBurns: side-effects cause unexpected errors because unless they are controlled side-effects, you lose the ability to modularize your code. Calling a function with a side-effect is bad because it does more than just 'return' a value. You thus cannot easily test functions with side-effects. Calling a function with side-effects twice may not return the same value; the output may depend on data that exists outside the function. It makes large amounts of code a mess. – ninjagecko Apr 01 '12 at 20:16
3

First rule: when dealing with a bunch of similar items, don't use a bunch of named variables - use an array (list, set, dictionary, whatever makes the most sense).

Second rule: unless you are really pressed for space, don't overwrite your variables this way - you are trying to make one label (the variable name) stand for two different things (raw mark and/or gpa). This makes debugging really nasty.

def get_marks():
    marks = []
    while True:
        inp = raw_input("Type in the next mark (just hit <Enter> to quit): ")
        try:
            marks.append(float(inp))
        except ValueError:
            return marks

def gpa(mark):
    if mark >= 90.0:
        return 4.0
    elif mark >= 80.0:
        return 3.0
    elif mark >= 70.0:
        return 2.0
    elif mark >= 60.0:
        return 1.0
    else:
        return 0.0

def average(ls):
    return sum(ls)/len(ls)

def main():
    marks = get_marks()
    grades = [gpa(mark) for mark in marks]

    print("Average mark is {}".format(average(marks)))
    print("Average grade is {}".format(average(grades)))

if __name__=="__main__":
    main()
Hugh Bothwell
  • 55,315
  • 8
  • 84
  • 99
  • I apologize, I am a bit new at this. Thank you so much for the advice. I'm curious, and I'll try looking it up myself, but when you def average(ls), where are you pulling that variable from. Does ls just mean all values from the predefined list? If so, that would make sense. Again, thank you for your help! – Dustin Burns Apr 01 '12 at 21:09
  • Oh, wait, I see what you did there. You called the function later in main and put ls into the defined functions so you wouldn't limit the variables you were able to push into the function. Cool. – Dustin Burns Apr 01 '12 at 21:11
2

The problem is that when you write this:

for num in [score1, score2, score3, score4, score5]:

what is happening is that you are creating a list which has five elements which are defined by the values of score1 through score 5 at the time you start iterating. Changing one of these elements does not change the original variable, because you've created a list containing copies of these values.

If you run the following script:

score1 = 2
score2 = 3

for num in [score1, score2]:
    num = 1

for num in [score1, score2]:
    print(num)

you'll see that changing each value in the list containing copies of your actual variables does not actually change the original variable's values. To get a better understanding of this, you might consider looking up the difference between "pass by reference" and "pass by value".

For this particular problem, I'd recommend placing the variables you want to be able to modify in a list to begin with, then iterating over that list rather than a list containing copies of those variables.

  • Yep. I understand that when the for loop starts and calls on those specific values, it's going to pull those values from outside of the for loop, so internal changes will have no effect when reinitiating the loop. For this reason, it would be easier to define the list outside of the loop, so then you could modify the list outside of the loop before you send the values in. I'm curious though, if you know of a way to modify the contents of a list, and then return those new values to the main function/body of code. That wouldn't be exactly what I'm looking to do, but I wonder if you can do it – Dustin Burns Apr 01 '12 at 19:47
2
# Take the list of grade as input, assume list is not empty
def convertGrade(myGrades):
    myResult = [] # List that store the new grade
    for grade in myGrades:
        gpa = (grade / 20) -1
        # Depending on how many deciaml you want
        gpa = round(gpa, 1)
        myResult.append(gpa)
    return myResult

# The list of grades, can be more than 5 if you want to
grades = [88.3, 93.6, 50.2, 70.2, 80.5]
convertedGrades = convertGrade(grades)
print(convertedGrades)

total = 0
# If you want the average of them
for grade in convertedGrades:
    total += grade # add each grade into the total

average = total / len(convertedGrades)
print('Average GPA is:', average)

I think this might do what you want, this sort of things is quite simple, so python would expect you to write it yourself, I don't know if you mean python should come with a GPA converter function, you can surely write one easily. If you want whole number only, (which I am not sure, since GPA usually comes with decimal point), then you can use int(), or round it to 0 before you append it.

output:

[3.4, 3.7, 1.5, 2.5, 3.0]
Average GPA is: 2.82
George
  • 4,514
  • 17
  • 54
  • 81
  • I was just wondering if you could put such a conversion directly into the for loop, instead of having to write another function to do a separate conversion. This option will work just fine, I was just curious what my limits were when writing a for loop. Thank you very much :) – Dustin Burns Apr 01 '12 at 19:28
  • I think, instead of doing that, I'll just convert the average to a GPA as you've listed above. So when I take the average of the list of scores, I'll just convert it to a GPA in the function that takes the average. – Dustin Burns Apr 01 '12 at 19:38
  • 1
    Yea, either way works. Sometimes it might be a good idea to break down everything. Depending your needs. – George Apr 01 '12 at 23:35