Holen Sie sich das erste Element aus einer Iterable, die einer Bedingung entspricht

303

Ich möchte das erste Element aus einer Liste erhalten, die einer Bedingung entspricht. Es ist wichtig, dass die resultierende Methode nicht die gesamte Liste verarbeitet, was sehr groß sein kann. Zum Beispiel ist die folgende Funktion ausreichend:

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

Diese Funktion könnte ungefähr so ​​verwendet werden:

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

Ich kann mir jedoch keinen guten eingebauten Einzeiler vorstellen, mit dem ich das machen kann. Ich möchte diese Funktion nicht besonders kopieren, wenn ich nicht muss. Gibt es eine integrierte Möglichkeit, um den ersten Artikel zu erhalten, der einer Bedingung entspricht?

Chris Phillips
quelle

Antworten:

476

In Python 2.6 oder neuer:

Wenn Sie ausgelöst StopIterationwerden möchten , wenn kein passendes Element gefunden wird:

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

Wenn Sie möchten, dass default_value(z. B. None) zurückgegeben wird:

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

Beachten Sie, dass Sie in diesem Fall ein zusätzliches Klammerpaar um den Generatorausdruck benötigen - diese werden immer dann benötigt, wenn der Generatorausdruck nicht das einzige Argument ist.

Ich sehe, dass die meisten Antworten die nextintegrierten Antworten entschlossen ignorieren, und gehe daher aus mysteriösen Gründen davon aus, dass sie sich zu 100% auf Versionen 2.5 und älter konzentrieren - ohne das Problem mit der Python-Version zu erwähnen (aber dann sehe ich diese Erwähnung nicht in die Antworten, die das Eingebaute erwähnen next, weshalb ich es für notwendig hielt, selbst eine Antwort zu geben - zumindest das Problem der "richtigen Version" wird auf diese Weise aufgezeichnet ;-).

In 2.5 wird die .next()Methode der Iteratoren sofort ausgelöst, StopIterationwenn der Iterator sofort beendet ist - dh für Ihren Anwendungsfall, wenn kein Element in der Iterierbarkeit die Bedingung erfüllt. Wenn Sie kümmern sich nicht (dh, wissen Sie , es muss mindestens ein zufriedenstellendes Element sein) , dann benutzen Sie einfach .next()(am besten auf einem genexp, Linie für die nextEinbau-in Python 2.6 und besser).

Wenn Sie sich darum kümmern, scheint es am besten, Dinge in eine Funktion zu verpacken, wie Sie sie zuerst in Ihrem Q angegeben haben, und obwohl die von Ihnen vorgeschlagene Funktionsimplementierung in Ordnung ist, können Sie alternativ itertoolseine for...: breakSchleife oder einen Genexp oder a try/except StopIterationals Funktionskörper verwenden , wie verschiedene Antworten vorschlugen. In keiner dieser Alternativen steckt viel Mehrwert, daher würde ich mich für die sehr einfache Version entscheiden, die Sie zuerst vorgeschlagen haben.

Alex Martelli
quelle
6
Funktioniert nicht wie beschrieben. Es wird ausgelöst, StopIterationwenn kein Element gefunden wurde
Suor
Da dies in den Suchergebnissen auftaucht, habe ich den Kommentar von @ Suor aus dem Jahr 2011 befolgt und den ersten Absatz ein wenig umformuliert, um die Dinge klarer zu machen. Bitte ändern Sie meine Bearbeitung, falls erforderlich.
Kos
4
Da dies die ausgewählte Antwort ist, fühle ich mich gezwungen, eine Antwort auf die richtige Auswahl des ersten Elements hier zu teilen . Kurz gesagt: Die Verwendung von next sollte nicht gefördert werden.
Guyarad
1
@guyarad Wie ist die in dieser Antwort vorgeschlagene Lösung weniger "kryptisch" als nur die nächste? Das einzige Argument gegen next (in dieser Antwort) ist, dass Sie eine Ausnahme behandeln müssen; Ja wirklich ?
Abraham TS
Meine Ansicht ist etwas anders als zu der Zeit, als ich den Kommentar geschrieben habe. Ich weiß, worauf du hinauswillst. Das heißt, damit umgehen zu müssen StopIterationist wirklich nicht schön. Verwenden Sie besser eine Methode.
Guyarad
29

