mlg4080
mlg4080

Reputation: 423

How to remove the last occurrence of an item from a list?

The list.remove() function serves to remove the first time an item appears in a list. Is there a built-in function to remove the last time? For instance if I have a list, say:

X = ['sf', 'cc', 'ch', 'sc', 'sh', 'ch']

and I want to remove the last 'ch' from the list, is there a better method than what I'm currently doing, which is:

X.reverse()
X.remove('ch')
X.reverse()

I will soon also have to worry about cases where the item being removed is potentially not in the list. So methods that do not throw errors in this case would be preferred.

Upvotes: 8

Views: 13850

Answers (8)

Faelin
Faelin

Reputation: 79

I'm surprised that no one has suggested

del X[len(X) - X[::-1].index('ch') - 1]
  1. X[::-1] to get a reversed list (without dealing with iterator type conversion)
  2. list.index() to get the index of the "first" occurrence
  3. Subtract from the total length of X - 1 to get the equivalent index in the unflipped list
  4. call del to move the object at that index.

While it isn't a builtin, it is at least a simple one-liner, which would be easily converted into a function or lambda:

rremove = lambda lst, obj : del lst[len(lst) - lst[::-1].index(obj) - 1]

As for the error, list.remove() will throw a ValueError if the target is not in the list, so that behavior is expected (use an if or try/except to prevent errors)

Upvotes: 0

EngiDev
EngiDev

Reputation: 41

Without reverse() and similar to one answer above:

def RightRemove(alist, x):
    for i in range(len(alist), 0, -1): # from end to begin
        if alist[i-1] == x: # element x exists
            alist.pop(i-1) # remove it
            break # return

Upvotes: 2

smarie
smarie

Reputation: 5256

Yet another one ..

def remove_last_occurrence_one_liner(lst, element):
    """
    Removes the last occurrence of a given element in a list (modifies list in-place).
    Raises same exception than lst.index(element) if element can not be found.
    """
    del lst[len(lst) - lst[::-1].index(element) - 1]

But it does not beat the for loop from abarnert

x = [random.choice(string.ascii_lowercase) for _ in range(10000)]
%timeit y = x[:]; f_rev_ex(y, 'a')
34.3 µs ± 219 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
%timeit y = x[:]; f_rev_if(y, 'a')
34.9 µs ± 195 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
%timeit y = x[:]; f_for(y, 'a')
26.9 µs ± 109 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
%timeit y = x[:]; f_enum(y, 'a')
699 µs ± 4.86 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit y = x[:]; remove_last_occurrence_one_liner(y, 'a')
49 µs ± 375 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

Upvotes: 0

Fabio Zadrozny
Fabio Zadrozny

Reputation: 25372

Yet another answer...

def remove_last_occurrence(lst, element):
    '''
    Removes the last occurrence of a given element in a list (modifies list in-place).

    :return bool:
        True if the element was found and False otherwise.
    '''
    for i, s in enumerate(reversed(lst)):
        if s == element:
            del lst[len(lst) - 1 - i]
            return True
    return False

Upvotes: 0

abarnert
abarnert

Reputation: 366103

There's really nothing wrong with your code at all. It works, it's clear why it works, it's hard to get wrong or misunderstand.

Yes, you could make it faster, but only by a constant factor. (Your algorithm does a two reverses, for N steps each, and one remove, which is N-1 steps, so O(N). And since your data aren't sorted or anything that would help us find a value faster, it's obvious that the ideal algorithm would also be O(N).) And at the cost of making it more complicated.

The obvious probably-faster way to do it is to just manually iterate from the end until we find a value, then delete that value. That also avoids having to deal with the ValueError. Using enumerate might help… but getting it right (without copying the whole thing) may be tricky.

So, let's compare these to your existing code, both wrapped it in a try/except, and in an if:

def f_rev_ex(xs, s):
    xs.reverse()
    try:
        xs.remove(s)
    except ValueError:
        pass
    xs.reverse()

def f_rev_if(xs, s):
    if s in xs:
        xs.reverse()
        xs.remove(s)
        xs.reverse()

def f_for(xs, s):
    for i in range(len(xs)-1, -1, -1):
        if s == xs[i]:
            del xs[i]
            break

def f_enum(xs, s):
    for i, x in reversed(list(enumerate(xs))):
        if x == s:
            del xs[i]
            break

For a list as tiny as yours, the test isn't even worth running, so I invented my own random data (in real life you have to know your data, of course):

In [58]: xs = [random.choice(string.ascii_lowercase) for _ in range(10000)]
In [59]: %timeit y = x[:]; f_rev_ex(y, 'a')
10000 loops, best of 3: 34.7 µs per loop
In [60]: %timeit y = x[:]; f_rev_if(y, 'a')
10000 loops, best of 3: 35.1 µs per loop
In [61]: %timeit y = x[:]; f_for(y, 'a')
10000 loops, best of 3: 26.6 µs per loop
In [62]: %timeit y = x[:]; f_enum(y, 'a')
1000 loops, best of 3: 604 µs per loop

Well, that last one wasn't a very good idea… but the other one is about 25% faster than what we started with. So we've saved a whole 9 microseconds, on data 4 orders of magnitude larger than your actual data. It's up to you whether that's worth the less-readable, easier-to-screw up code. (And I'm not going to show you my enumerate-based implementation without copying, because I got it wrong. :P)

Upvotes: 7

Paul Rooney
Paul Rooney

Reputation: 21619

Produce a reversed list, preserving the original indexes and remove the first instance you find.

X = ['sf', 'cc', 'ch', 'sc', 'sh', 'ch']

print X

for i, e in reversed(list(enumerate(X))):
    if e == 'ch':
        del X[i]
        break

print X

If it doesn't find the string it leaves the list untouched.

Upvotes: 2

skolsuper
skolsuper

Reputation: 639

if 'ch' in X:
    X.reverse()
    X.remove('ch')
    X.reverse()

The most pythonic way would be to do a try: except around remove:

X.reverse()
try:
    X.remove('ch')
except:
    pass
X.reverse()

As per your comment on speed, both of these methods are O(N), as x in list and list.reverse() are both O(N), so there's not much between them. If you expect the element to usually be there, you can save the x in list check by using try: catch, however if you expect it to usually not be there, you can save the 2 reverse()s by checking for membership first.

Upvotes: 9

michaelpri
michaelpri

Reputation: 3651

Well first you can check if the item is in the list using a if in statement. Then you can reverse the list and remove the element.

if "ch" in X:
    X.reverse()
    X.remove("ch")
    X.reverse()

Upvotes: 1

Related Questions