Suchen Sie das erste Element in einer Sequenz, die einem Prädikat entspricht

169

Ich möchte auf idiomatische Weise das erste Element in einer Liste finden, das einem Prädikat entspricht.

Der aktuelle Code ist ziemlich hässlich:

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

Ich habe darüber nachgedacht, es zu ändern in:

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

Aber es muss etwas eleganteres geben ... Und es wäre schön, wenn es einen NoneWert zurückgibt , anstatt eine Ausnahme auszulösen, wenn keine Übereinstimmung gefunden wird.

Ich weiß, ich könnte einfach eine Funktion definieren wie:

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

Es ist jedoch geschmacklos, den Code mit solchen Dienstprogrammfunktionen zu füllen (und die Leute werden wahrscheinlich nicht bemerken, dass sie bereits vorhanden sind, sodass sie sich im Laufe der Zeit wiederholen), wenn es integrierte Funktionen gibt, die diese bereits bereitstellen.

fortran
quelle
2
Diese Frage wird nicht nur später als " Python Sequence Find Function " gestellt, sondern hat auch einen viel besseren Titel .
Wolf

Antworten:

248

So finden Sie das erste Element in einer Sequenz seq, die mit a übereinstimmt predicate:

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

Oder ( itertools.ifilterauf Python 2) :

next(filter(predicate, seq))

Es wird erhöht, StopIterationwenn es keine gibt.


Um zurückzukehren, Nonewenn es kein solches Element gibt:

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

Oder:

next(filter(predicate, seq), None)
jfs
quelle
27
Oder Sie können ein zweites "Standard" -Argument angeben, nextdas verwendet wird, anstatt die Ausnahme auszulösen.
Karl Knechtel
2
@fortran: next()ist seit Python 2.6 verfügbar. Sie können die Seite " Neue Funktionen" lesen, um sich schnell mit den neuen Funktionen vertraut zu machen.
JFS
1
Ich bin ein Python-Neuling und lese die Dokumente und der ifilter verwendet die "Yield" -Methode. Ich gehe davon aus, dass dies bedeutet, dass das Prädikat im weiteren Verlauf träge ausgewertet wird. Das heißt, wir führen das Prädikat nicht durch die gesamte Liste, weil ich eine Prädikatfunktion habe, die etwas teuer ist, und ich möchte nur bis zu dem Punkt iterieren, an dem wir einen Gegenstand finden
Kannan Ekanath
2
@geekazoid: seq.find(&method(:predicate))oder noch prägnanter zum Beispiel Methoden zB:[1,1,4].find(&:even?)
jfs
16
ifilterwurde filterin Python 3 umbenannt .
Tsauerwein
92

Sie können einen Generatorausdruck mit einem Standardwert verwenden und dann next:

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

Obwohl Sie für diesen Einzeiler Python> = 2.6 verwenden müssen.

In diesem recht beliebten Artikel wird dieses Problem weiter erläutert: Sauberste Python-Funktion zum Suchen in der Liste? .

Chewie
quelle
8

Ich glaube nicht, dass an beiden Lösungen, die Sie in Ihrer Frage vorgeschlagen haben, etwas falsch ist.

In meinem eigenen Code würde ich es jedoch so implementieren:

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

Die Syntax mit ()erstellt einen Generator, der effizienter ist als das gleichzeitige Generieren der gesamten Liste mit [].

Mac
quelle
Und nicht nur das - mit []Ihnen könnten Probleme auftreten, wenn der Iterator nie endet oder seine Elemente schwer zu erstellen sind, je später er wird ...
glglgl
6
'generator' object has no attribute 'next'auf Python 3.
jfs
@glglgl - Was den ersten Punkt betrifft (endet nie), bezweifle ich dies, da das Argument eine endliche Folge ist [genauer gesagt eine Liste gemäß der OP-Frage]. Zum zweiten: Da das angegebene Argument eine Sequenz ist, sollten die Objekte zum Zeitpunkt des Aufrufs dieser Funktion bereits erstellt und gespeichert worden sein ... oder fehlt mir etwas?
Mac
@JFSebastian - Danke, das war mir nicht bewusst! :) Was ist aus Neugier das Designprinzip hinter dieser Wahl?
Mac
@mac - Zur Übereinstimmung mit dem doppelten Unterstrich anderer spezieller Methoden. Siehe python.org/dev/peps/pep-3114
Chewie
1

Die Antwort von JF Sebastian ist am elegantesten, erfordert jedoch Python 2.6, wie fortran hervorhob.

Für Python-Version <2.6 ist hier das Beste, was ich mir vorstellen kann:

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

Wenn Sie später eine Liste benötigen (Liste behandelt die StopIteration) oder mehr als nur die erste, aber immer noch nicht alle benötigen, können Sie dies mit islice tun:

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

UPDATE: Obwohl ich persönlich eine vordefinierte Funktion namens first () verwende, die eine StopIteration abfängt und None zurückgibt, ist hier eine mögliche Verbesserung gegenüber dem obigen Beispiel: Vermeiden Sie die Verwendung von filter / ifilter:

from itertools import islice,chain
chain((x for x in seq if predicate(x)),repeat(None)).next()
Parität3
quelle
11
Huch! Wenn es darauf ankommt, würde ich einfach die einfache "for" -Schleife mit einem "if" darin machen - viel einfacher zu lesen
Nick Perkins