Deleting multiple elements from a list

Viewed 291823

Is it possible to delete multiple elements from a list at the same time? If I want to delete elements at index 0 and 2, and try something like del somelist[0], followed by del somelist[2], the second statement will actually delete somelist[3].

I suppose I could always delete the higher numbered elements first but I'm hoping there is a better way.

32 Answers

You can use enumerate and remove the values whose index matches the indices you want to remove:

indices = 0, 2
somelist = [i for j, i in enumerate(somelist) if j not in indices]

If you're deleting multiple non-adjacent items, then what you describe is the best way (and yes, be sure to start from the highest index).

If your items are adjacent, you can use the slice assignment syntax:

a[2:10] = []

As a specialisation of Greg's answer, you can even use extended slice syntax. eg. If you wanted to delete items 0 and 2:

>>> a= [0, 1, 2, 3, 4]
>>> del a[0:3:2]
>>> a
[1, 3, 4]

This doesn't cover any arbitrary selection, of course, but it can certainly work for deleting any two items.

As a function:

def multi_delete(list_, *args):
    indexes = sorted(list(args), reverse=True)
    for index in indexes:
        del list_[index]
    return list_

Runs in n log(n) time, which should make it the fastest correct solution yet.

So, you essentially want to delete multiple elements in one pass? In that case, the position of the next element to delete will be offset by however many were deleted previously.

Our goal is to delete all the vowels, which are precomputed to be indices 1, 4, and 7. Note that its important the to_delete indices are in ascending order, otherwise it won't work.

to_delete = [1, 4, 7]
target = list("hello world")
for offset, index in enumerate(to_delete):
  index -= offset
  del target[index]

It'd be a more complicated if you wanted to delete the elements in any order. IMO, sorting to_delete might be easier than figuring out when you should or shouldn't subtract from index.

You can use this logic:

my_list = ['word','yes','no','nice']

c=[b for i,b in enumerate(my_list) if not i in (0,2,3)]

print c

Another implementation of the idea of removing from the highest index.

for i in range(len(yourlist)-1, -1, -1):
    del yourlist(i)

You may want to simply use np.delete:

list_indices = [0, 2]
original_list = [0, 1, 2, 3]
new_list = np.delete(original_list, list_indices)

Output

array([1, 3])

Here, the first argument is the original list, the second is the index or a list of indices you want to delete.

There is a third argument which you can use in the case of having ndarrays: axis (0 for rows and 1 for columns in case of ndarrays).

You can do that way on a dict, not on a list. In a list elements are in sequence. In a dict they depend only on the index.

Simple code just to explain it by doing:

>>> lst = ['a','b','c']
>>> dct = {0: 'a', 1: 'b', 2:'c'}
>>> lst[0]
'a'
>>> dct[0]
'a'
>>> del lst[0]
>>> del dct[0]
>>> lst[0]
'b'
>>> dct[0]
Traceback (most recent call last):
  File "<pyshell#19>", line 1, in <module>
    dct[0]
KeyError: 0
>>> dct[1]
'b'
>>> lst[1]
'c'

A way to "convert" a list in a dict is:

>>> dct = {}
>>> for i in xrange(0,len(lst)): dct[i] = lst[i]

The inverse is:

lst = [dct[i] for i in sorted(dct.keys())] 

Anyway I think it's better to start deleting from the higher index as you said.

I can actually think of two ways to do it:

  1. slice the list like (this deletes the 1st,3rd and 8th elements)

    somelist = somelist[1:2]+somelist[3:7]+somelist[8:]

  2. do that in place, but one at a time:

    somelist.pop(2) somelist.pop(0)

some_list.remove(some_list[max(i, j)])

Avoids sorting cost and having to explicitly copy list.

None of the answers offered so far performs the deletion in place in O(n) on the length of the list for an arbitrary number of indices to delete, so here's my version:

def multi_delete(the_list, indices):
    assert type(indices) in {set, frozenset}, "indices must be a set or frozenset"
    offset = 0
    for i in range(len(the_list)):
        if i in indices:
            offset += 1
        elif offset:
            the_list[i - offset] = the_list[i]
    if offset:
        del the_list[-offset:]

# Example:
a = [0, 1, 2, 3, 4, 5, 6, 7]
multi_delete(a, {1, 2, 4, 6, 7})
print(a)  # prints [0, 3, 5]

