Zdobądź pierwszy przedmiot z iteracji spełniającej warunek


303

Chciałbym uzyskać pierwszy element z listy spełniający warunek. Ważne jest, aby wynikowa metoda nie przetwarzała całej listy, która może być dość duża. Na przykład wystarczająca jest następująca funkcja:

def first(the_iterable, condition = lambda x: True):
    for i in the_iterable:
        if condition(i):
            return i

Tej funkcji można użyć w następujący sposób:

>>> first(range(10))
0
>>> first(range(10), lambda i: i > 3)
4

Nie mogę jednak wymyślić dobrego wbudowanego / jedno-liniowego, który pozwoliłby mi to zrobić. Nie chcę szczególnie kopiować tej funkcji, jeśli nie muszę. Czy istnieje wbudowany sposób, aby pierwszy element pasował do określonego warunku?


Odpowiedzi:


476

W Python 2.6 lub nowszym:

Jeśli chcesz StopIterationzostać podniesiony, jeśli nie znaleziono pasującego elementu:

next(x for x in the_iterable if x > 3)

Jeśli zamiast tego chcesz default_value(np. None) Zostać zwrócony:

next((x for x in the_iterable if x > 3), default_value)

Zauważ, że w tym przypadku potrzebujesz dodatkowej pary nawiasów wokół wyrażenia generatora - są one potrzebne, gdy wyrażenie generatora nie jest jedynym argumentem.

Widzę, że większość odpowiedzi zdecydowanie ignoruje nextwbudowane, więc zakładam, że z jakiegoś tajemniczego powodu są one w 100% skoncentrowane na wersjach 2.5 i starszych - nie wspominając o problemie z wersją Pythona (ale potem nie widzę tej wzmianki w odpowiedzi, które wspominają o nextwbudowanym, dlatego uważam, że konieczne jest udzielenie odpowiedzi osobiście - przynajmniej w ten sposób rejestruje się problem „poprawnej wersji” ;-).

W wersji 2.5 .next()metoda iteratorów natychmiast się podnosi, StopIterationjeśli iterator natychmiast się kończy - tj. W twoim przypadku użycia, jeśli żaden element iteracji nie spełnia warunku. Jeśli cię to nie obchodzi (tzn. Wiesz, że musi istnieć co najmniej jeden zadowalający element), po prostu użyj .next()(najlepiej na geneksie, linii dla nextwbudowanego w Pythonie 2.6 i lepszych).

Jeśli zrobić opieki, rzeczy owijania w funkcji, jak po raz pierwszy wskazano w Q wydaje się najlepiej, a jednocześnie realizacja funkcji, którą zaproponował jest dobrze, można alternatywnie użyć itertools, a for...: breakpętlę, lub genexp lub try/except StopIterationjako ciała danej funkcji , jak sugerują różne odpowiedzi. Żadna z tych alternatyw nie ma dużej wartości dodanej, więc wybrałbym bardzo prostą wersję, którą pierwszy raz zaproponowałeś.


6
Nie działa tak jak opisano. Rośnie, StopIterationgdy nie znaleziono elementu
Suor,

Ponieważ pojawia się to w wynikach wyszukiwania, śledziłem komentarz @ Suor z 2011 roku i przeredagowałem nieco pierwszy akapit, aby wszystko było bardziej jasne. Proszę, jeśli to konieczne, poprawcie moją edycję.
Kos

4
Ponieważ jest to wybrana odpowiedź, czuję się zmuszony podzielić się odpowiedzią dotyczącą prawidłowego wyboru pierwszego elementu tutaj . Krótko mówiąc: nie należy zachęcać do korzystania z next.
guyarad

1
@ guyarad, w jaki sposób rozwiązanie zaproponowane w tej odpowiedzi jest mniej „tajemnicze” niż tylko użycie następnego? Jedynym argumentem przeciwko następnemu (w tej odpowiedzi) jest to, że musisz obsłużyć wyjątek; naprawdę?
Abraham TS

Moje zdanie różni się nieco od czasu, kiedy napisałem komentarz. Rozumiem co masz na myśli. To powiedziawszy, konieczność obsługi nie StopIterationjest naprawdę ładna. Lepiej użyj metody.
guyarad

29

Jako funkcja wielokrotnego użytku, udokumentowana i przetestowana

def first(iterable, condition = lambda x: True):
    """
    Returns the first item in the `iterable` that
    satisfies the `condition`.

    If the condition is not given, returns the first item of
    the iterable.

    Raises `StopIteration` if no item satysfing the condition is found.

    >>> first( (1,2,3), condition=lambda x: x % 2 == 0)
    2
    >>> first(range(3, 100))
    3
    >>> first( () )
    Traceback (most recent call last):
    ...
    StopIteration
    """

    return next(x for x in iterable if condition(x))

