Получить первый элемент из iterable, который соответствует условию

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

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

эту функцию можно использовать примерно так:

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

однако я не могу придумать хороший встроенный / один лайнер, чтобы позволить мне это сделать. Я не особенно хочу копировать эту функцию, если мне это не нужно. Есть встроенный способ получить первый элемент, соответствующий условию?

13 ответов


в Python 2.6 или лучше:

если вы хотите StopIteration будет поднят, если соответствующий элемент не найден:

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

если вы хотите default_value (например,None) будет возвращен вместо этого:

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

обратите внимание, что в этом случае вам нужна дополнительная пара скобок вокруг выражения генератора - они нужны всегда, когда выражение генератора не является единственным аргументом.

я вижу большинство ответов решительно игнорировать next встроенный и поэтому я предполагаю, что по какой-то таинственной причине они на 100% сосредоточены на версиях 2.5 и старше-без упоминания проблемы с версией Python (но тогда я не вижу этого упоминания в ответах, что do отметить next встроенный, поэтому я подумал, что необходимо предоставить ответ сам - по крайней мере, проблема" правильной версии " записывается таким образом;-).

в 2.5, то .next() метод итераторы немедленно поднимают StopIteration Если итератор немедленно завершает -- т. е. для вашего случая использования, если ни один элемент в iterable не удовлетворяет условию. Если вам все равно (то есть, вы знаете там должны быть хотя бы одним удовлетворительным пунктом), то просто используйте .next() (лучше всего на genexp, строка для next встроенный в Python 2.6 и лучше).

если вы do уход, обертывание вещей в функции, как вы впервые указали в своем Q, кажется лучшим, и в то время как реализация функции, которую вы предложили, просто прекрасна, вы можете альтернативно использовать itertools, a for...: break цикл, или genexp, или try/except StopIteration как тело функции, как предлагали различные ответы. В любой из этих альтернатив нет большой добавленной стоимости, поэтому я бы пошел на совершенно простую версию, которую вы впервые предложили.


как многоразовая, документированная и протестированная функция

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))

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

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

в любом случае, вы, вероятно, хотите, чтобы поймать StopIteration хотя, если никакие элементы не удовлетворяют вашему условию.

Технически говоря, я полагаю, вы могли бы сделать что-то вроде этого:

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

Это не try/except блок. Но это кажется неясным и оскорбительным для синтаксиса.


Блин Исключения!

Я люблю ответ. Однако, поскольку next() поднять StopIteration исключение, когда нет элементов, я бы использовал следующий фрагмент, чтобы избежать исключения:

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

например,

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

буду ставить StopIteration исключение:

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

для более старых версий Python, где следующий встроенный не существует:

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

Я бы написал так

next(x for x in xrange(10) if x > 3)

на itertools модуль содержит функцию фильтра для итераторов. Первый элемент отфильтрованного итератора можно получить, вызвав next() на:

from itertools import ifilter

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

С помощью

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

можно проверить условие на стоимостью первый элемент the_iterable, и получить от него индекс без необходимости оценивать все элементы!--5-->the_iterable.

полное выражение в использовании

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

здесь first_index принимает значение первого значения, указанного в описанном выше выражении.


поскольку вы запросили встроенный однострочный, это позволит избежать проблемы StopIteration исключение, хотя оно требует, чтобы ваш iterable был небольшим, поэтому вы можете привести его к списку, так как это единственная известная мне конструкция, которая проглотит Стопитерацию и позволит вам заглянуть в значения:

(lambda x:x[0] if x else None)(list(y for y in ITERABLE if CONDITION))

(если ни один элемент не соответствует, вы получите None, а не StopIteration исключения.)


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

Если вы хотите найти индекс первого элемента, соответствующего критериям с помощью генераторов, вы можете просто сделать:

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

наиболее эффективным способом в Python 3 является один из следующих (используя аналогичный пример):

С "понимание" стиль:

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

предупреждение: выражение работает с Python 2, но в примере используется range который возвращает итеративный объект в Python 3 вместо списка, такого как Python 2 (Если вы хотите построить итерацию в Python 2, Используйте xrange вместо).

обратите внимание, что выражение избегает составьте список в выражении понимания next([i for ...]), что приведет к созданию списка со всеми элементами перед фильтрованием элементов и приведет к обработке всех параметров, а не к остановке итерации один раз i == 1000.

С "функциональный" стиль:

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

предупреждение: это не работает в Python 2, даже заменив range С xrange из-за filter создать список вместо итератора (неэффективно), и the next функция работает только с итераторами.

значение по умолчанию

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

"функциональный" стиль:

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

"понимание" стиль:

С этим стилем вам нужно окружить выражение понимания с () чтобы избежать SyntaxError: Generator expression must be parenthesized if not sole argument:

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

В Python 3:

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

В Python 2.6:

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

EDIT: я думал, что это очевидно, но, по-видимому, нет: вместо None вы можете передать функцию (или lambda) с проверкой на условие:

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

Oneliner:

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

если вы не уверены, что какой-либо элемент будет действителен в соответствии с критериями, вы должны заключить это с try/except С тех пор [0] поднять IndexError.