Rechercher le premier élément d'une séquence qui correspond à un prédicat

171

Je veux une manière idiomatique de trouver le premier élément d'une liste qui correspond à un prédicat.

Le code actuel est assez moche:

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

J'ai pensé à le changer en:

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

Mais il doit y avoir quelque chose de plus élégant ... Et ce serait bien s'il renvoie une Nonevaleur plutôt que de lever une exception si aucune correspondance n'est trouvée.

Je sais que je pourrais simplement définir une fonction comme:

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

Mais il est assez insipide de commencer à remplir le code avec des fonctions utilitaires comme celle-ci (et les gens ne remarqueront probablement pas qu'elles sont déjà là, donc elles ont tendance à se répéter au fil du temps) s'il y a des inserts intégrés qui fournissent déjà la même chose.

fortran
la source
3
En plus d'être posée plus tard que " fonction de recherche de séquence python ", cette question a un bien meilleur titre .
Wolf

Réponses:

250

Pour rechercher le premier élément d'une séquence seqqui correspond à un predicate:

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

Ou ( itertools.ifiltersur Python 2) :

next(filter(predicate, seq))

Il augmente StopIterations'il n'y en a pas.


Pour retourner Nones'il n'y a pas un tel élément:

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

Ou:

next(filter(predicate, seq), None)
jfs
la source
28
Ou vous pouvez fournir un deuxième argument "par défaut" nextqui est utilisé au lieu de lever l'exception.
Karl Knechtel
2
@fortran: next()est disponible depuis Python 2.6 Vous pouvez lire la page Quoi de neuf pour vous familiariser rapidement avec les nouvelles fonctionnalités.
jfs
1
Je suis un novice en python et je lis la documentation et l'ifilter utilise la méthode "yield". Je suppose que cela signifie que le prédicat est évalué paresseusement au fur et à mesure. c'est-à-dire que nous n'exécutons pas le prédicat sur toute la liste car j'ai une fonction de prédicat un peu chère et je veux seulement itérer jusqu'au point où nous trouvons un élément
Kannan Ekanath
2
@geekazoid: seq.find(&method(:predicate))ou même plus concis par exemple les méthodes par exemple:[1,1,4].find(&:even?)
jfs
16
ifiltera été renommé filteren Python 3.
tsauerwein
92

Vous pouvez utiliser une expression de générateur avec une valeur par défaut, puis next:

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

Bien que pour ce one-liner, vous devez utiliser Python> = 2.6.

Cet article plutôt populaire traite plus en détail de ce problème: Fonction de recherche dans la liste Python la plus propre? .

Chewie
la source
8

Je ne pense pas qu'il y ait quelque chose qui cloche dans les solutions que vous avez proposées dans votre question.

Dans mon propre code, je l'implémenterais comme ceci:

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

La syntaxe avec ()crée un générateur, ce qui est plus efficace que de générer toute la liste en même temps avec [].

Mac
la source
Et pas seulement cela - avec []vous pourriez rencontrer des problèmes si l'itérateur ne se termine jamais ou si ses éléments sont difficiles à créer, plus il
tarde
6
'generator' object has no attribute 'next'sur Python 3.
jfs
@glglgl - Quant au premier point (ne se termine jamais) j'en doute, car l'argument est une suite finie [plus précisément une liste selon la question OP ']. Quant à la seconde: encore une fois, puisque l'argument fourni est une séquence, les objets devraient déjà avoir été créés et stockés au moment où cette fonction est appelée .... ou est-ce que je manque quelque chose?
mac
@JFSebastian - Merci, je n'étais pas au courant! :) Par curiosité, quel est le principe de conception derrière ce choix?
mac
@mac - Par souci de cohérence avec le double trait de soulignement d'autres méthodes spéciales. Voir python.org/dev/peps/pep-3114
Chewie
1

La réponse de JF Sebastian est la plus élégante mais nécessite python 2.6 comme l'a souligné fortran.

Pour la version Python <2.6, voici le meilleur que je puisse proposer:

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

Alternativement, si vous aviez besoin d'une liste plus tard (la liste gère la StopIteration), ou si vous aviez besoin de plus que la première mais pas de la totalité, vous pouvez le faire avec islice:

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

MISE À JOUR: Bien que j'utilise personnellement une fonction prédéfinie appelée first () qui intercepte un StopIteration et renvoie None, voici une amélioration possible par rapport à l'exemple ci-dessus: évitez d'utiliser filter / ifilter:

from itertools import islice,chain
chain((x for x in seq if predicate(x)),repeat(None)).next()
parité3
la source
11
Yikes! si cela se résume à cela, je ferais simplement la simple boucle "pour" avec un "si" à l'intérieur - beaucoup plus facile à lire
Nick Perkins