Как разбить последовательность в соответствии с предикатом?
Я очень часто сталкиваюсь с необходимостью разбить последовательность на две подпоследовательности элементов, которые удовлетворяют и не удовлетворяют заданному предикату (сохраняя исходное относительное упорядочение).
Эта гипотетическая функция "сплиттера" будет выглядеть примерно так:
>>> 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, ())