daftar pemahaman vs lambda + filter

858

Saya kebetulan menemukan diri saya memiliki kebutuhan penyaringan dasar: Saya punya daftar dan saya harus menyaringnya dengan atribut item.

Kode saya terlihat seperti ini:

my_list = [x for x in my_list if x.attribute == value]

Tapi kemudian saya berpikir, bukankah lebih baik menulis seperti ini?

my_list = filter(lambda x: x.attribute == value, my_list)

Ini lebih mudah dibaca, dan jika diperlukan untuk kinerja lambda dapat diambil untuk mendapatkan sesuatu.

Pertanyaannya adalah: apakah ada peringatan dalam menggunakan cara kedua? Adakah perbedaan kinerja? Apakah saya kehilangan Pythonic Way ™ sepenuhnya dan harus melakukannya dengan cara lain (seperti menggunakan itemgetter bukan lambda)?

Lalu
sumber
19
Contoh yang lebih baik adalah kasus di mana Anda telah memiliki fungsi yang dinamai dengan baik untuk digunakan sebagai predikat Anda. Dalam hal ini, saya pikir lebih banyak orang akan setuju yang filterlebih mudah dibaca. Ketika Anda memiliki ekspresi sederhana yang dapat digunakan apa adanya di listcomp, tetapi harus dibungkus dengan lambda (atau dibangun di luar dari partialatau operatorfungsi, dll) untuk dilewati filter, saat itulah Listcomps menang.
abarnert
3
Harus dikatakan bahwa dalam Python3 setidaknya, kembalinya filteradalah objek generator filter bukan daftar.
Matteo Ferla

Jawaban:

588

Sungguh aneh betapa banyak keindahan bervariasi untuk orang yang berbeda. Saya menemukan pemahaman daftar jauh lebih jelas daripada filter+ lambda, tetapi gunakan mana yang Anda temukan lebih mudah.

Ada dua hal yang dapat memperlambat penggunaan Anda filter.

Yang pertama adalah overhead panggilan fungsi: segera setelah Anda menggunakan fungsi Python (apakah dibuat oleh defatau lambda), kemungkinan filter akan lebih lambat daripada pemahaman daftar. Ini hampir pasti tidak cukup untuk masalah, dan Anda tidak harus berpikir banyak tentang kinerja sampai Anda menghitung waktu kode Anda dan menemukan itu menjadi hambatan, tetapi perbedaannya ada di sana.

Overhead lain yang mungkin berlaku adalah bahwa lambda dipaksa untuk mengakses variabel scoped ( value). Itu lebih lambat daripada mengakses variabel lokal dan dalam Python 2.x daftar pemahaman hanya mengakses variabel lokal. Jika Anda menggunakan Python 3.x pemahaman daftar berjalan dalam fungsi terpisah sehingga juga akan mengakses valuemelalui penutupan dan perbedaan ini tidak akan berlaku.

Pilihan lain untuk dipertimbangkan adalah menggunakan generator alih-alih pemahaman daftar:

def filterbyvalue(seq, value):
   for el in seq:
       if el.attribute==value: yield el

Kemudian dalam kode utama Anda (yang mana keterbacaan sangat penting) Anda telah mengganti pemahaman daftar dan filter dengan nama fungsi yang diharapkan bermakna.

Duncan
sumber
68
+1 untuk generator. Saya memiliki tautan di rumah untuk presentasi yang menunjukkan betapa menakjubkannya generator. Anda juga dapat mengganti pemahaman daftar dengan ekspresi generator hanya dengan mengubah []ke (). Juga, saya setuju bahwa daftar comp lebih indah.
Wayne Werner
1
Sebenarnya, tanpa filter lebih cepat. Cukup jalankan beberapa tolok ukur cepat menggunakan sesuatu seperti stackoverflow.com/questions/5998245/...
skqr
2
@skqr lebih baik hanya menggunakan timeit untuk tolok ukur, tapi tolong berikan contoh di mana Anda menemukan filterlebih cepat menggunakan fungsi panggilan balik Python.
Duncan
8
@ tnq177 Ini presentasi David Beasley tentang generator - dabeaz.com/generators
Wayne Werner
2
@ VictorSchröder ya, mungkin saya tidak jelas. Apa yang saya coba katakan adalah bahwa dalam kode utama Anda harus dapat melihat gambar yang lebih besar. Dalam fungsi pembantu kecil Anda hanya perlu peduli tentang satu fungsi itu, apa lagi yang terjadi di luar dapat diabaikan.
Duncan
237

