87

How can I break a list comprehension based on a condition, for instance when the number 412 is found?

Code:

numbers = [951, 402, 984, 651, 360, 69, 408, 319, 601, 485, 980, 507, 725, 547, 544,
           615, 83, 165, 141, 501, 263, 617, 865, 575, 219, 390, 984, 592, 236, 105, 942, 941,
           386, 462, 47, 418, 907, 344, 236, 375, 823, 566, 597, 978, 328, 615, 953, 345, 399,
           162, 758, 219, 918, 237, 412, 566, 826, 248, 866, 950, 626, 949, 687, 217, 815, 67,
           104, 58, 512, 24, 892, 894, 767, 553, 81, 379, 843, 831, 445, 742, 717, 958, 609, 842,
           451, 688, 753, 854, 685, 93, 857, 440, 380, 126, 721, 328, 753, 470, 743, 527]

even = [n for n in numbers if 0 == n % 2]

So functionally, it would be something you can infer this is supposed to do:

even = [n for n in numbers if 0 == n % 2 and break if n == 412]

I really prefer:

  • a one-liner
  • no other fancy libraries like itertools, "pure python" if possible (read: the solution should not use any import statement or similar)
codeforester
  • 39,467
  • 16
  • 112
  • 140
Flavius
  • 13,566
  • 13
  • 80
  • 126

9 Answers9

76

Use a function to raise StopIteration and list to catch it:

>>> def end_of_loop():
...     raise StopIteration
... 
>>> even = list(end_of_loop() if n == 412 else n for n in numbers if 0 == n % 2)
>>> print(even)
[402, 984, 360, 408, 980, 544, 390, 984, 592, 236, 942, 386, 462, 418, 344, 236, 566, 978, 328, 162, 758, 918]

For those complaining it is not a one-liner:

even = list(next(iter(())) if n == 412 else n for n in numbers if 0 == n % 2)

For those complaining it is hackish and should not be used in production code: Well, you're right. Definitely.

Reinstate Monica
  • 4,568
  • 1
  • 24
  • 35
  • This is not list comprehension, from my understanding, BUT it's an one-liner (I'll consider it as such, since the function is only used because of an unexplicable limitation in python for those `raise` statements there), and it doesn't seem to have any drawbacks. +1ed – Flavius Mar 05 '12 at 19:49
  • +0. Clever. But hackish and not a one-liner. – Steven Rumbalski Mar 05 '12 at 19:50
  • 2
    Is there a fix that is kosher in Python 3+? – Zuza Nov 07 '18 at 21:13
  • 4
    @Zuza A very similar example is included in the PEP's ["Examples of breakage"](https://www.python.org/dev/peps/pep-0479/#examples-of-breakage) section. You can find it at the end. A single line workaround will no longer be possible - you'll need to define a generator function. – Sherpa Feb 06 '19 at 22:47
  • 10
    `RuntimeError: generator raised StopIteration` – wim Oct 29 '20 at 05:33
69

You can use generator expressions together with itertools.takewhile():

even_numbers = (n for n in numbers if not n % 2)
list(itertools.takewhile(lambda x: x != 412, even_numbers))

Edit: I just noticed the requirement not to use any imports. Well, I leave this answer here anyway.

Jacktose
  • 709
  • 7
  • 21
Sven Marnach
  • 574,206
  • 118
  • 941
  • 841
28

I know this is a VERY OLD post, however since OP asked about using break inside a list-comprehension and I was also looking for something similar, I thought I would post my findings here for future reference.

While investigating break, I came across little known feature of iter as iter(callable, sentinel) which return an iterator that "breaks" iteration once callable function value is equal to sentinel value.

>>> help(iter)
Help on built-in function iter in module __builtin__:

iter(...)
    iter(collection) -> iterator
    iter(callable, sentinel) -> iterator

    Get an iterator from an object.  In the first form, the argument must
    supply its own iterator, or be a sequence.
    In the second form, the callable is called until it returns the sentinel.

Tricky part here is defining a function that would fit given problem. In this case first we need to convert given list of numbers to an iterator using x = iter(numbers) which feeds as external variable into lambda function.

Next, our callable function is just a call to the iterator to spit out next value. The iterator then compares with our sentinel value (412 in this case) and "breaks" once that value is reached.

print [i for i in iter(lambda x=iter(numbers): next(x),412) if i %2 == 0]

>>> 
[402, 984, 360, 408, 980, 544, 390, 984, 592, 236, 942, 386, 462, 418,  
 344, 236, 566, 978, 328, 162, 758, 918]
Anil_M
  • 10,893
  • 6
  • 47
  • 74
  • Can you have it stop _after_ hitting value, rather than before? – Elliptica Jun 22 '18 at 00:38
  • I do not believe so. This arrangement by definition "breaks" iteration once callable function value is equal to sentinel value. You may want to look at @ WolframH solution and modify function to suite your needs. – Anil_M Jun 25 '18 at 15:39
  • 4
    A modification that avoids lambda and makes it shorter is to use `__next__`: `[i for i in iter(iter(numbers).__next__, 412) if i%2 == 0]`. However, though it is not clear, the original question implies that only the evens are checked against sentinel, so we can make this simpler: `list(iter((i for i in numbers if i%2 == 0).__next__, 412))`. – tristanslater May 10 '22 at 06:21
