Aaron Sky
Aaron Sky

Reputation: 3

Python TypeError in traversing a list

I'm teaching myself Python 3.2 and I'm trying to make a program to match a list of names. pList is a multidimensional list with a string at column 0, an integer at column 1, and a boolean at column 2. However, whenever I try and call this function (which only runs if the number of rows in the list is even), I get a TypeError.

Traceback (most recent call last):
 File "C:\Users\METC\Dropbox\assassins.py", line 150, in <module>
main()
 File "C:\Users\METC\Dropbox\assassins.py", line 11, in main
update(ops, pList)
 File "C:\Users\METC\Dropbox\assassins.py", line 125, in update
b = match(pList)
 File "C:\Users\METC\Dropbox\assassins.py", line 47, in match
q, p = 0
TypeError: 'int' object is not iterable

Any help would be appreciated, but keep in mind I'm a beginner with the language, so be gentle. :) I don't mind if you get too technical though; I have experience with computer science.

def match(pList):
    b = []
    z = len(pList)-1
    for x in range(z):
        b.append([pList[x][0],0])
    for x in range(z):
        isValid = False
        q, p = 0
        while isValid == False:
            q = random.randint(0, z)
            print('q is ' + str(q))
            if q > z:
                isValid = False
            elif q < 0:
                isValid = False
            elif pList[q][1] == True:
                isValid = False
            else:
                isValid = True
        isMatch = False
        while isMatch == False:
            if pList[q][1] == False:
                isValid = False
                while isValid == False:
                    p = random.randint(0,z)
                    print('p is ' + str(p))
                    if p > z:
                        isValid = False
                    elif p < 0:
                        isValid = False
                    elif pList[p][2] == True:
                        isValid = False
                    else:
                        if q == p:
                            isValid = False
                        else:
                            isValid = True
                print('match valid')
                b[q][1] = pList[p][0]
                isMatch = True
    print('')
    return b

Upvotes: 0

Views: 482

Answers (2)

Karl Knechtel
Karl Knechtel

Reputation: 61509

You've made the logic far, far too complicated, to the point where I'm going to have to make several passes to get it down to size and show you what you're doing wrong.

First, we'll fix the actual reported error, as the others noted. At the same time, we'll apply a simple principle: don't compare to boolean literals. You don't say "if it is true that it is raining, I will need an umbrella". You say "if it is raining, I will need an umbrella". So cut out the extra stuff. if isValid is more clear than if isValid == True, as it highlights exactly what isValid is supposed to mean. I'm also going to take out the debug traces (print statements that are clearly only there to check if the code's doing the right thing; simplify the code first, and then there is less to check).

def match(pList):
    b = []
    z = len(pList)-1
    for x in range(z):
        b.append([pList[x][0],0])
    for x in range(z):
        isValid = False
        q = p = 0
        while not isValid:
            q = random.randint(0, z)
            if q > z:
                isValid = False
            elif q < 0:
                isValid = False
            elif pList[q][1]:
                isValid = False
            else:
                isValid = True
        isMatch = False
        while not isMatch:
            if not pList[q][1]:
                isValid = False
                while not isValid:
                    p = random.randint(0,z)
                    if p > z:
                        isValid = False
                    elif p < 0:
                        isValid = False
                    elif pList[p][2]:
                        isValid = False
                    else:
                        if q == p:
                            isValid = False
                        else:
                            isValid = True
                b[q][1] = pList[p][0]
                isMatch = True
    return b

Next, we're going to simplify our conditional logic. First off, the result returned from random.randint(0, z) cannot be < 0 nor > z, ever, no matter what. That's part of the very point of the function. So there is no point in writing code to handle those cases and in fact is wrong to do so. Writing code to handle something implies that it could actually happen. That's a distraction for the person reading the code, and a lie, at that. It puts extra space between the things that matter (the call to random.randint and the check against the pList value).

We're also going to simplify if/else pairs that simply set another boolean accordingly. For the same reason that you wouldn't write

if x == 1:
    y == 1
elif x == 2:
    y == 2
# ... etc. ad infinitum for every possible integer value of x

you shouldn't do the same with booleans, either. Finally, we can and should use logical and and or to connect boolean conditions.

def match(pList):
    b = []
    z = len(pList)-1
    for x in range(z):
        b.append([pList[x][0],0])
    for x in range(z):
        isValid = False
        q = p = 0
        while not isValid:
            q = random.randint(0, z)
            isValid = not pList[q][1]
        isMatch = False
        while not isMatch:
            if not pList[q][1]:
                isValid = False
                while not isValid:
                    p = random.randint(0,z)
                    isValid = not pList[p][2] and (q != p)
                b[q][1] = pList[p][0]
                isMatch = True
    return b

My next step will be to fix up the list indexing. Indexing into lists is usually not really what you want, and here in fact it introduces a bug. It's evident that you want to iterate over each "row" of pList; but range(z) gives you numbers from 0 to z-1 inclusive, so it's incorrect to subtract 1 from len(pList) in calculating z. For example, if pList has 5 elements, you will calculate z = 4, and produce range(z) = [0, 1, 2, 3]. You will never access pList[4], and b will only have 4 elements.

