Ottieni il primo elemento da un iterabile che corrisponde a una condizione


303

Vorrei ottenere il primo elemento da un elenco corrispondente a una condizione. È importante che il metodo risultante non elabori l'intero elenco, che potrebbe essere abbastanza grande. Ad esempio, la seguente funzione è adeguata:

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

Questa funzione potrebbe essere usata in questo modo:

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

Tuttavia, non riesco a pensare a un buon built-in / one-liner per farmi fare questo. In particolare, non voglio copiare questa funzione se non è necessario. Esiste un modo integrato per ottenere il primo elemento corrispondente a una condizione?


Risposte:


476

In Python 2.6 o versioni successive:

Se vuoi StopIterationessere sollevato se non viene trovato alcun elemento corrispondente:

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

Se invece vuoi default_value(ad esempio None) essere restituito:

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

Si noti che in questo caso è necessaria una coppia di parentesi aggiuntiva attorno all'espressione del generatore: sono necessarie ogni volta che l'espressione del generatore non è l'unico argomento.

Vedo che la maggior parte delle risposte ignora risolutamente il nextbuilt-in e quindi presumo che per qualche misteriosa ragione siano focalizzate al 100% sulle versioni 2.5 e precedenti - senza menzionare il problema della versione di Python (ma poi non vedo quella menzione in le risposte che fanno parlare dellanext built-in, che è il motivo per cui ho ritenuto necessario per fornire una risposta me stesso - almeno la questione "versione corretta" ottiene il disco in questo modo ;-).

In 2.5, il .next()metodo degli iteratori aumenta immediatamente StopIterationse l'iteratore termina immediatamente, ovvero, per il tuo caso d'uso, se nessun elemento nell'iterabile soddisfa la condizione. Se non ti interessa (ad esempio, sai che ci deve essere almeno un elemento soddisfacente), usa semplicemente .next()(meglio su un genexp, linea per il nextPython 2.6 integrato e meglio).

Se fai attenzione, le cose di avvolgimento in una funzione come avevate prima indicato nel vostro Q sembra migliore, e mentre l'implementazione della funzione avete proposto va bene, si può utilizzare in alternativa itertools, un for...: breakloop, o un genexp, o try/except StopIterationcome il corpo della funzione , come suggerito da varie risposte. Non c'è molto valore aggiunto in nessuna di queste alternative, quindi sceglierei la versione assolutamente semplice che hai proposto per la prima volta.


6
Non funziona come descritto. Si alza StopIterationquando nessun elemento trovato
Suor

Dal momento che questo emerge nei risultati di ricerca, ho seguito il commento di @ Suor del 2011 e ho riformulato un po 'il primo paragrafo per rendere le cose più chiare. Vai avanti e modifica la mia modifica se necessario.
Kos,

4
Poiché questa è la risposta selezionata, mi sento in dovere di condividere una risposta alla selezione corretta del primo elemento qui . In breve: l'uso del prossimo non dovrebbe essere incoraggiato.
Guyarad,

1
@guyarad in che modo la soluzione proposta in quella risposta è meno "criptica" rispetto a quella successiva? L'unico argomento contro next (in quella risposta) è che devi gestire un'eccezione; veramente ?
Abraham TS,

La mia opinione è un po 'diversa rispetto a quando ho scritto il commento. Vedo il tuo punto. Detto questo, dover gestire non StopIterationè davvero carino. Meglio usare un metodo.
Guyarad,

29

Come funzione riutilizzabile, documentata e testata

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

Versione con argomento predefinito

@zorf ha suggerito una versione di questa funzione in cui è possibile avere un valore di ritorno predefinito se l'iterabile è vuoto o non ha elementi che soddisfano la condizione:

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
Se lo stai avvolgendo con un metodo, almeno cattura StopIteration e genera l'errore EmptySequence. Sarebbe molto più bello quando non ci sono elementi.
Guyarad,

@guyarad È una specie di ValueError?
Caridorc,

2
@guyarad StopIterationè l'eccezione canonica "fuori dagli elementi" in Python. Non vedo un problema quando viene lanciato. Probabilmente userei un valore predefinito "Nessuno" che può essere passato come parametro predefinito alla funzione.
Baldrickk,

1
Baldrickk Penso che questo non sia un metodo di iterazione. Non lo chiamerai in una gara di un iteratore. Ma non mi sento troppo forte al riguardo :)
guyarad,

1
Dovrebbe esserci un argomento predefinito facoltativo e, se tale argomento non viene fornito, solleva un'eccezione solo quando nessun elemento nella sequenza soddisfa la condizione.
Zorf,

28

Maledette eccezioni!

Adoro questa risposta . Tuttavia, poiché next()sollevo StopIterationun'eccezione quando non ci sono articoli, vorrei usare il seguente frammento per evitare un'eccezione:

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