Wersja z domyślnym argumentem

@zorf zasugerował wersję tej funkcji, w której możesz mieć predefiniowaną wartość zwracaną, jeśli iterowalny jest pusty lub nie ma elementów pasujących do warunku:

def first(iterable, default = None, condition = lambda x: True):
    """
    Returns the first item in the `iterable` that
    satisfies the `condition`.

    If the condition is not given, returns the first item of
    the iterable.

    If the `default` argument is given and the iterable is empty,
    or if it has no items matching the condition, the `default` argument
    is returned if it matches the condition.

    The `default` argument being None is the same as it not being given.

    Raises `StopIteration` if no item satisfying the condition is found
    and default is not given or doesn't satisfy the condition.

    >>> first( (1,2,3), condition=lambda x: x % 2 == 0)
    2
    >>> first(range(3, 100))
    3
    >>> first( () )
    Traceback (most recent call last):
    ...
    StopIteration
    >>> first([], default=1)
    1
    >>> first([], default=1, condition=lambda x: x % 2 == 0)
    Traceback (most recent call last):
    ...
    StopIteration
    >>> first([1,3,5], default=1, condition=lambda x: x % 2 == 0)
    Traceback (most recent call last):
    ...
    StopIteration
    """

    try:
        return next(x for x in iterable if condition(x))
    except StopIteration:
        if default is not None and condition(default):
            return default
        else:
            raise

6
Jeśli otaczasz ją metodą, przynajmniej złap StopIteration i podnieś błąd EmptySequence. Byłoby o wiele ładniejsze, gdy nie ma elementów.
guyarad

@guyarad Czy to rodzaj ValueError?
Caridorc

2
@guyarad StopIterationjest kanonicznym wyjątkiem „poza elementami” w pythonie. Nie widzę problemu z wyrzuceniem go. Prawdopodobnie użyłbym wartości domyślnej „Brak”, którą można przekazać jako domyślny parametr funkcji.
Baldrickk

1
Baldrickk Czuję, że to nie jest metoda iteracji. Nie nazwiesz tego w konkursie iteratora. Ale nie czuję się zbyt mocno :)
guyarad

1
Powinien istnieć opcjonalny domyślny argument, a jeśli ten argument nie zostanie podany, tylko wtedy zgłaszaj wyjątek, gdy żaden element w sekwencji nie spełnia warunku.
Zorf

28

Cholerne wyjątki!

Uwielbiam tę odpowiedź . Ponieważ jednak next()zgłaszam StopIterationwyjątek, gdy nie ma żadnych elementów, użyłbym następującego fragmentu kodu, aby uniknąć wyjątku:

a = []
item = next((x for x in a), None)

Na przykład,

a = []
item = next(x for x in a)

Podniesie StopIterationwyjątek;

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

13

Podobnie do używania ifilter, możesz użyć wyrażenia generatora:

>>> (x for x in xrange(10) if x > 5).next()
6

W obu przypadkach prawdopodobnie chcesz złapać StopIteration, na wypadek gdyby żadne elementy nie spełniały Twojego warunku.

Technicznie rzecz biorąc, przypuszczam, że możesz zrobić coś takiego:

>>> foo = None
>>> for foo in (x for x in xrange(10) if x > 5): break
... 
>>> foo
6

Pozwoliłoby to uniknąć konieczności wykonania try/exceptbloku. Ale wydaje się to niejasne i obraźliwe dla składni.


+1: Nie niejasne ani obelżywe. Biorąc wszystko pod uwagę, ostatni wydaje się całkiem czysty.
S.Lott,

6
Ostatni nie jest wcale czysty - for foo in genex: breakto tylko sposób na zrobienie tego foo = next(genex)bez wyraźnego przypisania zadania, z wyjątkiem, który zostałby zgłoszony, gdyby operacja nie miała sensu zostać zmiażdżona. Skończyło się na tym, że kod błędu zamiast łapać wyjątek, jest zwykle złą rzeczą w Pythonie.
Mike Graham

13

Najbardziej wydajnym sposobem w Pythonie 3 jest jeden z poniższych (na podobnym przykładzie):

W stylu „rozumienia” :

next(i for i in range(100000000) if i == 1000)

OSTRZEŻENIE : Wyrażenie działa również z Python 2, ale w tym przykładzie użyto rangezwracającego obiekt iterowalny w Python 3 zamiast listy takiej jak Python 2 (jeśli chcesz zbudować iterowalną w Python 2 użyjxrange ).

