0

In addition to this question "Get the first item from an iterable that matches a condition" I need the element one before the first match. Or fully equivalent: the last element in a row that matches a criterion. For example, if I need the last prime before the first non-prime in this list [2, 3, 4, 5, 6, 7, 8, 9], the answer is 3. In case there is no such element the result could be None, an exception or a default value.

The straightforward solution is:

result = None
for x in numbers:
    if is_prime(x):
        result = x
    else:
        break

Is there a simple one-liner that solves the task?

Dmitry Kuzminov
  • 6,180
  • 6
  • 18
  • 40

2 Answers2

2

Not sure if there is a simpler way. My approach requires itertools.takewhile.

  1. Use itertools.takewhile to cease iteration after a condition is no longer met to create a list of prime numbers until the first non-prime appears, i.e. [2, 3]
  2. Then use list[-1] to get the last element of the list.
  3. In case the list is empty, use (list or [None])[-1] so that it returns None
import itertools
result = ([x for x in itertools.takewhile(lambda n: is_prime(n), numbers)] or [None])[-1]

collections.deque()

from collections import deque
result = (deque((itertools.takewhile(lambda n: is_prime(n), numbers)), maxlen=1) or [None]).pop()
henrywongkk
  • 1,840
  • 3
  • 17
  • 26
  • 1
    This solution creates full list from a generator. Loosing the readability we loose the efficiency as well. The ideal solution should keep previous and current elements only. – Dmitry Kuzminov Oct 28 '19 at 06:19
2

You can use zip and islice to construct an iterator with its own "future" available to it.

next(
    filter(
        lambda pair: isprime(pair[1]),  # The predicate applied to a pair
        zip(xs, islice(xs, 1, None))  # The iterator and its 'future'
    ),
    (None, None)  # A default pair in case no matches are found
)[0]  # Retrieve the 'current' entry from the matching pair

At each step you have a pair that you can think of as (present, future) and the predicate will be applied to future. At the end, we unpack the present, which represents the last non-matching entry in the iterator.

Note that this implementation, as presented, does not return the last entry in the list, because inherently if you zip [a, b, c] against its own shifted version you end up with one shorter than the other. You can use zip_longest (from itertools) to overcome this, but you'll need to handle the fillvalue (usually None) in your predicate.


As noted in a comment, this does not work for generators because it will consume the generator. Using a fold (reduce in Python), though, it becomes easier. First, in a more "Pythonic" presentation, the function looks something like this:

def fold(hist, cur):
    stop, prev = hist

    if stop:
        return hist

    if isprime(cur):
        return (True, prev)

    return (False, cur)

The first element of the tuple serves as a "stop" marker, and the second is our "needle." You can use this with reduce thus:

reduce(fold, xs, (False, None))[1]

That's not exactly a "one liner," but we can compress it into a lambda:

reduce(lambda z, x: (z if z[0] else ((1, z[1]) if isprime(x) else (0, x))), xs, (0, None))[1]
asthasr
  • 9,125
  • 1
  • 29
  • 43
  • Interesting solution. Much closer to what I expected. This however will not work for generators, but if the expression `zip(xs, islice(xs, 1, None))` is substituted with something that accepts `xs` just one time, that would work. – Dmitry Kuzminov Oct 28 '19 at 06:50
  • You could do it with a fold. I will add it to the answer. – asthasr Oct 28 '19 at 15:30