Separating elements from a list in Python depending on a condition

Question:

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?

Asked By: Bastian Venthur

||

Answers:

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.

Answered By: Bharel

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
Answered By: Bharel

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 ✨

Answered By: Abdelrahman Tawfik

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()
Answered By: Bharel
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.