Как разбить последовательность в соответствии с предикатом?

Я очень часто сталкиваюсь с необходимостью разбить последовательность на две подпоследовательности элементов, которые удовлетворяют и не удовлетворяют заданному предикату (сохраняя исходное относительное упорядочение).

Эта гипотетическая функция "сплиттера" будет выглядеть примерно так:

>>> data = map(str, range(14))
>>> pred = lambda i: int(i) % 3 == 2
>>> splitter(data, pred)
[('2', '5', '8', '11'), ('0', '1', '3', '4', '6', '7', '9', '10', '12', '13')]

Мой вопрос:

У Python уже есть стандартный/встроенный способ сделать это?

Эту функциональность, конечно, не сложно скомпоновать (см. добавление ниже), но по ряду причин я предпочел бы использовать стандартный/встроенный метод, чем самокалиброванный.

Спасибо!



Добавление:

Лучшая стандартная функция, которую я нашел до сих пор для обработки этой задачи в Python, - itertools.groupby. Однако, чтобы использовать его для этой конкретной задачи, необходимо дважды вызвать функцию предиката для каждого члена списка, что я считаю досадно глупым:

>>> import itertools as it
>>> [tuple(v[1]) for v in it.groupby(sorted(data, key=pred), key=pred)]
[('0', '1', '3', '4', '6', '7', '9', '10', '12', '13'), ('2', '5', '8', '11')]

(Последний вывод выше отличается от желаемого, показанного ранее тем, что подпоследовательность элементов, которые удовлетворяют предикату, появляется скорее, чем первая, но это очень незначительно и очень легко исправить при необходимости.)

Можно избежать избыточных вызовов предиката (делая, в основном, "встроенную memoization" ), но мой лучший удар по этому поводу становится довольно сложным, далеким от простоты splitter(data, pred):

>>> first = lambda t: t[0]
>>> [zip(*i[1])[1] for i in it.groupby(sorted(((pred(x), x) for x in data),
... key=first), key=first)]
[('0', '1', '3', '4', '6', '7', '9', '10', '12', '13'), ('2', '5', '8', '11')]

Кстати, если вы не заботитесь о сохранении исходного заказа, sorted порядок сортировки по умолчанию получает задание (поэтому параметр key может быть пропущен в вызове sorted):

>>> [zip(*i[1])[1] for i in it.groupby(sorted(((pred(x), x) for x in data)),
... key=first)]
[('0', '1', '3', '4', '6', '7', '9', '10', '12', '13'), ('2', '5', '8', '11')]

Ответ 1

Разделение является одним из тех рецептов itertools, которые делают именно это. Он использует tee(), чтобы убедиться, что он выполняет итерацию коллекции за один проход, несмотря на несколько итераторов, встроенную функцию filter() для захвата элементов, удовлетворяющих предикату, а также 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)

Ответ 2

Я знаю, что вы сказали, что не хотите писать свою собственную функцию, но я не могу себе представить, почему. Ваши решения включают в себя запись собственного кода, вы просто не модулируете их в функции.

Это делает именно то, что вы хотите, понятно и только оценивает предикат один раз для каждого элемента:

def splitter(data, pred):
    yes, no = [], []
    for d in data:
        if pred(d):
            yes.append(d)
        else:
            no.append(d)
    return [yes, no]

Если вы хотите, чтобы он был более компактным (по какой-то причине):

def splitter(data, pred):
    yes, no = [], []
    for d in data:
        (yes if pred(d) else no).append(d)
    return [yes, no]

Ответ 3

Если вы не заботитесь об эффективности, я думаю, что groupby (или любые функции ввода данных в n bins) имеют некоторое приятное соответствие,

by_bins_iter = itertools.groupby(sorted(data, key=pred), key=pred)
by_bins = dict((k, tuple(v)) for k, v in by_bins_iter)

Затем вы можете перейти к своему решению,

return by_bins.get(True, ()), by_bins.get(False, ())