Per esempio,

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

Solleverà StopIterationun'eccezione;

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

13

Simile all'utilizzo ifilter, è possibile utilizzare un'espressione del generatore:

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

In entrambi i casi, probabilmente vorrai catturarlo StopIteration, nel caso in cui nessun elemento soddisfi la tua condizione.

Tecnicamente parlando, suppongo che potresti fare qualcosa del genere:

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

Eviterebbe di dover fare un try/exceptblocco. Ma sembra un po 'oscuro e offensivo per la sintassi.


+1: non oscuro, né offensivo. Tutto sommato, l'ultimo sembra abbastanza pulito.
S.Lott

6
L'ultimo non è affatto pulito - for foo in genex: breakè solo un modo di fare foo = next(genex)senza chiarire il compito e con l'eccezione che verrebbe sollevata se l'operazione non avesse senso essere schiacciata. Finendo con un codice di errore, invece di cattura un'eccezione di solito è una cattiva cosa in Python.
Mike Graham

13

Il modo più efficiente in Python 3 è uno dei seguenti (usando un esempio simile):

Con stile "comprensione" :

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

ATTENZIONE : L'espressione funziona anche con Python 2, ma nell'esempio viene utilizzato rangeche restituisce un oggetto iterabile in Python 3 anziché un elenco come Python 2 (se si desidera invece costruire un iterabile in Python 2 xrange).

Si noti che l'espressione evita di costruire un elenco nell'espressione di comprensione next([i for ...]), ciò provocherebbe la creazione di un elenco con tutti gli elementi prima di filtrare gli elementi e causerebbe l'elaborazione di tutte le opzioni, invece di interrompere l'iterazione una volta i == 1000.

Con stile "funzionale" :

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

ATTENZIONE : Questo non funziona in Python 2, anche sostituendolo rangecon xrangedue che filtercreano un elenco anziché un iteratore (inefficiente) e la nextfunzione funziona solo con gli iteratori.

Valore predefinito

Come indicato in altre risposte, è necessario aggiungere un parametro extra alla funzione nextse si desidera evitare un'eccezione sollevata quando la condizione non è soddisfatta.

stile "funzionale" :

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

stile di "comprensione" :

Con questo stile devi circondare l'espressione di comprensione ()per evitare un SyntaxError: Generator expression must be parenthesized if not sole argument:

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


6

Il itertoolsmodulo contiene una funzione filtro per iteratori. Il primo elemento dell'iteratore filtrato può essere ottenuto richiamandolo next():

from itertools import ifilter

print ifilter((lambda i: i > 3), range(10)).next()

2
Le espressioni del generatore sono più semplici.
Eric O Lebigot

1
( i) filtere ( i) mappuò avere senso per i casi in cui le funzioni applicate esistono già, ma in una situazione come questa ha molto più senso usare solo un'espressione del generatore.
Mike Graham

Questa è la risposta migliore Evita le comprensioni dell'elenco xahlee.info/comp/list_comprehension.html
mit

6

Per le versioni precedenti di Python in cui non esiste il prossimo built-in:

(x for x in range(10) if x > 3).next()

5

Usando

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

si può verificare la condizione del valore del primo elemento in the_iterable e ottenere il suo indice senza la necessità di valutare tutti gli elementi in the_iterable .

L'espressione completa da usare è

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

Qui first_index assume il valore del primo valore identificato nell'espressione discussa sopra.


4

Questa domanda ha già ottime risposte. Sto solo aggiungendo i miei due centesimi perché sono atterrato qui cercando di trovare una soluzione al mio problema, che è molto simile al PO.

Se vuoi trovare l'INDICE del primo oggetto che corrisponde a un criterio usando i generatori, puoi semplicemente fare:

next(index for index, value in enumerate(iterable) if condition)


0

Puoi anche usare la argwherefunzione in Numpy. Per esempio:

i) Trova la prima "l" in "mondo":

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) Trova il primo numero casuale> 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) Trova l'ultimo numero casuale> 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

In Python 3:

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

In Python 2.6:

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

EDIT: ho pensato che fosse ovvio, ma apparentemente no: invece di Nonete puoi passare una funzione (o a lambda) con un controllo per la condizione:

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]

Se non sei sicuro che qualsiasi elemento sia valido in base ai criteri, dovresti racchiuderlo in try/exceptquanto ciò [0]può generare un IndexError.


TypeError: l'oggetto 'generator' non è iscrivibile
Josh Lee

Mio male, dovrebbe essere la comprensione della lista non un generatore, risolto ... grazie! :)
Mizipzor

2
Non c'è motivo di valutare l'intero iterabile (che potrebbe non essere possibile). È più robusto ed efficiente utilizzare una delle altre soluzioni fornite.
Mike Graham
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.