Trova il primo elemento in una sequenza che corrisponde a un predicato


171

Voglio un modo idiomatico per trovare il primo elemento in un elenco che corrisponda a un predicato.

Il codice attuale è abbastanza brutto:

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

Ho pensato di cambiarlo in:

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

Ma ci deve essere qualcosa di più elegante ... E sarebbe bello se restituisse un Nonevalore piuttosto che sollevare un'eccezione se non viene trovata alcuna corrispondenza.

So che potrei semplicemente definire una funzione come:

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

Ma è abbastanza insipido iniziare a riempire il codice con funzioni di utilità come questa (e probabilmente le persone non noteranno che sono già lì, quindi tendono a ripetersi nel tempo) se ci sono insiemi che forniscono già lo stesso.


3
Oltre a essere stato chiesto più tardi della " funzione di ricerca della sequenza Python ", questa domanda ha un titolo molto migliore .
Lupo,

Risposte:


250

Per trovare il primo elemento in una sequenza seqche corrisponde a predicate:

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

Oppure ( itertools.ifiltersu Python 2) :

next(filter(predicate, seq))

Si alza StopIterationse non ce n'è.


Per restituire Nonese non esiste tale elemento:

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

O:

next(filter(predicate, seq), None)

27
Oppure è possibile fornire un secondo argomento "predefinito" nextche viene utilizzato invece di sollevare l'eccezione.
Karl Knechtel,

2
@fortran: next()è disponibile da Python 2.6. Puoi leggere la pagina Novità per familiarizzare rapidamente con le nuove funzionalità.
jfs,

1
Sono un principiante di Python e leggo i documenti e l'ifilter usa il metodo "yield". Presumo che ciò significhi che il predicato viene valutato pigramente mentre procediamo. cioè, non eseguiamo il predicato attraverso l'intero elenco perché ho una funzione predicato che è un po 'costosa e voglio iterare solo fino al punto in cui troviamo un elemento
Kannan Ekanath,

2
@geekazoid: seq.find(&method(:predicate))o ancora più conciso, ad esempio, per esempio:[1,1,4].find(&:even?)
jfs

16
ifilterfu rinominato filterin Python 3.
tsauerwein il

92

È possibile utilizzare un'espressione del generatore con un valore predefinito e quindi next:

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

Sebbene per questo one-liner devi usare Python> = 2.6.

Questo articolo piuttosto popolare discute ulteriormente questo problema: la funzione di ricerca nell'elenco Python più pulita? .


8

Non credo che ci sia qualcosa di sbagliato in entrambe le soluzioni che hai proposto nella tua domanda.

Nel mio codice, lo implementerei in questo modo però:

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

La sintassi con ()crea un generatore, che è più efficiente che generare tutto l'elenco contemporaneamente [].


E non solo: con []te potresti incorrere in problemi se l'iteratore non finisce mai o se i suoi elementi sono difficili da creare, più tardi diventa ...
glglgl

6
'generator' object has no attribute 'next'su Python 3.
jfs il

@glglgl - Per quanto riguarda il primo punto (non finisce mai) ne dubito, poiché l'argomento è una sequenza finita [più precisamente un elenco secondo la domanda del PO]. Per quanto riguarda il secondo: di nuovo, poiché l'argomento fornito è una sequenza, gli oggetti dovrebbero essere già stati creati e archiviati quando questa funzione viene chiamata .... o mi manca qualcosa?
Mac,

@JFSebastian - Grazie, non ne ero a conoscenza! :) Per curiosità, qual è il principio progettuale alla base di questa scelta?
Mac,

@mac - Per coerenza con il doppio trattino basso di altri metodi speciali. Vedi python.org/dev/peps/pep-3114
Chewie

1

La risposta di JF Sebastian è molto elegante ma richiede python 2.6 come ha sottolineato Fortran.

Per la versione Python <2.6, ecco il meglio che posso inventare:

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

In alternativa, se hai bisogno di un elenco in seguito (l'elenco gestisce StopIteration) o hai bisogno di qualcosa di più del primo ma ancora non tutto, puoi farlo con islice:

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

AGGIORNAMENTO: Anche se sto personalmente usando una funzione predefinita chiamata first () che rileva StopIteration e restituisce None, ecco un possibile miglioramento rispetto all'esempio sopra: evita di usare filter / ifilter:

from itertools import islice,chain
chain((x for x in seq if predicate(x)),repeat(None)).next()

11
Yikes! se si riduce a questo, farei semplicemente il semplice ciclo "for" con un "if" al suo interno - molto più facile da leggere
Nick Perkins
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.