I tested the suggested solutions with perfplot and found that NumPy's

np.delete(lst, remove_ids)

is the fastest solution if the list is longer than about 100 entries. Before that, all solutions are around 10^-5 seconds. The list comprehension seems simple enough then:

out = [item for i, item in enumerate(lst) if i not in remove_ids]

enter image description here


Code to reproduce the plot:

import perfplot
import random
import numpy as np
import copy


def setup(n):
    lst = list(range(n))
    random.shuffle(lst)
    # //10 = 10%
    remove_ids = random.sample(range(n), n // 10)
    return lst, remove_ids


def if_comprehension(lst, remove_ids):
    return [item for i, item in enumerate(lst) if i not in remove_ids]


def del_list_inplace(lst, remove_ids):
    out = copy.deepcopy(lst)
    for i in sorted(remove_ids, reverse=True):
        del out[i]
    return out


def del_list_numpy(lst, remove_ids):
    return np.delete(lst, remove_ids)


b = perfplot.bench(
    setup=setup,
    kernels=[if_comprehension, del_list_numpy, del_list_inplace],
    n_range=[2**k for k in range(20)],
)
b.save("out.png")
b.show()

How about one of these (I'm very new to Python, but they seem ok):

ocean_basin = ['a', 'Atlantic', 'Pacific', 'Indian', 'a', 'a', 'a']
for i in range(1, (ocean_basin.count('a') + 1)):
    ocean_basin.remove('a')
print(ocean_basin)

['Atlantic', 'Pacific', 'Indian']

ob = ['a', 'b', 4, 5,'Atlantic', 'Pacific', 'Indian', 'a', 'a', 4, 'a']
remove = ('a', 'b', 4, 5)
ob = [i for i in ob if i not in (remove)]
print(ob)

['Atlantic', 'Pacific', 'Indian']

I put it all together into a list_diff function that simply takes two lists as inputs and returns their difference, while preserving the original order of the first list.

def list_diff(list_a, list_b, verbose=False):

    # returns a difference of list_a and list_b,
    # preserving the original order, unlike set-based solutions

    # get indices of elements to be excluded from list_a
    excl_ind = [i for i, x in enumerate(list_a) if x in list_b]
    if verbose:
        print(excl_ind)

    # filter out the excluded indices, producing a new list 
    new_list = [i for i in list_a if list_a.index(i) not in excl_ind]
    if verbose:
        print(new_list)

    return(new_list)

Sample usage:

my_list = ['a', 'b', 'c', 'd', 'e', 'f', 'woof']
# index = [0, 3, 6]

# define excluded names list
excl_names_list = ['woof', 'c']

list_diff(my_list, excl_names_list)
>> ['a', 'b', 'd', 'e', 'f']

Use numpy.delete which is definitely faster (376 times, as shown later) than python lists.

First method (using numpy):

import numpy as np

arr = np.array([0,3,5,7])
# [0,3,5,7]
indexes = [0,3]
np.delete(arr, indexes)
# [3,5]

Second method (using a python list):

arr = [0,3,5,7]
# [0,3,5,7]
indexes = [0,3]
for index in sorted(indexes, reverse=True):
    del arr[index]
arr
# [3,5]

Code to benchmark the two methods on an array of 500000 elements, deleting half of the elements randomly:

import numpy as np
import random
import time

start = 0
stop = 500000
elements = np.arange(start,stop)
num_elements = len(temp)

temp = np.copy(elements)
temp2 = elements.tolist()

indexes = random.sample(range(0, num_elements), int(num_elements/2))

start_time = time.time()

temp = np.delete(temp, indexes)

end_time = time.time()
total_time = end_time - start_time
print("First method: ", total_time)

start_time = time.time()

for index in sorted(indexes, reverse=True):
    del temp2[index]

end_time = time.time()
total_time = end_time - start_time
print("Second method: ", total_time)

# First method:  0.04500985145568848
# Second method:  16.94180393218994

The first method is about 376 times faster than the second one.

You can use remove, too.

delete_from_somelist = []
for i in [int(0), int(2)]:
     delete_from_somelist.append(somelist[i])
for j in delete_from_somelist:
     newlist = somelist.remove(j)
Related