Znajdź pierwszy element w sekwencji, który pasuje do predykatu

171

Chcę idiomatycznego sposobu na znalezienie pierwszego elementu na liście, który pasuje do predykatu.

Obecny kod jest dość brzydki:

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

Myślałem o zmianie tego na:

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

Ale musi być coś bardziej eleganckiego ... I byłoby miło, gdyby zwracało Nonewartość, zamiast zgłaszać wyjątek, jeśli nie zostanie znalezione dopasowanie.

Wiem, że mógłbym po prostu zdefiniować funkcję taką jak:

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

Ale rozpoczęcie wypełniania kodu takimi funkcjami użytkowymi jest zupełnie bez smaku (a ludzie prawdopodobnie nie zauważą, że już tam są, więc mają tendencję do powtarzania się w czasie), jeśli istnieją wbudowane narzędzia, które już zapewniają to samo.

fortran
źródło
3
Poza tym, że zostało zadane później niż „ funkcja znajdowania sekwencji w Pythonie ”, to pytanie ma znacznie lepszy tytuł .
Wilk

Odpowiedzi:

249

Aby znaleźć pierwszy element w sekwencji, seqktóry pasuje do a predicate:

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

Lub ( itertools.ifilterw Pythonie 2) :

next(filter(predicate, seq))

Podnosi się, StopIterationjeśli nie ma.


Aby wrócić, Nonejeśli nie ma takiego elementu:

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

Lub:

next(filter(predicate, seq), None)
jfs
źródło
27
Lub możesz podać drugi argument „domyślny”, nextktóry jest używany zamiast zgłaszania wyjątku.
Karl Knechtel
2
@fortran: next()jest dostępny od wersji Pythona 2.6. Możesz przeczytać stronę Co nowego, aby szybko zapoznać się z nowymi funkcjami.
jfs
1
Jestem nowicjuszem w Pythonie i czytam dokumentację, a ifilter używa metody „yield”. Zakładam, że oznacza to, że predykat jest oceniany leniwie w trakcie. tj. nie przeprowadzamy predykatu przez całą listę, ponieważ mam funkcję predykatu, która jest trochę droga i chcę tylko iterować do momentu, w którym znajdziemy element
Kannan Ekanath
2
@geekazoid: seq.find(&method(:predicate))lub jeszcze bardziej zwięzłe, na przykład metody:[1,1,4].find(&:even?)
jfs
16
ifilterzostała zmieniona na filterw Pythonie 3.
tsauerwein
92

Możesz użyć wyrażenia generatora z wartością domyślną, a następnie next:

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

Chociaż w przypadku tego jednowierszowego kodu musisz używać Pythona> = 2.6.

Ten dość popularny artykuł dalej omawia ten problem: Najczystsza funkcja znajdowania na liście w Pythonie? .

Chewie
źródło
8

Nie sądzę, żeby było coś złego w obu rozwiązaniach, które zaproponowałeś w swoim pytaniu.

We własnym kodzie zaimplementowałbym to jednak w następujący sposób:

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

Składnia with ()tworzy generator, który jest bardziej wydajny niż generowanie całej listy naraz [].

prochowiec
źródło
I nie tylko to - []możesz napotkać problemy, jeśli iterator nigdy się nie skończy lub jego elementy są trudne do utworzenia, im później to ...
glglgl
6
'generator' object has no attribute 'next'na Pythonie 3.
jfs,
@glglgl - Co do pierwszego punktu (nigdy się nie kończy), wątpię w to, ponieważ argumentem jest ciąg skończony [a dokładniej lista według pytania OP]. Co do drugiego: ponownie, ponieważ dostarczony argument jest sekwencją, obiekty powinny być już utworzone i zapisane do czasu wywołania tej funkcji ... czy czegoś mi brakuje?
Mac,
@JFSebastian - Dziękuję, nie byłem tego świadomy! :) Z ciekawości, jaka jest zasada projektowa tego wyboru?
Mac,
@mac - Aby zachować spójność z podwójnym podkreśleniem innych metod specjalnych. Zobacz python.org/dev/peps/pep-3114
Chewie,
1

Odpowiedź JF Sebastiana jest najbardziej elegancka, ale wymaga Pythona 2.6, jak wskazał Fortran.

Dla wersji Pythona <2.6, oto najlepsze, co mogę wymyślić:

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

Alternatywnie, jeśli potrzebowałeś listy później (lista obsługuje StopIteration) lub potrzebujesz czegoś więcej niż tylko pierwszego, ale nadal nie wszystkiego, możesz to zrobić za pomocą islice:

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

AKTUALIZACJA: Chociaż osobiście używam predefiniowanej funkcji o nazwie first (), która przechwytuje StopIteration i zwraca None, Oto możliwe ulepszenie w stosunku do powyższego przykładu: unikaj używania filtra / ifilter:

from itertools import islice,chain
chain((x for x in seq if predicate(x)),repeat(None)).next()
parzystość 3
źródło
11
Yikes! jeśli do tego dojdzie, wykonałbym po prostu prostą pętlę „for” z ciągiem „if” w środku - znacznie łatwiejsze do odczytania
Nick Perkins