Obtenga el primer elemento de un iterable que coincida con una condición

303

Me gustaría obtener el primer elemento de una lista que coincida con una condición. Es importante que el método resultante no procese la lista completa, lo que podría ser bastante grande. Por ejemplo, la siguiente función es adecuada:

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

Esta función podría usarse de la siguiente manera:

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

Sin embargo, no puedo pensar en un buen incorporado / one-liner que me permita hacer esto. En particular, no quiero copiar esta función si no es necesario. ¿Hay una forma integrada de obtener el primer elemento que coincida con una condición?

Chris Phillips
fuente

Respuestas:

477

En Python 2.6 o más reciente:

Si desea StopIterationsubir si no se encuentra ningún elemento coincidente:

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

Si desea default_value(por ejemplo None) que se devuelva en su lugar:

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

Tenga en cuenta que necesita un par adicional de paréntesis alrededor de la expresión del generador en este caso; son necesarios siempre que la expresión del generador no sea el único argumento.

Veo que la mayoría de las respuestas ignoran resueltamente el next incorporado y, por lo tanto, supongo que por alguna misteriosa razón están 100% enfocadas en las versiones 2.5 y anteriores, sin mencionar el problema de la versión de Python (pero no veo esa mención en las respuestas que hacen mencionan el nextincorporado, por lo que pensé que es necesario proporcionar una respuesta a mí mismo - por lo menos el tema "versión correcta" se pone en el registro de esta manera ;-).

En 2.5, el .next()método de iteradores aumenta inmediatamente StopIterationsi el iterador termina inmediatamente, es decir, para su caso de uso, si ningún elemento en el iterable cumple la condición. Si no te importa (es decir, sabes que hay debe haber al menos un elemento satisfactorio), simplemente use .next()(mejor en un genexp, línea para el nextincorporado en Python 2.6 y mejor).

Si haces el cuidado, las cosas envolver en una función como se había indicado por primera vez en su Q parece mejor, y mientras que la implementación de la función usted propuso está bien, podría utilizar alternativamente itertools, un for...: breakbucle, o una genexp, o una try/except StopIterationcomo el cuerpo de la función , como lo sugirieron varias respuestas. No hay mucho valor agregado en ninguna de estas alternativas, así que elegiría la versión simple que propusiste por primera vez.

Alex Martelli
fuente
66
No funciona como lo describe usted. Aumenta StopIterationcuando no se encuentra ningún elemento
Suor
Como esto aparece en los resultados de búsqueda, he seguido el comentario de @ Suor de 2011 y redacté el primer párrafo un poco para aclarar las cosas. Siga adelante y modifique mi edición si es necesario.
Kos
44
Como esta es la respuesta seleccionada, me siento obligado a compartir una respuesta para seleccionar el primer elemento correctamente aquí . En resumen: no se debe alentar el uso de next.
guyarad
1
@guyarad, ¿cómo la solución propuesta en esa respuesta es menos "críptica" que simplemente usar la siguiente? El único argumento en contra de next (en esa respuesta) es que debe manejar una excepción; De Verdad ?
Abraham TS
Mi punto de vista es un poco diferente al momento en que escribí el comentario. Entiendo tu argumento. Dicho esto, tener que manejar StopIterationrealmente no es bonito. Mejor usa un método.
guyarad
29

Como una función reutilizable, documentada y probada

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

Versión con argumento predeterminado

@zorf sugirió una versión de esta función donde puede tener un valor de retorno predefinido si el iterable está vacío o no tiene elementos que coincidan con la condición:

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
Caridorc
fuente
66
Si lo está envolviendo con un método, al menos detecte StopIteration y genere el error EmptySequence. Sería mucho más bonito cuando no hay elementos.
guyarad
@guyarad ¿Es eso un tipo de ValueError?
Caridorc
2
@guyarad StopIterationes la excepción canónica "fuera de elementos" en python. No veo un problema con el lanzamiento. Probablemente usaría un valor predeterminado de "Ninguno" que se puede pasar como parámetro predeterminado a la función.
Baldrickk
1
Baldrickk Siento que este no es un método de iteración. No llamarás a este en un concurso de un iterador. Pero no me siento demasiado fuerte al respecto :)
guyarad
1
Debería haber un argumento predeterminado opcional, y si ese argumento no se proporciona, solo entonces generará una excepción cuando ningún elemento de la secuencia cumpla la condición.
Zorf
28

¡Malditas excepciones!

Me encanta esta respuesta . Sin embargo, dado que next()genera una StopIterationexcepción cuando no hay elementos, usaría el siguiente fragmento para evitar una excepción:

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

Por ejemplo,

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

Levantará una StopIterationexcepción;

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

Similar al uso ifilter, podría usar una expresión generadora:

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

En cualquier caso, es probable que desee atrapar StopIteration, en caso de que ningún elemento satisfaga su condición.

