Найти первый элемент в последовательности, которая соответствует предикату

171

Я хочу идиоматический способ найти первый элемент в списке, который соответствует предикату.

Текущий код довольно уродлив:

[x for x in seq if predicate(x)][0]

Я думал об изменении его на:

from itertools import dropwhile
dropwhile(lambda x: not predicate(x), seq).next()

Но должно быть что-то более элегантное ... И было бы неплохо, если бы оно возвращало Noneзначение, а не вызывало исключение, если совпадение не найдено.

Я знаю, что мог бы просто определить функцию как:

def get_first(predicate, seq):
    for i in seq:
        if predicate(i): return i
    return None

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

Фортран
источник
3
Помимо того, что его спрашивают позже, чем « функция поиска последовательности Python », этот вопрос имеет гораздо лучший заголовок .
Волк

Ответы:

250

Чтобы найти первый элемент в последовательности, seqкоторая соответствует predicate:

next(x for x in seq if predicate(x))

Или ( itertools.ifilterна Python 2) :

next(filter(predicate, seq))

Он поднимается, StopIterationесли его нет.


Чтобы вернуть, Noneесли такого элемента нет:

next((x for x in seq if predicate(x)), None)

Или:

next(filter(predicate, seq), None)
JFS
источник
28
Или вы можете указать второй аргумент «по умолчанию», nextкоторый используется вместо вызова исключения.
Карл Кнехтель
2
@fortran: next()доступен с Python 2.6. Вы можете прочитать страницу Что нового, чтобы быстро ознакомиться с новыми функциями.
JFS
1
Я новичок в Python и читаю документы, а ifilter использует метод yield. Я предполагаю, что это означает, что предикат лениво оценивается по мере продвижения. то есть мы не запускаем предикат по всему списку, потому что у меня есть функция предиката, которая немного дороже, и я хочу выполнять итерации только до той точки, где мы найдем элемент
Kannan Ekanath
2
@geekazoid: seq.find(&method(:predicate))или даже более краткий для методов экземпляра, например:[1,1,4].find(&:even?)
jfs
16
ifilterбыл переименован filterв Python 3.
tsauerwein
92

Вы можете использовать выражение генератора со значением по умолчанию, а затем nextоно:

next((x for x in seq if predicate(x)), None)

Хотя для этой однострочной вы должны использовать Python> = 2.6.

Эта довольно популярная статья дополнительно обсуждает эту проблему: Чистая функция поиска в списке Python? ,

Чуи
источник
8

Я не думаю, что что-то не так с любым решением, которое вы предложили в своем вопросе.

В моем собственном коде я бы реализовал это так:

(x for x in seq if predicate(x)).next()

Синтаксис с ()создает генератор, который более эффективен, чем генерация всего списка одновременно [].

макинтош
источник
И не только это - с []вами могут возникнуть проблемы, если итератор никогда не заканчивается или его элементы трудно создать, чем позже он становится ...
glglgl
6
'generator' object has no attribute 'next'на Python 3.
JFS
@glglgl - Что касается первого пункта (никогда не заканчивается), я сомневаюсь в этом, поскольку аргумент представляет собой конечную последовательность [точнее, список в соответствии с вопросом OP ']. Что касается второго: опять же, поскольку предоставленный аргумент является последовательностью, объекты должны быть уже созданы и сохранены к моменту вызова этой функции .... или я что-то упустил?
Mac
@JFSebastian - Спасибо, я не знал об этом! :) Из любопытства, каков принцип дизайна этого выбора?
Mac
@mac - для совместимости с двойным подчеркиванием других специальных методов. См python.org/dev/peps/pep-3114
Чуи
1

Ответ Дж. Ф. Себастьяна самый элегантный, но, как указал Фортран, требует python 2.6.

Для Python версии <2.6 вот лучшее, что я могу придумать:

from itertools import repeat,ifilter,chain
chain(ifilter(predicate,seq),repeat(None)).next()

В качестве альтернативы, если вам нужен список позже (list обрабатывает StopIteration), или вам нужно больше, чем просто первый, но все же не все, вы можете сделать это с помощью islice:

from itertools import islice,ifilter
list(islice(ifilter(predicate,seq),1))

ОБНОВЛЕНИЕ: Хотя я лично использую предопределенную функцию first (), которая перехватывает StopIteration и возвращает None, вот возможное улучшение по сравнению с приведенным выше примером: избегайте использования filter / ifilter:

from itertools import islice,chain
chain((x for x in seq if predicate(x)),repeat(None)).next()
parity3
источник
11
Хлоп! если бы все сводилось к этому, я просто сделал бы простой цикл «for» с «если» внутри - гораздо легче читать
Ник Перкинс