Als wiederverwendbare, dokumentierte und getestete Funktion

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 mit Standardargument

@zorf hat eine Version dieser Funktion vorgeschlagen, bei der Sie einen vordefinierten Rückgabewert haben können, wenn die Iterable leer ist oder keine Elemente vorhanden sind, die der Bedingung entsprechen:

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
quelle
6
Wenn Sie es mit einer Methode umschließen, fangen Sie mindestens StopIteration ab und lösen Sie den EmptySequence-Fehler aus. Wäre viel schöner, wenn es keine Elemente gibt.
Guyarad
@guyarad Ist das eine Art ValueError?
Caridorc
2
@guyarad StopIterationist die kanonische Ausnahme "out of elements" in Python. Ich sehe kein Problem damit, dass es geworfen wird. Ich würde wahrscheinlich den Standardwert "None" verwenden, der als Standardparameter an die Funktion übergeben werden kann.
Baldrickk
1
Baldrickk Ich denke, dies ist keine Iterationsmethode. Sie werden dies nicht in einem Wettbewerb eines Iterators nennen. Aber ich fühle mich nicht zu stark dabei :)
Guyarad
1
Es sollte ein optionales Standardargument geben. Wenn dieses Argument nicht angegeben wird, wird nur dann eine Ausnahme ausgelöst, wenn kein Element in der Sequenz die Bedingung erfüllt.
Zorf
28

Verdammte Ausnahmen!

Ich liebe diese Antwort . Da jedoch next()eine StopIterationAusnahme ausgelöst wird, wenn keine Elemente vorhanden sind, würde ich das folgende Snippet verwenden, um eine Ausnahme zu vermeiden:

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

Zum Beispiel,

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

Wird eine StopIterationAusnahme auslösen;

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

Ähnlich wie bei der Verwendung ifilterkönnen Sie einen Generatorausdruck verwenden:

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

In beiden Fällen möchten Sie wahrscheinlich jedoch fangen StopIteration, falls keine Elemente Ihre Bedingung erfüllen.

Technisch gesehen könnten Sie so etwas tun:

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

Es würde vermeiden, einen try/exceptBlock machen zu müssen. Aber das scheint irgendwie dunkel und missbräuchlich für die Syntax.

Matt Anderson
quelle
+1: Nicht dunkel oder missbräuchlich. Alles in allem scheint der letzte ziemlich sauber zu sein.
S.Lott
6
Der letzte ist überhaupt nicht sauber - for foo in genex: breakist nur eine Möglichkeit, foo = next(genex)ohne die Zuordnung klar zu machen, und mit der Ausnahme, dass sie ausgelöst wird, wenn die Operation keinen Sinn ergibt, wenn sie gequetscht wird. In Python ist es normalerweise eine schlechte Sache, einen Fehlercode zu erhalten, anstatt eine Ausnahme abzufangen.
Mike Graham
13

Der effizienteste Weg in Python 3 ist einer der folgenden (anhand eines ähnlichen Beispiels):

Mit "Verständnis" -Stil:

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

WARNUNG : Der Ausdruck funktioniert auch mit Python 2, wird jedoch im Beispiel verwendet range, das ein iterierbares Objekt in Python 3 anstelle einer Liste wie Python 2 zurückgibt (wenn Sie ein iterierbares Objekt in Python 2 erstellen möchten, verwenden Siexrange stattdessen).

Beachten Sie, dass der Ausdruck es vermeidet, eine Liste im Verständnisausdruck next([i for ...])zu erstellen, was dazu führen würde, dass eine Liste mit allen Elementen erstellt wird, bevor die Elemente gefiltert werden, und dass die gesamten Optionen verarbeitet werden, anstatt die Iteration einmal zu stoppen i == 1000.

