Temukan elemen pertama dalam urutan yang cocok dengan predikat

171

Saya ingin cara idiomatis untuk menemukan elemen pertama dalam daftar yang cocok dengan predikat.

Kode saat ini sangat jelek:

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

Saya sudah berpikir untuk mengubahnya menjadi:

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

Tapi pasti ada sesuatu yang lebih elegan ... Dan alangkah baiknya jika mengembalikan Nonenilai daripada menaikkan pengecualian jika tidak ada kecocokan yang ditemukan.

Saya tahu saya bisa mendefinisikan fungsi seperti:

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

Tetapi cukup hambar untuk mulai mengisi kode dengan fungsi-fungsi utilitas seperti ini (dan orang-orang mungkin tidak akan menyadari bahwa mereka sudah ada di sana, sehingga mereka cenderung untuk diulangi dari waktu ke waktu) jika ada seluk-beluk internal yang sudah menyediakan hal yang sama.

fortran
sumber
3
Selain ditanyakan lebih dari " fungsi pencarian urutan python ", pertanyaan ini memiliki judul yang jauh lebih baik .
Wolf

Jawaban:

250

Untuk menemukan elemen pertama dalam urutan seqyang cocok dengan predicate:

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

Atau ( itertools.ifilterdi Python 2) :

next(filter(predicate, seq))

Itu memunculkan StopIterationjika tidak ada.


Untuk kembali Nonejika tidak ada elemen seperti itu:

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

Atau:

next(filter(predicate, seq), None)
jfs
sumber
27
Atau Anda dapat memberikan argumen "default" kedua nextyang digunakan alih-alih menaikkan pengecualian.
Karl Knechtel
2
@fortran: next()tersedia sejak Python 2.6 Anda bisa membaca halaman What's New untuk membiasakan diri dengan fitur baru dengan cepat.
jfs
1
Saya seorang pemula python dan membaca dokumen dan ifilter menggunakan metode "hasil". Saya berasumsi ini berarti bahwa predikat itu dievaluasi malas saat kita pergi. yaitu, kami tidak menjalankan predikat melalui seluruh daftar karena saya memiliki fungsi predikat yang agak mahal dan saya hanya ingin mengulang sampai titik di mana kami menemukan item
Kannan Ekanath
2
@geekazoid: seq.find(&method(:predicate))atau bahkan lebih ringkas untuk metode contoh misalnya:[1,1,4].find(&:even?)
jfs
16
ifilterdiganti namanya menjadi filterdalam Python 3.
tsauerwein
92

Anda bisa menggunakan ekspresi generator dengan nilai default dan kemudian next:

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

Meskipun untuk one-liner ini Anda harus menggunakan Python> = 2.6.

Artikel yang agak populer ini lebih lanjut membahas masalah ini: Fungsi find-in-list Python terbersih? .

Chewie
sumber
8

Saya tidak berpikir ada yang salah dengan solusi yang Anda ajukan dalam pertanyaan Anda.

Dalam kode saya sendiri, saya akan mengimplementasikannya seperti ini:

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

Sintaks dengan ()menciptakan generator, yang lebih efisien daripada menghasilkan semua daftar sekaligus [].

Mac
sumber
Dan tidak hanya itu - dengan []Anda mungkin mengalami masalah jika iterator tidak pernah berakhir atau elemen-elemennya sulit dibuat, semakin lambat ...
glglgl
6
'generator' object has no attribute 'next'pada Python 3.
jfs
@glglgl - Adapun poin pertama (tidak pernah berakhir) saya meragukannya, karena argumennya adalah urutan yang terbatas [lebih tepatnya daftar sesuai dengan pertanyaan OP '). Adapun yang kedua: sekali lagi, karena argumen yang diberikan adalah urutan, objek seharusnya sudah dibuat dan disimpan pada saat fungsi ini disebut .... atau apakah saya kehilangan sesuatu?
mac
@JFSebastian - Terima kasih, saya tidak menyadarinya! :) Karena penasaran, apa prinsip desain di balik pilihan ini?
mac
@ Mac - Untuk konsistensi dengan garis bawah ganda dari metode khusus lainnya. Lihat python.org/dev/peps/pep-3114
Chewie
1

Jawaban JF Sebastian paling elegan tetapi membutuhkan python 2.6 seperti yang ditunjukkan fortran.

Untuk versi Python <2.6, ini yang terbaik yang bisa saya lakukan:

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

Atau jika Anda membutuhkan daftar nanti (daftar menangani StopIteration), atau Anda membutuhkan lebih dari sekadar yang pertama tetapi masih belum semuanya, Anda dapat melakukannya dengan islice:

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

UPDATE: Meskipun saya secara pribadi menggunakan fungsi yang telah ditentukan yang disebut first () yang menangkap StopIteration dan mengembalikan None, Berikut adalah kemungkinan peningkatan atas contoh di atas: hindari menggunakan filter / ifilter:

from itertools import islice,chain
chain((x for x in seq if predicate(x)),repeat(None)).next()
paritas3
sumber
11
Astaga! jika sampai seperti itu, saya hanya akan melakukan perulangan "for" sederhana dengan "jika" di dalamnya - lebih mudah dibaca
Nick Perkins