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


303

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

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

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


Ответы:


477

В 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-версией (но тогда я не вижу этого упоминания в ответы, в которых упоминается nextвстроенная функция, поэтому я решил, что необходимо дать ответ сам - по крайней мере, проблема с «правильной версией» регистрируется таким образом ;-).

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

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


6
Не работает, как вы описываете. Он поднимается, StopIterationкогда элемент не найден
Suor

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

4
Поскольку это выбранный ответ, я чувствую себя обязанным поделиться ответом о правильном выборе первого элемента здесь . Вкратце: использование next не должно поощряться.
Guyarad

1
@guyarad, как решение, предложенное в этом ответе, менее "загадочно", чем просто использование следующего? Единственный аргумент против следующего (в этом ответе) заключается в том, что вы должны обработать исключение; действительно ?
Авраам Т.С.

Мое мнение немного отличается от того времени, когда я написал комментарий. Я понимаю вашу точку зрения. Это говорит о том, что справляться с этим StopIterationна самом деле не красиво. Лучше использовать метод.
Гайарад

29

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

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

Версия с аргументом по умолчанию

@zorf предложил версию этой функции, в которой вы можете иметь предопределенное возвращаемое значение, если итерация пуста или не содержит элементов, соответствующих условию:

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
Если вы заключаете его в метод, по крайней мере перехватите StopIteration и вызовите ошибку EmptySequence. Было бы намного красивее, когда нет элементов.
Гайарад

@guyarad Это что-то вроде ValueError?
Caridorc

2
@guyarad StopIteration- это каноническое исключение "вне элементов" в python. Я не вижу проблемы с его выбросом. Я бы, вероятно, использовал значение по умолчанию «None», которое можно передать в качестве параметра по умолчанию функции.
Балдрикк

1
Baldrickk Я чувствую, что это не метод итерации. Вы не будете называть это в конкурсе итераторов. Но я не слишком сильно к этому
отношусь

1
Должен быть необязательный аргумент по умолчанию, и если этот аргумент не указан, только тогда выдается исключение, когда ни один элемент в последовательности не удовлетворяет условию.
Цорф

28

Черт, исключения!

я люблю этот ответ . Однако, поскольку 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

13

Подобно использованию 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блока. Но это кажется неясным и оскорбительным для синтаксиса.


+1: не темный и не оскорбительный. Учитывая все обстоятельства, последний кажется довольно чистым.
S.Lott

6
Последний не совсем чистый for foo in genex: break- это просто способ foo = next(genex)обойтись без ясного назначения и за исключением того, что будет вызвано, если операция не имеет смысла быть сжатой. Завершение работы с кодом ошибки вместо перехвата исключения - это обычно плохая вещь в Python.
Майк Грэм

13

Наиболее эффективный способ в 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создает список вместо итератора (неэффективно), а 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)


6

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

from itertools import ifilter

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

2
Выражения генератора проще.
Эрик О Лебиго

1
( i) filterи ( i) mapмогут иметь смысл для случаев, когда применяемые функции уже существуют, но в подобной ситуации имеет гораздо больше смысла просто использовать выражение генератора.
Майк Грэм

Это лучший ответ. Избегайте списочных представлений xahlee.info/comp/list_comprehension.html
с

6

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

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

5

Используя

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

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

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

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

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


4

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

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

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


0

Вы также можете использовать argwhereфункцию в Numpy. Например:

i) Найдите первое «l» в «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) Найти первое случайное число> 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) Найти последнее случайное число> 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

В 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

РЕДАКТИРОВАТЬ: Я думал, что это было очевидно, но, очевидно, нет: вместо Noneвас можно передать функцию (или lambda) с проверкой на условие:

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

-3

Один лайнер:

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

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


Ошибка TypeEr: объект «генератор» является неподписанным
Джош Ли

Мое плохое, должно быть понимание списка, а не генератор, исправлено ... спасибо! :)
Мизипзор

2
Нет причин оценивать всю итерацию (что может быть невозможно). Более надежно и эффективно использовать одно из предоставленных решений.
Майк Грэм
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.