Técnicamente hablando, supongo que podrías hacer algo como esto:

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

Evitaría tener que hacer un try/exceptbloqueo. Pero eso parece un poco oscuro y abusivo de la sintaxis.

Matt Anderson
fuente
+1: No es oscuro, ni abusivo. A fin de cuentas, el último parece bastante limpio.
S.Lott
66
El último no está del todo limpio: for foo in genex: breakes solo una forma de hacerlo foo = next(genex)sin dejar en claro la tarea y con la excepción de que se plantearía si la operación no tiene sentido ser aplastada. Terminar con un código de falla en lugar de detectar una excepción suele ser algo malo en Python.
Mike Graham
13

Las formas más eficientes en Python 3 son una de las siguientes (usando un ejemplo similar):

Con estilo de "comprensión" :

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

ADVERTENCIA : La expresión también funciona con Python 2, pero en el ejemplo se usa rangeque devuelve un objeto iterable en Python 3 en lugar de una lista como Python 2 (si desea construir un iterable en Python 2 usexrange en lugar).

Tenga en cuenta que la expresión evita construir una lista en la expresión de comprensión next([i for ...]) , lo que provocaría crear una lista con todos los elementos antes de filtrar los elementos, y provocaría procesar todas las opciones, en lugar de detener la iteración una vez i == 1000.

Con estilo "funcional" :

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

ADVERTENCIA : Esto no funciona en Python 2, incluso reemplazando rangeconxrange debido que filtercrea una lista en lugar de un iterador (ineficiente), y la nextfunción solo funciona con iteradores.

Valor por defecto

Como se menciona en otras respuestas, debe agregar un parámetro adicional a la función next si desea evitar una excepción provocada cuando no se cumple la condición.

estilo "funcional" :

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

"comprensión"estilo de :

Con este estilo necesita rodear la expresión de comprensión ()para evitar SyntaxError: Generator expression must be parenthesized if not sole argument:

next((i for i in range(100000000) if i == 1000), False)
Mariano Ruiz
fuente
7

Yo escribiria esto

next(x for x in xrange(10) if x > 3)
Mike Graham
fuente
Creo que i > 3debería estar x > 3en tu ejemplo
Ricky Robinson
6

El itertoolsmódulo contiene una función de filtro para iteradores. El primer elemento del iterador filtrado se puede obtener llamando next()a él:

from itertools import ifilter

print ifilter((lambda i: i > 3), range(10)).next()
algo
fuente
2
Las expresiones generadoras son más simples.
Eric O Lebigot
1
( i) filtery ( i) mappueden tener sentido para los casos en que las funciones que se aplican ya existen, pero en una situación como esta, tiene mucho más sentido usar una expresión generadora.
Mike Graham
Esta es la mejor respuesta. Evite las comprensiones de listas xahlee.info/comp/list_comprehension.html
mit
6

Para versiones anteriores de Python donde no existe el próximo incorporado:

(x for x in range(10) if x > 3).next()
Menno Smits
fuente
5

Mediante el uso

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

se puede verificar la condición del valor del primer elemento en the_iterable y obtener su índice sin la necesidad de evaluar todos los elementos en the_iterable .

La expresión completa para usar es

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

Aquí first_index asume el valor del primer valor identificado en la expresión discutida anteriormente.

nota azul
fuente
4

Esta pregunta ya tiene excelentes respuestas. Solo agrego mis dos centavos porque llegué aquí tratando de encontrar una solución a mi propio problema, que es muy similar al OP.

Si desea encontrar el ÍNDICE del primer elemento que coincide con un criterio utilizando generadores, simplemente puede hacer:

next(index for index, value in enumerate(iterable) if condition)
dangom
fuente
Ver también: stackoverflow.com/questions/1701211/…
dreftymac
0

También podría usar la argwherefunción en Numpy. Por ejemplo:

i) Encuentra la primera "l" en "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) Encuentra el primer número aleatorio> 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) Encuentra el último número aleatorio> 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()
objetivo
fuente
-1

En Python 3:

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

En Python 2.6:

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

EDITAR: pensé que era obvio, pero aparentemente no: en lugar de Noneque pueda pasar una función (o una lambda) con una verificación de la condición:

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

Un trazador de líneas:

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

Si no está seguro de que algún elemento será válido de acuerdo con los criterios, debe adjuntarlo try/exceptya que [0]puede generar un IndexError.

Mizipzor
fuente
TypeError: el objeto 'generador' no se puede suscribir
Josh Lee
Lo malo es que la lista de comprensión no es un generador, arreglado ... ¡gracias! :)
Mizipzor
2
No hay ninguna razón para evaluar todo el iterable (que puede no ser posible). Es más robusto y eficiente usar una de las otras soluciones proporcionadas.
Mike Graham