20
even = [n for n in numbers[:None if 412 not in numbers else numbers.index(412)] if not n % 2] 

Just took F.J.'s code above and added a ternary to check if 412 is in the list. Still a 'one liner' and will work even if 412 is not in the list.

Michael David Watson
  • 3,028
  • 22
  • 36
  • 1
    If 412 is not in `numbers` the last element is lost if it's even. – Reinstate Monica Jun 05 '15 at 23:22
  • 16
    Who is F.J. and where is the "code above"? Do not mistake Stack Overflow for a (conventional) *forum*; per the [tour], answers may rise to the top or fall to the bottom. – Jongware Mar 09 '18 at 11:35
  • 9
    @usr2564301. I will keep that in mind. When I answered this question **6 years ago** there must have been another answer that was rated above this that I improved on. – Michael David Watson Mar 09 '18 at 21:08
  • Do you still remember which answer you amended to? :) – Jongware Mar 09 '18 at 21:19
  • @usr2564301 I think it should be this one https://stackoverflow.com/a/9572876/4458173 – Angelo Cardellicchio Mar 06 '19 at 09:02
  • 3
    Thank you to all who contributed to sharing advice on this post. However, this is the kind of practice that just frustrates less-skilled people trying to make sense of this 12 months later. Can we avoid "one-liners" which result in "one-hour" trying to decode them? – Felipe Alvarez Feb 11 '20 at 10:11
  • @FelipeAlvarez No. If you want to avoid them due to style/readability and effort that's perfectly fine - but some of us like to see what's possible and different way to do things. – WestCoastProjects May 27 '22 at 23:48
15

If 412 will definitely be in the list you could use this:

even = [n for n in numbers[:numbers.index(412)] if not n % 2]

If you want to include 412 in the result just use numbers[:numbers.index(412)+1] for the slice.

Note that because of the slice this will be less efficient (at least memory-wise) than an itertools or for loop solution.

Andrew Clark
  • 202,379
  • 35
  • 273
  • 306
  • Not only because of the slice it is less efficient, it also has to make a linear search over the list. – Felix Kling Mar 05 '12 at 19:47
  • 2
    @FelixKling - Nevertheless, in a quick timeit test with the other answers shows that this is faster for the sample data provided. I would definitely expect the others to pass it as the data set increases though. – Andrew Clark Mar 05 '12 at 19:58
  • 4
    @FelixKling: The linear search for 412 is super-fast C code, while the other solutions test for 412 in Python code, some solutions calling a function (which is expensive in CPython) for *every number*. I'm sure this solution is faster! -- The list copy is also done in very fast C code; unless you are tight on memory there shouldn't be a performance problem. (OK, if the first number is 412 and the list has 10**6 entries, it's bad; but if 412 is the last number this solution should still be very, very fast.) – Reinstate Monica Mar 05 '12 at 20:30
  • Considering the previous comment, +1ed. – Flavius Mar 05 '12 at 21:29
3

The syntax for list displays (including list comprehensions) is here: http://docs.python.org/reference/expressions.html#list-displays

As you can see, there is no special while or until syntax. The closest you can get is:

even_numbers = (n for n in numbers if 0 == n % 2)
list(itertools.takewhile(lambda x: x != 412, even_numbers))

(Code taken from Sven Marnach's answer, posted while I was typing this).

Marcin
  • 48,559
  • 18
  • 128
  • 201
  • 5
    I didn't downvote, but I assume it's because you took the code from someone else. At least you admitted it. I'll upvote it. – CoffeeRain Mar 05 '12 at 19:59
1

Once I met a similar question on SO, the answer was:

next((x for x in [1, 2, 3, 4] if x % 2 == 0), [])

The last [] needs as default to prevent the StopIteration error if not found

or

any(print(x) if x < 2 else True for x in range(5))

print to prove, he returns None (logically False).

Давид Шико
  • 362
  • 1
  • 4
  • 13
0

another sneaky one-line solution to solve breaking in list comprehension, with the help of end condition.

without using numbers.index(412), maybe a little bit faster?

even = [n for end in [[]] for n in numbers
        if (False if end or n != 412 else end.append(42))
        or not end and not n % 2]

Note: This is a bad idea. just for fun : )

as @WolframH said:

For those complaining it is hackish and should not be used in production code: Well, you're right. Definitely.

recnac
  • 3,744
  • 6
  • 24
  • 46
0

Considering the generator solution is outdated I came up with the following:

even = [n for n in next((numbers[:i] for i, n in enumerate(numbers) if n == 412)) if not n % 2]

Then I went back and saw Andrew Clark's answer which is the same but much better

even = [n for n in numbers[:numbers.index(412)] if not n % 2]

Regardless the best part about the slicing solution is you can choose to include or exclude a number of elements on either side of the ending element for example to get 412 and the number after:

even = [n for n in numbers[:numbers.index(412)+2] if not n % 2]