Obtenez le premier élément d'un itérable qui correspond à une condition

303

Je souhaite obtenir le premier élément d'une liste correspondant à une condition. Il est important que la méthode résultante ne traite pas la liste entière, ce qui pourrait être assez volumineux. Par exemple, la fonction suivante est adéquate:

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

Cette fonction pourrait être utilisée quelque chose comme ceci:

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

Cependant, je ne peux pas penser à un bon intégré / un-doublure pour me permettre de faire cela. Je ne veux pas particulièrement copier cette fonction si je n'ai pas à le faire. Existe-t-il un moyen intégré pour obtenir le premier élément correspondant à une condition?

Chris Phillips
la source

Réponses:

477

En Python 2.6 ou plus récent:

Si vous souhaitez StopIterationêtre relevé si aucun élément correspondant n'est trouvé:

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

Si vous souhaitez default_value(par exemple None) être retourné à la place:

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

Notez que vous avez besoin d'une paire de parenthèses supplémentaires autour de l'expression de générateur dans ce cas - elles sont nécessaires chaque fois que l'expression de générateur n'est pas le seul argument.

Je vois que la plupart des réponses ignorent résolument le nextintégré et je suppose donc que pour une raison mystérieuse, ils sont 100% concentrés sur les versions 2.5 et antérieures - sans mentionner le problème de la version Python (mais je ne vois pas cette mention dans les réponses qui font mention du next, intégré qui est la raison pour laquelle je pensais qu'il est nécessaire de fournir une réponse moi - même - au moins la question « version correcte » obtient enregistrée cette façon ;-).

En 2.5, la .next()méthode des itérateurs augmente immédiatement StopIterationsi l'itérateur se termine immédiatement - c'est-à-dire, pour votre cas d'utilisation, si aucun élément de l'itérable ne satisfait la condition. Si vous ne vous en souciez pas (c'est-à-dire que vous savez qu'il doit y avoir au moins un élément satisfaisant), alors utilisez-le .next()(mieux sur un genexp, ligne pour le nextPython 2.6 intégré et mieux).

Si vous vous en souciez, envelopper les choses dans une fonction comme vous l'aviez indiqué dans votre Q semble le mieux, et bien que l'implémentation de la fonction que vous avez proposée soit très bien, vous pouvez également utiliser itertools, une for...: breakboucle, ou un genexp, ou un try/except StopIterationcomme corps de la fonction , comme l'ont suggéré diverses réponses. Il n'y a pas beaucoup de valeur ajoutée dans aucune de ces alternatives, donc j'opterais pour la version simple et simple que vous avez proposée en premier.

Alex Martelli
la source
6
Ne fonctionne pas comme vous le décrivez. Il augmente StopIterationquand aucun élément n'a été trouvé
Suor
Comme cela apparaît dans les résultats de recherche, j'ai suivi le commentaire de @ Suor de 2011 et reformulé un peu le premier paragraphe pour clarifier les choses. Veuillez continuer et modifier ma modification si vous en avez besoin.
Kos
4
Puisque c'est la réponse choisie, je me sens obligé de partager une réponse pour sélectionner correctement le premier élément ici . En bref: l'utilisation de next ne doit pas être encouragée.
guyarad le
1
@guyarad comment la solution proposée dans cette réponse est-elle moins "cryptique" que la simple utilisation suivante? Le seul argument contre next (dans cette réponse) est que vous devez gérer une exception; vraiment ?
Abraham TS
Mon point de vue est un peu différent du moment où j'ai écrit le commentaire. Je vois ce que tu veux dire. Cela étant dit, avoir à gérer StopIterationn'est vraiment pas joli. Mieux vaut utiliser une méthode.
guyarad
29

En tant que fonction réutilisable, documentée et testée

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

Version avec argument par défaut

@zorf a suggéré une version de cette fonction où vous pouvez avoir une valeur de retour prédéfinie si l'itérable est vide ou n'a aucun élément correspondant à la condition:

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
la source
6
Si vous l'encapsulez avec une méthode, interceptez au moins StopIteration et déclenchez l'erreur EmptySequence. Ce serait beaucoup plus joli en l'absence d'éléments.
guyarad
@guyarad Est-ce une sorte de ValueError?
Caridorc
2
@guyarad StopIterationest l'exception canonique "hors éléments" en python. Je ne vois aucun problème à ce qu'il soit lancé. J'utiliserais probablement une valeur par défaut "None" qui peut être transmise comme paramètre par défaut à la fonction.
Baldrickk
1
Baldrickk J'ai l'impression que ce n'est pas une méthode d'itération. Vous n'appellerez pas celui-ci dans un concours d'itérateur. Mais je ne le ressens pas trop :)
guyarad
1
Il doit y avoir un argument par défaut facultatif, et si cet argument n'est pas fourni, déclenchez alors une exception si aucun élément de la séquence ne remplit la condition.
Zorf
28

Merde exceptions!

J'adore cette réponse . Cependant, puisque next()lever une StopIterationexception lorsqu'il n'y a aucun élément, j'utiliserais l'extrait de code suivant pour éviter une exception:

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

Par exemple,

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

Lève une StopIterationexception;

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