Mit "funktionalem" Stil:

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

WARNUNG : Dies funktioniert in Python 2 nicht, selbst wenn es rangedurch xrangeDue ersetzt wird, filterdas eine Liste anstelle eines Iterators (ineffizient) erstelltnext Funktion funktioniert nur mit Iteratoren.

Standardwert

Wie in anderen Antworten erwähnt, müssen Sie der Funktion einen zusätzlichen Parameter hinzufügen, nextwenn Sie eine Ausnahme vermeiden möchten, die ausgelöst wird, wenn die Bedingung nicht erfüllt ist.

"funktionaler" Stil:

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

"Verständnis" -Stil:

Bei diesem Stil müssen Sie den Verständnisausdruck mit umgeben (), um Folgendes zu vermeiden SyntaxError: Generator expression must be parenthesized if not sole argument:

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

Ich würde das schreiben

next(x for x in xrange(10) if x > 3)
Mike Graham
quelle
Ich denke, i > 3sollte x > 3in Ihrem Beispiel sein
Ricky Robinson
6

Das itertoolsModul enthält eine Filterfunktion für Iteratoren. Das erste Element des gefilterten Iterators kann durch Aufrufen erhalten werden next():

from itertools import ifilter

print ifilter((lambda i: i > 3), range(10)).next()
etw
quelle
2
Generatorausdrücke sind einfacher.
Eric O Lebigot
1
( i) filterund ( i) mapkönnen in Fällen sinnvoll sein, in denen die angewendeten Funktionen bereits vorhanden sind. In einer solchen Situation ist es jedoch viel sinnvoller, nur einen Generatorausdruck zu verwenden.
Mike Graham
Dies ist die beste Antwort. Vermeiden Sie Listenverständnisse xahlee.info/comp/list_comprehension.html
mit
6

Für ältere Versionen von Python, in denen die nächste integrierte Version nicht vorhanden ist:

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

Durch die Nutzung

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

Man kann den Zustand des Wertes des ersten Elements in the_iterable überprüfen und seinen Index erhalten, ohne alle Elemente in the_iterable auswerten zu müssen .

Der vollständige zu verwendende Ausdruck ist

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

Hier nimmt first_index den Wert des ersten Werts an, der in dem oben diskutierten Ausdruck identifiziert wurde.

blue_note
quelle
4

Diese Frage hat bereits gute Antworten. Ich addiere nur meine zwei Cent, weil ich hier gelandet bin, um eine Lösung für mein eigenes Problem zu finden, das dem OP sehr ähnlich ist.

Wenn Sie mithilfe von Generatoren den INDEX des ersten Elements ermitteln möchten, das einem Kriterium entspricht, können Sie einfach Folgendes tun:

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

Sie können die argwhereFunktion auch in Numpy verwenden. Zum Beispiel:

i) Finde das erste "l" in "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) Finden Sie die erste Zufallszahl> 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) Finden Sie die letzte Zufallszahl> 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()
Ziel
quelle
-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: Ich dachte, es wäre offensichtlich, aber anscheinend nicht: Stattdessen Nonekönnen Sie eine Funktion (oder a lambda) mit einer Überprüfung auf die Bedingung übergeben:

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

Einzeiler:

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

Wenn Sie nicht sicher sind, ob ein Element gemäß den Kriterien gültig ist, sollten Sie dies einschließen, try/exceptda [0]dies eine auslösen kann IndexError.

Mizipzor
quelle
TypeError: 'Generator'-Objekt ist nicht abonnierbar
Josh Lee
Mein schlechtes, sollte Listenverständnis kein Generator sein, behoben ... danke! :)
Mizipzor
2
Es gibt keinen Grund, das gesamte iterable zu bewerten (was möglicherweise nicht möglich ist). Es ist robuster und effizienter, eine der anderen bereitgestellten Lösungen zu verwenden.
Mike Graham