Encuentre el primer elemento en una secuencia que coincida con un predicado

171

Quiero una forma idiomática de encontrar el primer elemento en una lista que coincida con un predicado.

El código actual es bastante feo:

[x for x in seq if predicate(x)][0]

He pensado en cambiarlo a:

from itertools import dropwhile
dropwhile(lambda x: not predicate(x), seq).next()

Pero debe haber algo más elegante ... Y sería bueno si devuelve un Nonevalor en lugar de generar una excepción si no se encuentra una coincidencia.

Sé que podría definir una función como:

def get_first(predicate, seq):
    for i in seq:
        if predicate(i): return i
    return None

Pero es bastante insípido comenzar a llenar el código con funciones de utilidad como esta (y las personas probablemente no notarán que ya están allí, por lo que tienden a repetirse con el tiempo) si hay elementos integrados que ya proporcionan lo mismo.

fortran
fuente
3
Además de hacerse más tarde que " función de búsqueda de secuencia de Python ", esta pregunta tiene un título mucho mejor .
Wolf

Respuestas:

250

Para buscar el primer elemento en una secuencia seqque coincida con predicate:

next(x for x in seq if predicate(x))

O ( itertools.ifilteren Python 2) :

next(filter(predicate, seq))

Se eleva StopIterationsi no hay ninguno.


Para volver Nonesi no hay tal elemento:

next((x for x in seq if predicate(x)), None)

O:

next(filter(predicate, seq), None)
jfs
fuente
28
O puede proporcionar un segundo argumento "predeterminado" para nextque se use en lugar de generar la excepción.
Karl Knechtel
2
@fortran: next()está disponible desde Python 2.6. Puede leer la página Novedades para familiarizarse rápidamente con las nuevas funciones.
jfs
1
Soy un novato de Python y leo los documentos y el ifilter utiliza el método "rendimiento". Supongo que esto significa que el predicado se evalúa perezosamente a medida que avanzamos. es decir, no ejecutamos el predicado en toda la lista porque tengo una función de predicado que es un poco costosa y solo quiero iterar hasta el punto en el que encontramos un artículo
Kannan Ekanath,
2
@geekazoid: seq.find(&method(:predicate))o métodos aún más concisos, por ejemplo:[1,1,4].find(&:even?)
jfs
16
ifilterfue renombrado filteren Python 3.
tsauerwein
8

No creo que haya nada malo con ninguna de las soluciones que propusiste en tu pregunta.

Sin embargo, en mi propio código, lo implementaría así:

(x for x in seq if predicate(x)).next()

La sintaxis con ()crea un generador, que es más eficiente que generar toda la lista a la vez con [].

Mac
fuente
Y no solo eso: con []usted podría tener problemas si el iterador nunca termina o si sus elementos son difíciles de crear, más tarde se vuelve ...
glglgl
66
'generator' object has no attribute 'next'en Python 3.
jfs
@glglgl - En cuanto al primer punto (nunca termina) lo dudo, ya que el argumento es una secuencia finita [más precisamente una lista de acuerdo con la pregunta del OP]. En cuanto al segundo: una vez más, dado que el argumento proporcionado es una secuencia, los objetos ya deberían haber sido creados y almacenados cuando se llama a esta función ... ¿o me falta algo?
mac
@JFSebastian - ¡Gracias, no estaba al tanto de eso! :) Por curiosidad, ¿cuál es el principio de diseño detrás de esta elección?
mac
@mac: para mantener la coherencia con el doble guión bajo de otros métodos especiales. Ver python.org/dev/peps/pep-3114
Chewie
1

La respuesta de JF Sebastian es muy elegante, pero requiere python 2.6, como señaló Fortran.

Para la versión Python <2.6, aquí está lo mejor que se me ocurre:

from itertools import repeat,ifilter,chain
chain(ifilter(predicate,seq),repeat(None)).next()

Alternativamente, si necesita una lista más tarde (la lista maneja StopIteration), o si necesita más que solo el primero pero aún no todo, puede hacerlo con islice:

from itertools import islice,ifilter
list(islice(ifilter(predicate,seq),1))

ACTUALIZACIÓN: Aunque personalmente estoy usando una función predefinida llamada first () que detecta una StopIteration y devuelve None, aquí hay una posible mejora con respecto al ejemplo anterior: evite usar filter / ifilter:

from itertools import islice,chain
chain((x for x in seq if predicate(x)),repeat(None)).next()
paridad3
fuente
11
¡Ay! si todo se reduce a eso, simplemente haría el simple bucle "para" con un "si" dentro de él, mucho más fácil de leer
Nick Perkins