Ini adalah masalah yang agak religius dalam Python. Meskipun Guido mempertimbangkan untuk menghapus map, filterdan reducedari Python 3 , ada cukup serangan balik yang pada akhirnya hanya reducedipindahkan dari built-in ke functools.reduce .

Secara pribadi saya menemukan daftar pemahaman lebih mudah dibaca. Lebih eksplisit apa yang terjadi dari ekspresi [i for i in list if i.attribute == value]karena semua perilaku di permukaan bukan di dalam fungsi filter.

Saya tidak akan terlalu khawatir tentang perbedaan kinerja antara kedua pendekatan karena marjinal. Saya benar-benar hanya akan mengoptimalkan ini jika terbukti menjadi hambatan dalam aplikasi Anda yang tidak mungkin.

Juga karena BDFL ingin filterpergi dari bahasa maka pasti itu secara otomatis membuat daftar pemahaman lebih Pythonic ;-)

Tendayi Mawushe
sumber
1
Terima kasih atas tautan ke masukan Guido, jika tidak ada yang lain bagi saya itu berarti saya akan mencoba untuk tidak menggunakannya lagi, sehingga saya tidak akan terbiasa, dan saya tidak akan mendukung agama itu :)
dashesy
1
tetapi mengurangi adalah yang paling rumit untuk dilakukan dengan alat sederhana! peta dan filter mudah untuk diganti dengan pemahaman!
njzk2
8
tidak tahu pengurangan diturunkan di Python3. terima kasih atas wawasannya! mengurangi () masih sangat membantu dalam komputasi terdistribusi, seperti PySpark. Saya pikir itu adalah kesalahan ..
Tagar
1
@Tagar Anda masih dapat menggunakan pengurangan, Anda hanya perlu mengimpornya dari functools
icc97
69

Karena perbedaan kecepatan apa pun pasti sangat kecil, apakah menggunakan filter atau daftar pemahaman menjadi masalah selera. Secara umum saya cenderung menggunakan pemahaman (yang tampaknya setuju dengan sebagian besar jawaban lain di sini), tetapi ada satu kasus di mana saya lebih suka filter.

Kasus penggunaan yang sangat sering adalah menarik keluar nilai dari beberapa subjek X yang dapat diubah ke predikat P (x):

[x for x in X if P(x)]

tetapi terkadang Anda ingin menerapkan beberapa fungsi ke nilai terlebih dahulu:

[f(x) for x in X if P(f(x))]


Sebagai contoh spesifik, pertimbangkan

primes_cubed = [x*x*x for x in range(1000) if prime(x)]

Saya pikir ini terlihat sedikit lebih baik daripada menggunakan filter. Tapi sekarang pertimbangkan

prime_cubes = [x*x*x for x in range(1000) if prime(x*x*x)]

Dalam hal ini kami ingin filtermenentang nilai pasca-perhitungan. Selain masalah menghitung kubus dua kali (bayangkan perhitungan yang lebih mahal), ada masalah menulis ekspresi dua kali, melanggar estetika KERING . Dalam hal ini saya akan cenderung menggunakan

prime_cubes = filter(prime, [x*x*x for x in range(1000)])
IJ Kennedy
sumber
7
Apakah Anda tidak mempertimbangkan menggunakan prime melalui pemahaman daftar lain? Seperti[prime(i) for i in [x**3 for x in range(1000)]]
viki.omega9
20
x*x*xtidak bisa menjadi bilangan prima, seperti yang terjadi x^2dan xsebagai faktor, contohnya tidak benar-benar masuk akal secara matematis, tapi mungkin itu masih membantu. (Mungkin kita bisa menemukan sesuatu yang lebih baik?)
Zelphir Kaltstahl
3
Perhatikan bahwa kita dapat menggunakan ekspresi generator sebagai gantinya untuk contoh terakhir jika kita tidak ingin memakan memori:prime_cubes = filter(prime, (x*x*x for x in range(1000)))
Mateen Ulhaq
4
@MateenUlhaq ini dapat dioptimalkan untuk prime_cubes = [1]menghemat memori dan siklus cpu ;-)
Dennis Krupenik
7
@DennisKrupenik Atau lebih tepatnya,[]
Mateen Ulhaq
29

Meskipun filtermungkin merupakan "jalan tercepat", "jalan Pythonic" tidak akan peduli tentang hal-hal seperti itu kecuali kinerja sangat penting (dalam hal ini Anda tidak akan menggunakan Python!).

