Search code examples
pythonif-statementbuilt-in

Built in (remove) function not working with function variable


Have a good day everyone, pardon my lack of understanding, but I can't seem to figure out why python built in function does not work when being called with another function variable and it just doesn't do what I want at all. Here is the code

def ignoreten(h):
    ignoring = False
    for i in range (1,len(h)-2):
        if ignoring == True and h[i]==10:
            h.remove(10)
        if ignoring == False and h[i] ==10:
            ignoring = True

The basic idea of this is just to decided the first 10 in a list, keep it, continue iterating until you faced another 10, then just remove that 10 to avoid replication, I had searched around but can't seem to find any solution and that's why I have to bring it up here. Thank you


Solution

  • The code you listed

    def ignoreten(h):
        ignoring = False
        for i in range (1,len(h)-2):
            if ignoring == True and h[i]==10:
                h.remove(10)
            if ignoring == False and h[i] ==10:
                ignoring = True
    

    Will actually do almost the exact opposite of what you want. It'll iterate over h (sort of, see [1]), and if it finds 10 twice, it'll remove the first occurrence from the list. (And, if it finds 10 three times, it'll remove the first two occurrences from the list.)

    Note that list.remove will:

    Remove the first item from the list whose value is equal to x. It raises a ValueError if there is no such item.

    Also note that you're mutating the list you're iterating over, so there's some additional weirdness here which may be confusing you, depending on your input.

    From your follow-up comment to my question, it looks like you want to remove only the second occurrence of 10, not the first and not any subsequent occurrences.

    Here are a few ways:

    Iterate, store index, use del

    def ignoreten(h):
        index = None
        found_first = False
        for i,v in enumerate(h):
            if v == 10:
                if not found_first:
                    found_first = True
                else:
                    index = i
                    break
    
        if index is not None:
            del h[index]
    

    A little more verbose than necessary, but explicit, safe, and modifiable without much fear.

    Alternatively, you could delete inside the loop but you want to make sure you immediately break:

    def ignoreten(h):
        found_first = False
        for i,v in enumerate(h):
            if v == 10:
                if not found_first:
                    found_first = True
                else:
                    del h[i]
                    break
    

    Collect indices of 10s, remove second

    def ignoreten(h):
        indices = [i for (i,v) in enumerate(h) if v == 10]
        if len(indices) > 1:
            del h[indices[1]]  # The second index of 10 is at indices[1]
    

    Clean, but will unnecessarily iterate past the second 10 and collect as many indices of 10s are there are. Not likely a huge issue, but worth pointing out.


    Collect indices of 10s, remove second (v2, from comments)

    def ignoreten(h):
        indices = [i for (i,v) in enumerate(h) if v == 10]
        for i in reversed(indices[1:]):
            del h[i]
    

    From your comment asking about removing all non-initial occurrences of 10, if you're looking for in-place modification of h, then you almost definitely want something like this.

    The first line collects all the indices of 10 into a list.

    The second line is a bit tricky, but working inside-out it:

    • [1:] "throws out" the first element of that list (since you want to keep the first occurrence of 10)
    • reversed iterates over that list backwards
    • del h[i] removes the values at those indices.

    The reason we iterate backwards is because doing so won't invalidate the rest of our indices that we've yet to delete.

    In other words, if the list h was [1, 10, 2, 10, 3, 10], our indices list would be [1, 3, 5].

    In both cases we skip 1, fine.

    But if we iterate forwards, once we delete 3, and our list shrinks to 5 elements, when we go to delete 5 we get an IndexError.

    Even if we didn't go out of bounds to cause an IndexError, our elements would shift and we'd be deleting incorrect values.

    So instead, we iterate backwards over our indices, delete 5, the list shrinks to 5 elements, and index 3 is still valid (and still 10).


    With list.index

    def ignoreten(h):
        try:
            second_ten = h.index(10, h.index(10)+1)
            del h[second_ten]
        except ValueError:
            pass
    

    The inner .index call finds the first occurrence, the second uses the optional begin parameter to start searching after that. Wrapped in try/except in case there are less than two occurrences.


    ⇒ Personally, I'd prefer these in the opposite order of how they're listed.


    [1] You're iterating over a weird subset of the list with your arguments to range. You're skipping (not applying your "is 10" logic to) the first and last two elements this way.


    Bonus: Walrus abuse

    (don't do this)

    def ignoreten(h):
       x = 0
       return [v for v in h if v != 10 or (x := x + 1) != 1]
    

    (unlike the previous versions that operated on h in-place, this creates a new list without the second occurrence of 10)

    But the walrus operator is contentious enough already, please don't let this code out in the wild. Really.