77

If I have a list of card suits in arbitrary order like so:

suits = ["h", "c", "d", "s"]

and I want to return a list without the 'c'

noclubs = ["h", "d", "s"]

is there a simple way to do this?

cs95
  • 379,657
  • 97
  • 704
  • 746
fox
  • 15,428
  • 20
  • 55
  • 85

9 Answers9

70
suits = ["h", "c", "d", "s"]

noclubs = [x for x in suits if x != "c"]
Vulwsztyn
  • 2,140
  • 1
  • 12
  • 20
avasal
  • 14,350
  • 4
  • 31
  • 47
  • 7
    I can't lie...I was hoping there would be something without a loop. This is much shorter and more intuitive in languages like R. – tumultous_rooster Dec 17 '13 at 09:57
  • I would avoid this solution as it seems to me to be quite unoptimized. Using .remove() is IMHO much faster. – Visgean Skeloru Mar 16 '14 at 23:39
  • 2
    @VisgeanSkeloru I would disagree that `.remove()` is "much faster". I posted an answer below to address that (it's hard to format code in the comment box). – alukach Nov 11 '14 at 22:33
  • Yeah you are right, my mistake, I did not take the time required to copy the list into consideration... I was only thinking about removing an element from a list not about the creation of the new list... Also I played with copy module and indeed it seems that [:] is a fastest way to copy... – Visgean Skeloru Nov 18 '14 at 21:40
  • 4
    A loop will always be used in some form eg. `remove` uses a loop although on `c` level. – jamylak Oct 12 '16 at 04:25
59
>>> suits = ["h", "c", "d", "s"]
>>> noclubs = list(suits)
>>> noclubs.remove("c")
>>> noclubs
['h', 'd', 's']

If you don't need a seperate noclubs

>>> suits = ["h", "c", "d", "s"]
>>> suits.remove("c")
Vulwsztyn
  • 2,140
  • 1
  • 12
  • 20
jamylak
  • 128,818
  • 30
  • 231
  • 230
  • 9
    It's important to note here that [`list.remove(x)`](https://docs.python.org/3/tutorial/datastructures.html) removes only *the first item from the list whose value is equal to x. It raises a `ValueError` if there is no such item.* The [list comprehension method](https://stackoverflow.com/a/15738712/5858851) removes all instances of x, and won't raise an error if the value does not exist. – pault May 15 '19 at 00:02
44

This question has been answered but I wanted to address the comment that using list comprehension is much slower than using .remove().

Some profiles from my machine (notebook using Python 3.6.9).

x = ['a', 'b', 'c', 'd']

%%timeit
y = x[:]  # fastest way to copy
y.remove('c')

1000000 loops, best of 3: 203 ns per loop

%%timeit
y = list(x)  # not as fast copy
y.remove('c')

1000000 loops, best of 3: 274 ns per loop

%%timeit
y = [n for n in x if n != 'c']  # list comprehension

1000000 loops, best of 3: 362 ns per loop

%%timeit
i = x.index('c')
y = x[:i] + x[i + 1:]

1000000 loops, best of 3: 375 ns per loop

If you use the fastest way to copy a list (which isn't very readable), you will be about 45% faster than using list comprehension. But if you copy the list by using the list() class (which is much more common and Pythonic), then you're going to be 25% slower than using list comprehension.

Really, it's all pretty fast. I think the argument could be made that .remove() is more readable than list a list comprehension technique, but it's not necessarily faster unless you're interested in giving up readability in the duplication.

The big advantage of list comprehension in this scenario is that it's much more succinct (i.e. if you had a function that was to remove an element from a given list for some reason, it could be done in 1 line, whilst the other method would require 3 lines.) There are times in which one-liners can be very handy (although they typically come at the cost of some readability). Additionally, using list comprehension excels in the case when you don't actually know if the element to be removed is actually in the list to begin with. While .remove() will throw a ValueError, list comprehension will operate as expected.

Max Ghenis
  • 14,783
  • 16
  • 84
  • 132
alukach
  • 5,921
  • 3
  • 39
  • 40
  • 14
    Also, please note that list comprehension solution will remove all "c'" characters while remove() will remove only the first one. – Kresimir Aug 18 '17 at 08:51
  • Re the performance of various ways to copy the list, please see [How do I clone a list so that it doesn't change unexpectedly after assignment?](https://stackoverflow.com/questions/2612802). As of 3.8, the built-in `.copy` method of lists, and the unpacking trick (which does essentially the same thing but without having to look up the method), are the fastest ways. The relative performance may change over time. – Karl Knechtel Mar 08 '23 at 19:25
9

you can use filter (or ifilter from itertools)

suits = ["h", "c", "d", "s"]
noclubs = filter(lambda i: i!='c', suits)

you can also filter using list construct

suits = ["h", "c", "d", "s"]
noclubs = [ i for i in suits if i!='c' ]
Vulwsztyn
  • 2,140
  • 1
  • 12
  • 20
Muayyad Alsadi
  • 1,506
  • 15
  • 23
  • ```noclubs = filter(lambda i: i!='c', suits)``` returns a filter object to me, not a list, it needs to be casted to list – fuomag9 Apr 25 '20 at 15:34
  • 1
    yes, in python3 you must cast it to list, in python2 it's returned as list directly. the question is from 2013. – Muayyad Alsadi Apr 28 '20 at 12:33
6

If order doesn't matter, a set operation can be used:

suits = ["h", "c", "d", "s"]
noclubs = list(set(suits) - set(["c"]))
# note no order guarantee, the following is the result here:
# noclubs -> ['h', 's', 'd']
5

Without using for loops or lambda functions and preserving order:

suits = ["h","c", "d", "s"]
noclubs = suits[:suits.index("c")]+suits[suits.index("c")+1:]

I'm aware that internally it'll still use loops, but at least you don't have to use them externally.

MSeifert
  • 145,886
  • 38
  • 333
  • 352
ierdna
  • 5,753
  • 7
  • 50
  • 84
  • 1
    This is a good method, but not as fast as copying the array with `suits[:]` then use `.remove()` to remove the element. – nowox Oct 28 '18 at 10:48
2

One possibility would be to use filter:

>>> import operator
>>> import functools

>>> suits = ["h", "c", "d", "s"]

>>> # Python 3.x
>>> list(filter(functools.partial(operator.ne, 'c'), suits))
['h', 'd', 's']

>>> # Python 2.x
>>> filter(functools.partial(operator.ne, 'c'), suits)
['h', 'd', 's']

Instead of the partial one could also use the __ne__ method of 'c' here:

>>> list(filter('c'.__ne__, suits))
['h', 'd', 's']

However, the latter approach is not considered very pythonic (normally you shouldn't use special methods - starting with double underscores - directly) and it could give weird results if the list contains mixed types but it could be a bit faster than the partial approach.

suits = ["h", "c", "d", "s"]*200   # more elements for more stable timings
%timeit list(filter('c'.__ne__, suits))
# 164 µs ± 5.98 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit list(filter(functools.partial(operator.ne, 'c'), suits))
# 337 µs ± 13.3 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit list(filter(lambda x: x != 'c', suits))
# 410 µs ± 13.7 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit [x for x in suits if x != "c"]
181 µs ± 465 ns per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Python 3.5.2 tested with IPythons magic %timeit command.

MSeifert
  • 145,886
  • 38
  • 333
  • 352
1

There doesn't seem to be anything like this built into Python by default unfortunately.

There are several answers but I though I'd add one using iterators. If changing in place is acceptable, that's going to be fastest. If you don't want to change the original and just want to loop over a filtered set, this should be pretty fast:

Implementation:

def without(iterable, remove_indices):
    """
    Returns an iterable for a collection or iterable, which returns all items except the specified indices.
    """
    if not hasattr(remove_indices, '__iter__'):
        remove_indices = {remove_indices}
    else:
        remove_indices = set(remove_indices)
    for k, item in enumerate(iterable):
        if k in remove_indices:
            continue
        yield item

Usage:

li = list(range(5))
without(li, 3)             
# <generator object without at 0x7f6343b7c150>
list(without(li, (0, 2)))  
# [1, 3, 4]
list(without(li, 3))       
# [0, 1, 2, 4]

So it is a generator - you'll need to call list or something to make it permanent.

If you only ever want to remove a single index, you can of course make it even faster by using k == remove_index instead of a set.

Mark
  • 18,730
  • 7
  • 107
  • 130
0

If it is important that you want to remove a specific element (as opposed to just filtering), you'll want something close to the following:

noclubs = [x for i, x in enumerate(suits) if i != suits.index('c')]

You may also consider using a set here to be more semantically correct if indeed your problem is related to playing cards.

MSeifert
  • 145,886
  • 38
  • 333
  • 352
Ceasar
  • 22,185
  • 15
  • 64
  • 83
  • 2
    Note that this answer only calculates the index of the **first** occurrence of `'c'` and does it for each element in the original list. So if the original contains multiple to-be-deleted `'c'` then the function won't work. And even if it only contains one it will be slow. Better to compare just the value `if x != 'c'` instead. – MSeifert Aug 06 '17 at 19:48
  • @MSeifert, I believe that was the point of my answer, to create a new list with a *specific* element removed rather than filtering everything that matches some predicate. I agree this could be more efficient though. – Ceasar Aug 07 '17 at 18:11