Umang
sumber
10
Komentar telat terhadap argumen yang sering terlihat: Kadang-kadang ada perbedaan untuk menjalankan analisis dalam 5 jam, bukan 10, dan jika itu dapat dicapai dengan mengambil satu jam mengoptimalkan kode python, itu bisa sia-sia (terutama jika ada nyaman dengan python dan tidak dengan bahasa yang lebih cepat).
bli
Tetapi yang lebih penting adalah seberapa banyak kode sumber memperlambat kita untuk mencoba membaca dan memahaminya!
thoni56
20

Saya pikir saya hanya akan menambahkan bahwa di python 3, filter () sebenarnya adalah objek iterator, jadi Anda harus melewati metode panggilan Anda ke daftar () untuk membangun daftar yang difilter. Jadi dengan python 2:

lst_a = range(25) #arbitrary list
lst_b = [num for num in lst_a if num % 2 == 0]
lst_c = filter(lambda num: num % 2 == 0, lst_a)

daftar b dan c memiliki nilai yang sama, dan diselesaikan dalam waktu yang hampir bersamaan dengan filter () sama dengan [x untuk x dalam y jika z]. Namun, dalam 3, kode yang sama ini akan meninggalkan daftar c yang berisi objek filter, bukan daftar yang difilter. Untuk menghasilkan nilai yang sama dalam 3:

lst_a = range(25) #arbitrary list
lst_b = [num for num in lst_a if num % 2 == 0]
lst_c = list(filter(lambda num: num %2 == 0, lst_a))

Masalahnya adalah list () mengambil iterable sebagai argumennya, dan membuat daftar baru dari argumen itu. Hasilnya adalah bahwa menggunakan filter dengan cara ini di python 3 membutuhkan waktu hingga dua kali lebih lama daripada metode [x untuk x in y jika z] karena Anda harus beralih pada output dari filter () serta daftar asli.

Jim50
sumber
13

Perbedaan penting adalah bahwa pemahaman daftar akan mengembalikan beberapa listsaat filter mengembalikan a filter, yang Anda tidak dapat memanipulasi seperti a list(yaitu: panggil len, yang tidak bekerja dengan pengembalian filter).

Belajar mandiri saya sendiri membawa saya ke beberapa masalah serupa.

Yang sedang berkata, jika ada cara untuk memiliki hasil listdari filter, sedikit seperti yang Anda lakukan di. NET ketika Anda melakukannya lst.Where(i => i.something()).ToList(), saya ingin tahu.

EDIT: Ini adalah kasus untuk Python 3, bukan 2 (lihat diskusi dalam komentar).

Adeynack
sumber
4
filter mengembalikan daftar dan kita dapat menggunakan len di atasnya. Setidaknya dalam Python 2.7.6 saya.
thiruvenkadam
7
Ini tidak terjadi dalam Python 3. a = [1, 2, 3, 4, 5, 6, 7, 8] f = filter(lambda x: x % 2 == 0, a) lc = [i for i in a if i % 2 == 0] >>> type(f) <class 'filter'> >>> type(lc) <class 'list'>
Adeynack
3
"Jika ada cara untuk mendapatkan daftar yang dihasilkan ... Saya ingin tahu". Panggil saja list()pada hasil: list(filter(my_func, my_iterable)). Dan tentu saja Anda bisa menggantinya listdengan set, atau tuple, atau apa pun yang membutuhkan perubahan. Tetapi bagi siapa pun selain programmer fungsional, kasus ini bahkan lebih kuat untuk menggunakan pemahaman daftar daripada filterditambah konversi eksplisit ke list.
Steve Jessop
10

Saya menemukan cara kedua lebih mudah dibaca. Ini memberitahu Anda apa maksudnya: filter daftar.
PS: jangan gunakan 'daftar' sebagai nama variabel

unbeli
sumber
7

umumnya filtersedikit lebih cepat jika menggunakan fungsi builtin.

Saya berharap pemahaman daftar menjadi sedikit lebih cepat dalam kasus Anda

John La Rooy
sumber
filter python -m timeit '(lambda x: x dalam [1,2,3,4,5], range (10000000))' 10 loop, terbaik 3: 1,44 detik per loop python -m timeit '[x untuk x dalam kisaran (10000000) jika x dalam [1,2,3,4,5]] '10 loop, terbaik 3: 860 msec per loop Tidak juga ?!
giaosudau
@sepdau, fungsi lambda bukan bawaan. Pemahaman daftar telah meningkat selama 4 tahun terakhir - sekarang perbedaannya dapat diabaikan meskipun dengan fungsi bawaan
John La Rooy
7

