Dapatkan item pertama dari iterable yang cocok dengan kondisi

303

Saya ingin mendapatkan item pertama dari daftar yang cocok dengan suatu syarat. Sangat penting bahwa metode yang dihasilkan tidak memproses seluruh daftar, yang bisa jadi cukup besar. Misalnya, fungsi berikut memadai:

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

Fungsi ini dapat digunakan seperti ini:

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

Namun, saya tidak bisa memikirkan built-in / one-liner yang bagus untuk membiarkan saya melakukan ini. Saya khususnya tidak ingin menyalin fungsi ini jika saya tidak perlu. Apakah ada cara bawaan untuk mendapatkan item pertama yang cocok dengan suatu kondisi?

Chris Phillips
sumber

Jawaban:

476

Dalam Python 2.6 atau yang lebih baru:

Jika Anda ingin StopIterationdinaikkan jika tidak ada elemen yang cocok ditemukan:

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

Jika Anda ingin default_value(misalnya None) dikembalikan:

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

Perhatikan bahwa Anda memerlukan sepasang tanda kurung tambahan di sekitar ekspresi generator dalam kasus ini - mereka diperlukan setiap kali ekspresi generator bukan satu-satunya argumen.

Saya melihat sebagian besar jawaban dengan tegas mengabaikan nextbuilt-in dan jadi saya berasumsi bahwa untuk beberapa alasan misterius mereka 100% fokus pada versi 2.5 dan yang lebih tua - tanpa menyebutkan masalah versi Python (tapi kemudian saya tidak melihat bahwa menyebutkan dalam jawaban yang melakukan menyebutkan nextbuilt-in, yang mengapa saya pikir itu diperlukan untuk memberikan jawaban sendiri - setidaknya "versi yang benar" masalah mendapat catatan cara ini ;-).

Dalam 2.5, .next()metode iterator segera naik StopIterationjika iterator segera selesai - yaitu, untuk kasus penggunaan Anda, jika tidak ada item di iterable yang memenuhi syarat. Jika Anda tidak peduli (yaitu, Anda tahu harus ada setidaknya satu item yang memuaskan) maka gunakan saja .next()(terbaik pada genexp, baris untuk nextbuilt-in di Python 2.6 dan lebih baik).

Jika Anda benar- benar peduli, membungkus hal-hal dalam suatu fungsi seperti yang pertama kali Anda tunjukkan dalam Q Anda tampaknya terbaik, dan sementara implementasi fungsi yang Anda usulkan baik-baik saja, Anda bisa menggunakan itertools, for...: breakloop, atau genexp, atau try/except StopIterationsebagai tubuh fungsi sebagai alternatif . , seperti yang disarankan berbagai jawaban. Tidak ada banyak nilai tambah di salah satu alternatif ini jadi saya akan pergi untuk versi sederhana-sederhana yang pertama kali Anda usulkan.

Alex Martelli
sumber
6
Tidak berfungsi seperti yang Anda gambarkan. Itu memunculkan StopIterationketika tidak ada elemen ditemukan
Suor
Karena ini muncul dalam hasil pencarian, saya telah mengikuti komentar @ Suor dari 2011 dan sedikit menulis ulang paragraf pertama untuk membuat semuanya lebih jelas. Silakan lanjutkan dan ubah edit saya jika perlu.
Kos
4
Karena ini adalah jawaban yang dipilih, saya merasa terdorong untuk membagikan jawaban untuk memilih elemen pertama dengan benar di sini . Singkatnya: penggunaan berikutnya tidak harus didorong.
guyarad
1
@guyarad bagaimana solusi yang diusulkan dalam jawaban itu kurang "samar" daripada hanya menggunakan selanjutnya? Satu-satunya argumen yang menentang berikutnya (dalam jawaban itu) adalah bahwa Anda harus menangani pengecualian; Betulkah ?
Abraham TS
Pandangan saya sedikit berbeda dari waktu saya menulis komentar. Saya mengerti maksud Anda. Itu dikatakan, harus menangani StopIterationbenar-benar tidak cantik. Lebih baik gunakan metode.
guyarad
29

Sebagai fungsi yang dapat digunakan kembali, didokumentasikan dan diuji

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

Versi dengan argumen default

@ zorf menyarankan versi fungsi ini di mana Anda dapat memiliki nilai kembali yang telah ditentukan jika iterable kosong atau tidak memiliki item yang cocok dengan ketentuan:

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
sumber
6
Jika Anda membungkusnya dengan metode, setidaknya tangkap StopIteration dan naikkan kesalahan EmptySequence. Akan jauh lebih cantik ketika tidak ada elemen.
guyarad
@guyarad Apakah itu semacam ValueError?
Caridorc
2
@guyarad StopIterationadalah pengecualian "dari elemen" kanonik dalam python. Saya tidak melihat masalah dengan itu dilemparkan. Saya mungkin akan menggunakan default "Tidak Ada" yang dapat diteruskan sebagai parameter default ke fungsi.
Baldrickk
1
Baldrickk Saya merasa ini bukan metode iterasi. Anda tidak akan memanggil yang ini dalam kontes iterator. Tapi aku tidak merasa terlalu kuat tentang hal itu :)
guyarad
1
Seharusnya ada argumen default opsional, dan jika argumen itu tidak disediakan, baru kemudian memunculkan eksepsi ketika tidak ada elemen dalam urutan memenuhi kondisi.
Zorf
28

Pengecualian Sialan!

Saya suka jawaban ini . Namun, karena next()memunculkan StopIterationpengecualian ketika tidak ada item, saya akan menggunakan cuplikan berikut untuk menghindari pengecualian:

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