You are doing two things with z, fundamentally. One is to make a loop run as many times as there are "rows" in pList, and (in the first loop) do something with each "row". To do this

This is very important: range is not magic, and it has no special connection to for-loops. It's just a function that produces a list of numbers. In Python, a for loop gives you the elements directly. All this indexing nonsense is just that - nonsense, that is best left to less capable languages. If you want to do something with each element of a list, then do something with each element of the list, by writing code that loops over each element of the list. Directly. Not over some separate list of indices that you then use to index back into the original. That's going out of your way to make things complicated.

The second thing you do with z is to generate a random number that's a possible index, so that you can index into pList to get a random row. In other words, you just want to choose a random row. So just choose a random row. The random module provides this functionality directly: the function is called random.choice, and it does exactly what it sounds like.

There's one small hitch here: the original code compares p == q, i.e. compares the two randomly chosen list indices for equality. If we're no longer indexing, then we don't have indices to compare. To fix this, we need to understand what the original purpose was: to check whether the new chosen row is actually the old chosen row again. Again, we simplify by checking that directly: we pick the new row instead of a new index, and then see if it is the old one.

We also have the problem that we need to choose a corresponding row from b that corresponds to whichever row in pList we would previously have identified as q. To deal with that, we can select the b row at the same time as the pList row. This is a little tricky: my approach will be to make a list of pairs of rows - in each pair, there will be a row from b and then a row from pList. This doesn't require anything complicated - there is in fact a built-in function that will stitch b and pList together exactly as we want: it's called zip. Anyway, having chosen a row-pair from that list of row-pairs, we just have to unpack the two rows into two variables - using the q, p = ... syntax that you were mistakenly using in the first place, as it turns out. That's what it's for.

With these changes, we can actually get rid of p, q and z entirely. Which is nice, because it's not at all clear what those names were supposed to mean anyway.

def match(pList):
    b = []
    for row in pList:
        b.append([row[0], 0])
    for row in pList:
        isValid = False
        while not isValid:
            first_row, b_row = random.choice(zip(pList, b))
            isValid = not first_row[1]
        isMatch = False
        while not isMatch:
            if not first_row[1]:
                isValid = False
                while not isValid:
                    second_row = random.choice(pList)
                    isValid = not second_row[2] and (first_row is not second_row)
                b_row[1] = second_row[0]
                isMatch = True
    return b

Time for a little more logical cleanup. In the first while loop, we will keep looping until isValid becomes true. That is to say, until not first_row[1] becomes true. In the second while loop, first_row never gets changed, so, since not first_row[1] was true when the loop started, it will remain true the entire time. Therefore, that if-check is completely unnecessary.

Once that's gone, we find that the second while loop is actually also completely useless: it will loop while not isMatch, i.e. until isMatch. What is isMatch? Well, before we start the loop, it's False, and at the end of the loop, it's True. Always. So we know that this code will run exactly once. We enter the loop, go to the end, set isMatch to true, and then exit, since isMatch, having just been set to true, is true. Code that runs exactly once doesn't need a loop; it's just code.

Another thing I'll do here is to clean up the while isValid loops a little bit by converting them to just break out when we're done. break is not evil (and neither is continue). They actually simplify our thinking about the booleans, because we aren't checking against not isValid any more (emphasis on the not); we're just directly comparing to what we would have assigned to isValid. And this means we get rid of the isValid variable, too, which again was a name that doesn't actually tell us very much.

def match(pList):
    b = []
    for row in pList:
        b.append([row[0], 0])
    for row in pList:
        while True:
            first_row, b_row = random.choice(zip(pList, b))
            if not first_row[1]:
                break
        while True:
            second_row = random.choice(pList)
            if not second_row[2] and (first_row is not second_row):
                break
        b_row[1] = second_row[0]
    return b

One last thing: we can construct b much more cleanly. Building up a list by appending elements is a sucker's game. Don't tell Python how to build lists. It knows how. Instead, ask for a list that meets your specifications, with a list comprehension. This is more simply shown than explained (if you need an explanation, please consult Google), so I'll just go ahead and give you one more version:

def match(pList):
    b = [[row[0], 0] for row in pList]
    for row in pList:
        while True:
            first_row, b_row = random.choice(zip(pList, b))
            if not first_row[1]:
                break
        while True:
            second_row = random.choice(pList)
            if not second_row[2] and (first_row is not second_row):
                break
        b_row[1] = second_row[0]
    return b

From here on, it's hard to correct or improve anything without understanding what it is that you're actually doing. After all this work, I still have no idea!

The way you have it set up, you choose a random row, as many times as there are rows - but you could still choose duplicates. Is that what you really want? Or did you want to choose each row once in a random order? What's the significance of doing it in a random order, anyway?

After choosing the first row, you choose a random second row to match. Did you really want one random row per first row? Or did you want to try all possible pairs of rows?

And just what is all of this data, anyway? What does the output b data represent? What exactly is in pList to begin with, and why is it called pList? What are you "matching" with this match function? I honestly can't imagine.

Upvotes: 3

Keith
Keith

Reputation: 43024

It's your construct:

q, p = 0

It's trying to unpack the single int by iterating over it. That's not valid Python syntax. I suppose the error could be better.

Use:

q = p = 0

instead.

Upvotes: 5

Related Questions