Filter hanya itu. Ini menyaring elemen daftar. Anda dapat melihat definisi menyebutkan hal yang sama (di tautan dokumen resmi yang saya sebutkan sebelumnya). Sedangkan, pemahaman daftar adalah sesuatu yang menghasilkan daftar baru setelah bertindak atas sesuatu pada daftar sebelumnya. (Kedua filter dan pemahaman daftar membuat daftar baru dan tidak melakukan operasi di tempat daftar yang lebih lama. Daftar baru di sini adalah sesuatu seperti daftar dengan , katakanlah, tipe data yang sama sekali baru. Seperti mengonversi bilangan bulat menjadi string, dll)

Dalam contoh Anda, lebih baik menggunakan filter daripada pemahaman daftar, sesuai definisi. Namun, jika Anda ingin, katakan other_attribute dari elemen daftar, dalam contoh Anda akan diambil sebagai daftar baru, maka Anda dapat menggunakan pemahaman daftar.

return [item.other_attribute for item in my_list if item.attribute==value]

Ini adalah bagaimana saya benar-benar ingat tentang pemahaman filter dan daftar. Hapus beberapa hal dalam daftar dan jaga elemen lainnya tetap utuh, gunakan filter. Gunakan beberapa logika Anda sendiri di elemen dan buat daftar encer yang cocok untuk beberapa tujuan, gunakan pemahaman daftar.

thiruvenkadam
sumber
2
Saya akan senang mengetahui alasan down voting sehingga saya tidak akan mengulanginya lagi di mana pun di masa depan.
thiruvenkadam
definisi filter dan daftar pemahaman tidak diperlukan, karena artinya tidak diperdebatkan. Bahwa pemahaman daftar harus digunakan hanya untuk daftar "baru" disajikan tetapi tidak diperdebatkan.
Agos
Saya menggunakan definisi untuk mengatakan bahwa filter memberi Anda daftar dengan elemen yang sama yang benar untuk sebuah kasus tetapi dengan pemahaman daftar kita dapat memodifikasi elemen itu sendiri, seperti mengkonversi int ke str. Tapi intinya diambil :-)
thiruvenkadam
4

Berikut adalah bagian pendek yang saya gunakan ketika saya perlu memfilter sesuatu setelah pemahaman daftar. Hanya kombinasi filter, lambda, dan daftar (atau dikenal sebagai kesetiaan kucing dan kebersihan anjing).

Dalam hal ini saya membaca file, menghapus garis kosong, mengomentari baris, dan apa pun setelah komentar pada baris:

# Throw out blank lines and comments
with open('file.txt', 'r') as lines:        
    # From the inside out:
    #    [s.partition('#')[0].strip() for s in lines]... Throws out comments
    #   filter(lambda x: x!= '', [s.part... Filters out blank lines
    #  y for y in filter... Converts filter object to list
    file_contents = [y for y in filter(lambda x: x != '', [s.partition('#')[0].strip() for s in lines])]
lebih keras
sumber
Ini mencapai banyak kode sangat sedikit memang. Saya pikir mungkin agak terlalu banyak logika dalam satu baris untuk mudah dimengerti dan keterbacaan adalah yang terpenting.
Zelphir Kaltstahl
Anda bisa menulis ini sebagaifile_contents = list(filter(None, (s.partition('#')[0].strip() for s in lines)))
Steve Jessop
4

Selain jawaban yang diterima, ada sudut kasus saat Anda harus menggunakan filter alih-alih pemahaman daftar. Jika daftar itu tidak dapat dilanggar, Anda tidak dapat langsung memprosesnya dengan pemahaman daftar. Contoh dunia nyata adalah jika Anda menggunakan pyodbcuntuk membaca hasil dari database. The fetchAll()Hasil dari cursoradalah daftar unhashable. Dalam situasi ini, untuk secara langsung memanipulasi hasil yang dikembalikan, filter harus digunakan:

cursor.execute("SELECT * FROM TABLE1;")
data_from_db = cursor.fetchall()
processed_data = filter(lambda s: 'abc' in s.field1 or s.StartTime >= start_date_time, data_from_db) 

Jika Anda menggunakan pemahaman daftar di sini Anda akan mendapatkan kesalahan:

TypeError: tipe yang tidak dapat ditemukan: 'list'