Similaire à l'utilisation ifilter, vous pouvez utiliser une expression de générateur:

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

Dans les deux cas, vous voudrez probablement attraper StopIteration, au cas où aucun élément ne satisferait votre condition.

Techniquement parlant, je suppose que vous pourriez faire quelque chose comme ça:

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

Cela éviterait d'avoir à faire un try/exceptbloc. Mais cela semble un peu obscur et abusif pour la syntaxe.

Matt Anderson
la source
+1: Pas obscur, ni abusif. Tout bien considéré, le dernier semble assez propre.
S.Lott
6
Le dernier n'est pas du tout propre - for foo in genex: breakc'est juste une façon de faire foo = next(genex)sans clarifier l'affectation et à l'exception qui serait levée si l'opération n'avait pas de sens d'être écrasée. Se retrouver avec un code d'échec au lieu d'attraper une exception est généralement une mauvaise chose en Python.
Mike Graham
13

Le moyen le plus efficace en Python 3 est l'un des suivants (en utilisant un exemple similaire):

Avec le style "compréhension" :

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

AVERTISSEMENT : l'expression fonctionne également avec Python 2, mais dans l'exemple est utilisé rangequi retourne un objet itérable en Python 3 au lieu d'une liste comme Python 2 (si vous voulez construire un itérable en Python 2, utilisez xrangeplutôt).

Notez que l'expression évite de construire une liste dans l'expression de compréhension next([i for ...]), ce qui entraînerait la création d'une liste avec tous les éléments avant de filtrer les éléments, et entraînerait le traitement de toutes les options, au lieu d'arrêter l'itération une fois i == 1000.

Avec un style "fonctionnel" :

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

AVERTISSEMENT : cela ne fonctionne pas en Python 2, même en le remplaçant rangeparxrange due qui filtercrée une liste au lieu d'un itérateur (inefficace), et la nextfonction ne fonctionne qu'avec des itérateurs.

Valeur par défaut

Comme mentionné dans d'autres réponses, vous devez ajouter un paramètre supplémentaire à la fonction nextsi vous souhaitez éviter une exception déclenchée lorsque la condition n'est pas remplie.

style "fonctionnel" :

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

style "compréhension" :

Avec ce style, vous devez entourer l'expression de compréhension ()pour éviter SyntaxError: Generator expression must be parenthesized if not sole argument:

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

J'écrirais ceci

next(x for x in xrange(10) if x > 3)
Mike Graham
la source
Je suppose que cela i > 3devrait être x > 3dans votre exemple
Ricky Robinson
6

Le itertoolsmodule contient une fonction de filtrage pour les itérateurs. Le premier élément de l'itérateur filtré peut être obtenu en l'appelant next():

from itertools import ifilter

print ifilter((lambda i: i > 3), range(10)).next()
qch
la source
2
Les expressions de générateur sont plus simples.
Eric O Lebigot
1
( i) filteret ( i) mappeuvent avoir un sens dans les cas où les fonctions appliquées existent déjà, mais dans une situation comme celle-ci, il est beaucoup plus logique d'utiliser une expression de générateur.
Mike Graham
C'est la meilleure réponse. Évitez la liste compréhensions xahlee.info/comp/list_comprehension.html~~V~~plural~~3rd
mit
6

Pour les anciennes versions de Python où le prochain intégré n'existe pas:

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

En utilisant

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

on peut vérifier la condition de la valeur du premier élément dans the_iterable , et obtenir son index sans avoir besoin d'évaluer tous les éléments dans the_iterable .

L'expression complète à utiliser est

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

Ici, first_index suppose la valeur de la première valeur identifiée dans l'expression discutée ci-dessus.

note bleue
la source
4

Cette question a déjà d'excellentes réponses. J'ajoute seulement mes deux cents parce que j'ai atterri ici en essayant de trouver une solution à mon propre problème, qui est très similaire au PO.

Si vous souhaitez trouver l'INDEX du premier élément correspondant à un critère à l'aide de générateurs, vous pouvez simplement faire:

next(index for index, value in enumerate(iterable) if condition)
dangom
la source
0

Vous pouvez également utiliser le argwhere fonction dans Numpy. Par exemple:

i) Trouvez le premier "l" dans "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) Trouver le premier nombre aléatoire> 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) Trouver le dernier nombre aléatoire> 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()
objectif
la source
-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

EDIT: Je pensais que c'était évident, mais apparemment pas: au lieu de cela, Nonevous pouvez passer une fonction (ou a lambda) avec une vérification de la condition:

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

Bon mot:

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

Si vous n'êtes pas sûr qu'un élément sera valide selon les critères, vous devez le joindre try/exceptcar cela [0]peut générer un IndexError.

Mizipzor
la source
TypeError: l'objet 'générateur' n'est pas inscriptible
Josh Lee
Mon mauvais, devrait être la liste de compréhension pas un générateur, réparé ... merci! :)
Mizipzor
2
Il n'y a aucune raison d'évaluer l'ensemble de l'itérable (ce qui n'est peut-être pas possible). Il est plus robuste et efficace d'utiliser l'une des autres solutions proposées.
Mike Graham