-1

I have a list of elements and want to separate the elements of the list by a certain condition.

A simple example is a list of numbers and i want to separate the odd from the even ones. For that could use the filter builtin like so:

def is_even(x):
    # ...

l = [0, 1, 2, 3, 4, 5, 6]

even = list(filter(is_even, l))
odd = list(filter(not is_even, l))

That is a bit error prone if the condition is a bit more complex, because i repeat myself twice in the filter functions. Is there a more elegant way to achieve this?

Bharel
  • 23,672
  • 5
  • 40
  • 80
Bastian Venthur
  • 12,515
  • 5
  • 44
  • 78

4 Answers4

3

itertools has a recipe exactly for that:

from itertools import tee, filterfalse
def partition(pred, iterable):
    "Use a predicate to partition entries into false entries and true entries"
    # partition(is_odd, range(10)) --> 0 2 4 6 8   and  1 3 5 7 9
    t1, t2 = tee(iterable)
    return filterfalse(pred, t1), filter(pred, t2)

Usage:

odd, even = partition(is_even, l)

You can convert them to lists, but I suggest leaving them as iterators.

Bastian Venthur
  • 12,515
  • 5
  • 44
  • 78
Bharel
  • 23,672
  • 5
  • 40
  • 80
  • A nice solution, thank you. Can i assume the tee-part is only needed if you have an iterator (i.e. something you can iterate over only once) - when you have a list, just providing the list to filter and filterfalse will do the trick as well, right? – Bastian Venthur Jan 22 '23 at 19:11
  • 1
    @BastianVenthur yup. – Bharel Jan 22 '23 at 19:50
  • 1
    @BastianVenthur but definitely use the `tee` technique so `partition` can be general enough for use with any iterable input – Mulan Jan 24 '23 at 15:35
1

My approach to this is you can make two lists one named [ even ] and the other named [ odd ] and you can make a for loop to filter the original list and add the even value to the [ even ] list and the odd value to the [ odd ] list:

original_list = [1, 2, 3, 4, 5, 6, 7, 8, 9]

even_list = []
odd_list = []

for value in original_list:
    if value % 2 == 0:
        even_list.append(value)

    else:
        odd_list.append(value)

something like this would work fine and I hope this solves your question ✨

1

A generator version, to be used when pred does heavy computation and you wish to run on iterables instead of sequences.

Does not hold all values in memory, runs pred only once for every object.

from collections import deque
from typing import Callable, TypeVar, Iterable
_T = TypeVar('_T')

def iter_split(pred: Callable[[_T], bool],
               iterable: Iterable[_T]) -> tuple[Iterable[_T], Iterable[_T]]:
    """Split an iterable into two iterables based on a predicate.
    
    The predicate will only be called once per element.
    
    Returns:
        A tuple of two iterables, the first containing all elements for which
        the predicate returned True, the second containing all elements for
        which the predicate returned False.
    """
    iterator = iter(iterable)
    true_values: deque[_T] = deque()
    false_values: deque[_T] = deque()
    
    def true_generator():
        while True:
            while true_values:
                yield true_values.popleft()
            
            for item in iterator:
                if pred(item):
                    yield item
                    break
                false_values.append(item)
            else:
                break
            
    def false_generator():
        while True:
            while false_values:
                yield false_values.popleft()
            
            for item in iterator:
                if not pred(item):
                    yield item
                    break
                true_values.append(item)
            else:
                break

    return true_generator(), false_generator()

A thread-proof example for the true_generator() (lock is shared between both gens):

lock = RLock() # RLock in case pred uses a generator.

def true_generator():
    lock.acquire()
    while True:
        while true_values:
            lock.release()
            yield true_values.popleft()
            lock.acquire()
        
        for item in iterator:
            try:
                res = pred(item)
            except BaseException:
                lock.release()
                raise
            if res:
                lock.release()
                yield item
                lock.acquire()
                break
            false_values.append(item)
        else:
            break
    lock.release()
Bharel
  • 23,672
  • 5
  • 40
  • 80
  • I like it. Only potential issue I see is if I request a value from the true_generator and it goes into its for-loop, but it gets interrupted by another thread where I request a value from the false_generator, then that second request might find and append true-values that the first request will miss. – Kelly Bundy Jan 22 '23 at 17:35
  • Or I guess instead of threads, the `pred(item)` call could also trip it up if it requests a value from the false_generator. But I doubt that's a realistic use case :-) – Kelly Bundy Jan 22 '23 at 17:48
  • @KellyBundy It's quite easy to add a lock to these – Bharel Jan 22 '23 at 18:24
  • [Here's mine](https://tio.run/##XZBLigMxDET3PoU2A3bwIunehMCcJThp9cTgH7ICyel7/GkPTLwwklX1Cjm9@RHDfE60bStFD5aROEaXwfoUiYERNdyjT4Q5i6aJCclwpD8JPfmhIUS@CrHgCjk5y9fiWOzdMMpa6YY2N4fqIqAcy6f6NtVrhu8aJIdEw6yaqDpzGXqT5B5TywGc1ZAVVqJpxzRXnxQTVsDYQLbYou/j1bj8OZ96Rl2nQfcMQn5S6EC9G4X41xbO5@rO@Nti4HWp3wOvrwInE35Qno6FKxLZwPLQKGp0Haa27Rc), btw. I also only compute each predicate value only once, but my two `compress` iterators still both *iterate* over all input values and predicate values and both truth-test all predicate truth values. Yours doesn't, that's what I like. – Kelly Bundy Jan 22 '23 at 18:27
  • @KellyBundy boop – Bharel Jan 22 '23 at 18:46
0

If you do not wish to run the predicate twice, you can create 2 lists like so:

def split_predicate(pred, iterable):
    """Split an iterable into two lists based on a predicate.
    
    The predicate will only be called once per element.
    
    Returns:
        A tuple of two lists, the first containing all elements for which
        the predicate returned True, the second containing all elements for
        which the predicate returned False."""
    t1, t2 = [], []
    for item in iterable:
        (t1 if pred(item) else t2).append(item)
    return t1, t2
Bharel
  • 23,672
  • 5
  • 40
  • 80
  • 1
    I'm interested in the generators version. – Kelly Bundy Jan 22 '23 at 16:25
  • I have an idea how to do it but don't like it much. Btw, might want to post [here](https://stackoverflow.com/q/949098/12671057) if it's something new (I think that's the "canonical"). – Kelly Bundy Jan 22 '23 at 16:30
  • @KellyBundy Done. Keep in mind it'll be slower for than the other 2, unless both the iterable is long, and predicate is complex. Otherwise `partition` or `split_predicate` should be used respectively. – Bharel Jan 22 '23 at 17:12