Получить первый элемент из итерируемого объекта, соответствующий условию
Я хотел бы получить первый элемент из списка, соответствующий заданному условию. Важно, чтобы итоговый метод не обрабатывал весь список, который может быть довольно большим. Например, следующая функция работает достаточно хорошо:
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
Тем не менее, я не могу придумать хороший встроенный способ или однострочный код, чтобы сделать это. Я не хочу копировать эту функцию каждый раз, если это не обязательно. Есть ли встроенный способ получить первый элемент, соответствующий условию?
5 ответ(ов)
Как многоразовая, документированная и протестированная функция
def first(iterable, condition=lambda x: True):
"""
Возвращает первый элемент из `iterable`, который
удовлетворяет заданному `condition`.
Если условие не задано, возвращает первый элемент
из iterable.
Вызывает `StopIteration`, если не найдено ни одного элемента,
удовлетворяющего условию.
>>> 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 предложил версию этой функции, в которой можно задать предопределенное значение, если iterable пуст или не содержит элементов, удовлетворяющих условию:
def first(iterable, default=None, condition=lambda x: True):
"""
Возвращает первый элемент из `iterable`, который
удовлетворяет заданному `condition`.
Если условие не задано, возвращает первый элемент
из iterable.
Если задан аргумент `default` и iterable пуст,
или если в нем нет элементов, удовлетворяющих условию,
то возвращается аргумент `default`, если он соответствует условию.
Аргумент `default`, равный None, эквивалентен его отсутствию.
Вызывает `StopIteration`, если не найдено ни одного элемента,
удовлетворяющего условию, и default не задан или не соответствует
условию.
>>> 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
В этой версии функции добавлена возможность указать значение по умолчанию при отсутствии подходящих элементов в итерабельном объекте. Если iterable
пуст и аргумент default
не удовлетворяет условию, будет вызвано исключение StopIteration
. В противном случае будет возвращено значение default
, если оно соответствует переданному условию.
Наиболее эффективные способы в 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)
Чтобы перевести данный код на русский, можно сформулировать ответ следующим образом:
Если вы хотите получить следующее число из диапазона от 0 до 9, которое больше 3, вы можете использовать следующую конструкцию:
next(x for x in range(10) if x > 3)
Обратите внимание, что в Python 3 замените xrange
на range
, так как xrange
был удалён в третьей версии Python. Этот код вернёт первое число из диапазона, соответствующее вашему условию. В данном случае, результатом будет число 4.
Похоже, вы хотите использовать генераторное выражение, аналогично 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
. Тем не менее, он выглядит довольно странно и может восприниматься как злоупотребление синтаксисом.
В более ранних версиях Python, в которых нет встроенной функции next()
, можно использовать функцию next()
на генераторах, как показано ниже.
Если вы хотите получить первый элемент генератора, который соответствует условию, вы можете применять встроенную функцию iter()
для создания итератора и затем использовать функцию next()
.
Вот пример, который можно использовать:
gen = (x for x in range(10) if x > 3)
first_element = gen.__next__() # Здесь мы вызываем метод __next__() на генераторе
print(first_element)
Если вы работаете с Python 2.x, можно просто использовать gen.next()
:
gen = (x for x in range(10) if x > 3)
first_element = gen.next() # Здесь используется метод next() для генератора
print(first_element)
Обратите внимание, что использование метода next()
в Python 2.7 возвращает следующий элемент генератора, или вызывает исключение StopIteration
, если элементов больше нет. Учтите, перед выполнением кода лучше обработать это исключение, чтобы избежать ошибок в случае отсутствия подходящих элементов.
Как пройтись по двум спискам параллельно?
Как перебрать файлы в указанной директории?
Разница между генераторами и итераторами в Python
Как клонировать список, чтобы он не изменялся неожиданно после присваивания?
Ошибка: "'dict' объект не имеет метода 'iteritems'"