Sebagai contoh,

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

Akan memunculkan StopIterationpengecualian;

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

Mirip dengan menggunakan ifilter, Anda bisa menggunakan ekspresi generator:

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

Dalam kedua kasus, Anda mungkin ingin menangkap StopIteration, jika tidak ada elemen yang memenuhi kondisi Anda.

Secara teknis, saya kira Anda bisa melakukan sesuatu seperti ini:

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

Itu akan menghindari membuat try/exceptblok. Tapi itu sepertinya agak kabur dan kasar terhadap sintaksis.

Matt Anderson
sumber
+1: Tidak jelas, tidak kasar. Semua hal dipertimbangkan, yang terakhir tampaknya cukup bersih.
S.Lott
6
Yang terakhir sama sekali tidak bersih — for foo in genex: breakhanya cara melakukan foo = next(genex)tanpa membuat tugas jelas dan dengan pengecualian yang akan dinaikkan jika operasi tidak masuk akal terjepit. Mengakhiri dengan kode kegagalan alih-alih menangkap pengecualian biasanya merupakan hal buruk di Python.
Mike Graham
13

Cara paling efisien dalam Python 3 adalah salah satu dari yang berikut (menggunakan contoh yang serupa):

Dengan gaya "pemahaman" :

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

PERINGATAN : Ekspresi bekerja juga dengan Python 2, tetapi dalam contoh ini digunakan rangeyang mengembalikan objek iterable di Python 3 bukannya daftar seperti Python 2 (jika Anda ingin membangun iterable di Python 2 gunakan xrangesaja).

Perhatikan bahwa ekspresi menghindari untuk membuat daftar dalam ekspresi pemahaman next([i for ...]), yang akan menyebabkan untuk membuat daftar dengan semua elemen sebelum memfilter elemen, dan akan menyebabkan untuk memproses seluruh opsi, alih-alih menghentikan iterasi satu kali i == 1000.

Dengan gaya "fungsional" :

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

PERINGATAN : Ini tidak bekerja di Python 2, bahkan mengganti rangedengan xrangekarena yang filtermembuat daftar bukan iterator (tidak efisien), dan nextfungsinya hanya bekerja dengan iterator.

Nilai standar

Seperti disebutkan dalam respons lain, Anda harus menambahkan parameter ekstra ke fungsi nextjika Anda ingin menghindari pengecualian yang muncul ketika kondisi tidak terpenuhi.

gaya "fungsional" :

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

gaya "pemahaman" :

Dengan gaya ini Anda perlu mengelilingi ekspresi pemahaman ()untuk menghindari SyntaxError: Generator expression must be parenthesized if not sole argument:

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

Saya akan menulis ini

next(x for x in xrange(10) if x > 3)
Mike Graham
sumber
Saya kira i > 3harus x > 3dalam contoh Anda
Ricky Robinson
6

The itertoolsmodul berisi fungsi filter untuk iterator. Elemen pertama dari iterator yang disaring dapat diperoleh dengan memanggilnya next():

from itertools import ifilter

print ifilter((lambda i: i > 3), range(10)).next()
sth
sumber
2
Ekspresi generator lebih sederhana.
Eric O Lebigot
1
( i) filterdan ( i) mapbisa masuk akal untuk kasus-kasus di mana fungsi yang diterapkan sudah ada, tetapi dalam situasi seperti ini lebih masuk akal hanya dengan menggunakan ekspresi generator.
Mike Graham
Ini jawaban terbaik. Hindari pemahaman daftar xahlee.info/comp/list_comprehension.html
mit
6

Untuk versi Python yang lebih lama di mana built-in berikutnya tidak ada:

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

Dengan menggunakan

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

satu dapat memeriksa kondisi dari nilai dari item pertama di the_iterable , dan mendapatkan nya indeks tanpa perlu untuk mengevaluasi semua item di the_iterable .

Ekspresi lengkap untuk digunakan adalah

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

Di sini first_index mengasumsikan nilai dari nilai pertama yang diidentifikasi dalam ekspresi yang dibahas di atas.

blue_note
sumber
4

Pertanyaan ini sudah memiliki jawaban yang bagus. Saya hanya menambahkan dua sen karena saya mendarat di sini mencoba mencari solusi untuk masalah saya sendiri, yang sangat mirip dengan OP.

Jika Anda ingin menemukan INDEX dari item pertama yang cocok dengan kriteria menggunakan generator, Anda dapat melakukannya:

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

Anda juga bisa menggunakan argwherefungsi ini di Numpy. Sebagai contoh:

i) Temukan "l" pertama di "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) Temukan nomor acak pertama> 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) Temukan nomor acak terakhir> 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()
tujuan
sumber
-1

Dengan Python 3:

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

Dengan Python 2.6:

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

EDIT: Saya pikir itu sudah jelas, tetapi ternyata tidak: alih-alih NoneAnda dapat melewati fungsi (atau a lambda) dengan memeriksa kondisi:

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

Oneliner:

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

Jika Anda tidak yakin bahwa elemen apa pun akan valid sesuai dengan kriteria, Anda harus melampirkan ini try/exceptkarena [0]dapat meningkatkan IndexError.

Mizipzor
sumber
Objek TypeError: 'generator' tidak dapat disubkripsikan
Josh Lee
Buruk saya, seharusnya daftar pemahaman bukan generator, diperbaiki ... terima kasih! :)
Mizipzor
2
Tidak ada alasan untuk mengevaluasi keseluruhan iterable (yang mungkin tidak mungkin). Lebih kuat dan efisien untuk menggunakan salah satu solusi lain yang disediakan.
Mike Graham