CWpraen
sumber
1
semua daftar tidak bisa dilanggar >>> hash(list()) # TypeError: unhashable type: 'list'kedua ini berfungsi dengan baik:processed_data = [s for s in data_from_db if 'abc' in s.field1 or s.StartTime >= start_date_time]
Thomas Grainger
"Jika daftar itu tidak dapat dilanggar, Anda tidak dapat langsung memprosesnya dengan pemahaman daftar." Ini tidak benar, dan semua daftar toh tidak bisa pecah.
juanpa.arrivillaga
3

Butuh beberapa waktu untuk membiasakan diri dengan higher order functions filterdan map. Jadi saya terbiasa dengan mereka dan saya benar-benar suka filterkarena secara eksplisit itu menyaring dengan menjaga apa pun yang benar dan saya merasa keren bahwa saya tahu beberapa functional programmingistilah.

Lalu saya membaca bagian ini (Fluent Python Book):

Fungsi peta dan filter masih dibangun di Python 3, tetapi karena pengenalan daftar komprehensif dan ekspresi generator, mereka tidak begitu penting. Listcomp atau genexp melakukan pekerjaan peta dan filter dikombinasikan, tetapi lebih mudah dibaca.

Dan sekarang saya pikir, mengapa repot-repot dengan konsep filter/ mapjika Anda dapat mencapainya dengan idiom yang sudah menyebar luas seperti daftar pemahaman. Lebih jauh lagi mapsdan filterssemacam fungsi. Dalam hal ini saya lebih suka menggunakan Anonymous functionslambdas.

Akhirnya, hanya untuk menguji, saya menghitung waktu kedua metode ( mapdan listComp) dan saya tidak melihat perbedaan kecepatan yang relevan yang akan membenarkan membuat argumen tentang hal itu.

from timeit import Timer

timeMap = Timer(lambda: list(map(lambda x: x*x, range(10**7))))
print(timeMap.timeit(number=100))

timeListComp = Timer(lambda:[(lambda x: x*x) for x in range(10**7)])
print(timeListComp.timeit(number=100))

#Map:                 166.95695265199174
#List Comprehension   177.97208347299602
pengguna1767754
sumber
0

Anehnya pada Python 3, saya melihat filter berkinerja lebih cepat daripada daftar pemahaman.

Saya selalu berpikir bahwa pemahaman daftar akan lebih baik. Sesuatu seperti: [nama untuk nama di brand_names_db jika nama tidak ada] Bytecode yang dihasilkan sedikit lebih baik.

>>> def f1(seq):
...     return list(filter(None, seq))
>>> def f2(seq):
...     return [i for i in seq if i is not None]
>>> disassemble(f1.__code__)
2         0 LOAD_GLOBAL              0 (list)
          2 LOAD_GLOBAL              1 (filter)
          4 LOAD_CONST               0 (None)
          6 LOAD_FAST                0 (seq)
          8 CALL_FUNCTION            2
         10 CALL_FUNCTION            1
         12 RETURN_VALUE
>>> disassemble(f2.__code__)
2           0 LOAD_CONST               1 (<code object <listcomp> at 0x10cfcaa50, file "<stdin>", line 2>)
          2 LOAD_CONST               2 ('f2.<locals>.<listcomp>')
          4 MAKE_FUNCTION            0
          6 LOAD_FAST                0 (seq)
          8 GET_ITER
         10 CALL_FUNCTION            1
         12 RETURN_VALUE

Tapi mereka sebenarnya lebih lambat:

   >>> timeit(stmt="f1(range(1000))", setup="from __main__ import f1,f2")
   21.177661532000116
   >>> timeit(stmt="f2(range(1000))", setup="from __main__ import f1,f2")
   42.233950221000214
Rod Senra
sumber
8
Perbandingan tidak valid . Pertama, Anda tidak meneruskan fungsi lambda ke versi filter, yang menjadikannya default untuk fungsi identitas. Ketika mendefinisikan if not Nonedalam daftar pemahaman Anda sedang mendefinisikan fungsi lambda (perhatikan MAKE_FUNCTIONpernyataan). Kedua, hasilnya berbeda, karena versi daftar pemahaman akan menghapus hanya Nonenilai, sedangkan versi filter akan menghapus semua nilai "falsy". Karena itu, seluruh tujuan microbenchmarking tidak berguna. Itu adalah satu juta iterasi, kali item 1k! Perbedaannya dapat diabaikan .
Victor Schröder
-7

Saya ambil

def filter_list(list, key, value, limit=None):
    return [i for i in list if i[key] == value][:limit]
tim
sumber
3
itidak pernah dikatakan sebagai dict, dan tidak perlu limit. Selain itu, bagaimana ini berbeda dari apa yang disarankan OP, dan bagaimana hal itu menjawab pertanyaan?