Zauważ, że wyrażenie to unika konstruowania listy w wyrażeniu ze zrozumieniem next([i for ...]), co spowodowałoby utworzenie listy ze wszystkimi elementami przed filtrowaniem elementów i spowodowałoby przetworzenie wszystkich opcji, zamiast zatrzymywać iterację raz i == 1000.

W stylu „funkcjonalnym” :

next(filter(lambda i: i == 1000, range(100000000)))

OSTRZEŻENIE : To nie działa w Pythonie 2, a nawet zamienia się rangez xrangepowodu, który filtertworzy listę zamiast iteratora (nieefektywnego) inext funkcja działa tylko z iteratorami.

Domyślna wartość

Jak wspomniano w innych odpowiedziach, musisz dodać dodatkowy parametr do funkcji, nextjeśli chcesz uniknąć wyjątku zgłaszanego, gdy warunek nie jest spełniony.

styl „funkcjonalny” :

next(filter(lambda i: i == 1000, range(100000000)), False)

styl „rozumienia” :

W tym stylu musisz otoczyć wyrażenie zrozumienia, ()aby uniknąć SyntaxError: Generator expression must be parenthesized if not sole argument:

next((i for i in range(100000000) if i == 1000), False)


6

itertoolsModuł zawiera funkcję filtra dla iteratorów. Pierwszy element filtrowanego iteratora można uzyskać, wywołując next()go:

from itertools import ifilter

print ifilter((lambda i: i > 3), range(10)).next()

2
Wyrażenia generatora są prostsze.
Eric O Lebigot,

1
( i) filteri ( i) mapmogą mieć sens w przypadkach, w których stosowane funkcje już istnieją, ale w takiej sytuacji bardziej sensowne jest użycie wyrażenia generatora.
Mike Graham


6

Dla starszych wersji Pythona, w których nie ma następnej wbudowanej:

(x for x in range(10) if x > 3).next()

5

Używając

(index for index, value in enumerate(the_iterable) if condition(value))

można sprawdzić stan na wartość pierwszego elementu the_iterable i uzyskania jego indeksu bez konieczności oceny wszystkich elementów w the_iterable .

Pełne wyrażenie do użycia to

first_index = next(index for index, value in enumerate(the_iterable) if condition(value))

Tutaj first_index przyjmuje wartość pierwszej wartości zidentyfikowanej w omówionym powyżej wyrażeniu.


4

To pytanie ma już świetne odpowiedzi. Dodam tylko dwa centy, ponieważ wylądowałem tutaj, próbując znaleźć rozwiązanie mojego problemu, który jest bardzo podobny do PO.

Jeśli chcesz znaleźć INDEKS pierwszego elementu spełniającego kryteria przy użyciu generatorów, możesz po prostu:

next(index for index, value in enumerate(iterable) if condition)


0

Możesz także użyć tej argwherefunkcji w Numpy. Na przykład:

i) Znajdź pierwsze „l” w „helloworld”:

import numpy as np
l = list("helloworld") # Create list
i = np.argwhere(np.array(l)=="l") # i = array([[2],[3],[8]])
index_of_first = i.min()

ii) Znajdź pierwszą liczbę losową> 0,1

import numpy as np
r = np.random.rand(50) # Create random numbers
i = np.argwhere(r>0.1)
index_of_first = i.min()

iii) Znajdź ostatnią liczbę losową> 0,1

import numpy as np
r = np.random.rand(50) # Create random numbers
i = np.argwhere(r>0.1)
index_of_last = i.max()

-1

W Pythonie 3:

a = (None, False, 0, 1)
assert next(filter(None, a)) == 1

W Pythonie 2.6:

a = (None, False, 0, 1)
assert next(iter(filter(None, a))) == 1

EDYCJA: Myślałem, że to oczywiste, ale najwyraźniej nie: zamiast tego Nonemożesz przekazać funkcję (lub a lambda) ze sprawdzeniem warunku:

a = [2,3,4,5,6,7,8]
assert next(filter(lambda x: x%2, a)) == 3

-3

Oneliner:

thefirst = [i for i in range(10) if i > 3][0]

Jeśli nie masz pewności, że jakikolwiek element będzie poprawny zgodnie z kryteriami, powinieneś dołączyć to, try/exceptponieważ [0]może to podnieść an IndexError.


TypeError: Obiekt „generator” nie podlega skryptowi
Josh Lee

Moim złym, powinno być zrozumienie listy, a nie generator, naprawione ... dzięki! :)
Mizipzor

2
Nie ma powodu, aby oceniać całość iterowalności (co może być niemożliwe). Zastosowanie jednego z pozostałych dostarczonych rozwiązań jest bardziej niezawodne i wydajne.
Mike Graham
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.