Отримайте перший елемент з ітерабельного файлу, який відповідає умові


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

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


Відповіді:


476

У 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 від 2011 року і трохи переробив перший абзац, щоб зробити речі більш зрозумілими. Будь ласка, продовжуйте та виправляйте мою редакцію, якщо вам потрібно.
Кос

4
Оскільки це обрана відповідь, я змушений поділитися відповіддю щодо правильного вибору першого елемента тут . Якщо коротко: використання наступного не слід заохочувати.
guyarad

1
@guyarad Як рішення, яке пропонується у цій відповіді, менш "кричуще", ніж просто використання наступного? Єдиний аргумент проти наступного (у цій відповіді) полягає в тому, що ви повинні обробляти виняток; справді?
Авраам TS

Мій погляд трохи інший, ніж час, коли я писав коментар. Я бачу вашу думку. Це, як говорять, поводитися з цим StopIterationнасправді не дуже. Краще використовувати метод.
guyarad

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

@guyarad Це такий собі ValueError?
Caridorc

2
@guyarad StopIterationє канонічним винятком "поза елементами" в python. Я не бачу проблеми з цим кинутим. Я, мабуть, використовував за замовчуванням "None", який може бути переданий функції як параметр за замовчуванням.
Балдрікк

1
Болдрік, я відчуваю, що це не метод ітерації. Ви не зателефонуєте цьому на конкурс ітератора. Але я не надто відчуваю це :)
guyarad

1
Має бути необов'язковий аргумент за замовчуванням, і якщо цей аргумент не надається, лише тоді створюйте виняток, коли жоден елемент у послідовності не задовольняє умові.
Zorf

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: Не затьмарений і не образливий. З урахуванням речей, останній здається досить чистим.
С.Лотт

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

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

Якщо ви хочете знайти INDEX першого елемента, який відповідає критеріям, використовуючи генератори, ви можете просто зробити:

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

EDIT: Я думав, що це очевидно, але, мабуть, ні: замість Noneвас можна передати функцію (або a lambda) з перевіркою на умову:

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]

Якщо ви не впевнені, що будь-який елемент буде дійсним відповідно до критеріїв, додайте це, try/exceptоскільки це [0]може призвести до підвищення IndexError.


TypeError: "Генератор" об'єкт не підписується
Джош Лі

Моє погано, має бути розуміння списку, а не генератор, виправлений ... дякую! :)
Мізіпзор

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