Apakah pengecualian untuk flow control praktik terbaik dalam Python?

15

Saya sedang membaca "Belajar Python" dan telah menemukan yang berikut:

Pengecualian yang ditentukan pengguna juga dapat menandakan kondisi non-teror. Misalnya, rutin pencarian dapat dikodekan untuk meningkatkan pengecualian ketika kecocokan ditemukan alih-alih mengembalikan bendera status untuk penelepon untuk ditafsirkan. Di bawah ini, pengendali pengecualian coba / kecuali / yang lain melakukan pekerjaan penguji nilai balik if / else:

class Found(Exception): pass

def searcher():
    if ...success...:
        raise Found()            # Raise exceptions instead of returning flags
    else:
        return

Karena Python diketik secara dinamis dan polimorfik ke inti, pengecualian, daripada nilai balik sentinel, adalah cara yang umumnya lebih disukai untuk memberi sinyal kondisi seperti itu.

Saya telah melihat hal semacam ini dibahas beberapa kali di berbagai forum, dan referensi ke Python menggunakan StopIteration untuk mengakhiri loop, tapi saya tidak dapat menemukan banyak hal dalam panduan gaya resmi (PEP 8 memiliki satu referensi langsung ke pengecualian untuk kontrol aliran) atau pernyataan dari pengembang. Apakah ada yang resmi yang menyatakan ini adalah praktik terbaik untuk Python?

Ini ( Apakah pengecualian sebagai aliran kontrol dianggap antipattern yang serius? Jika demikian, Mengapa? ) Juga memiliki beberapa komentator menyatakan bahwa gaya ini adalah Pythonic. Berdasarkan apa ini?

TIA

JB
sumber

Jawaban:

25

Konsensus umum “jangan gunakan pengecualian!” Sebagian besar berasal dari bahasa lain dan bahkan ada yang sudah usang.

  • Dalam C ++, melempar pengecualian sangat mahal karena “stack unwinding”. Setiap deklarasi variabel lokal seperti withpernyataan dalam Python, dan objek dalam variabel itu dapat menjalankan destruktor. Destructor ini dieksekusi ketika pengecualian dilemparkan, tetapi juga ketika kembali dari suatu fungsi. “RAII idiom” ini adalah fitur bahasa yang tidak terpisahkan dan sangat penting untuk menulis kode yang kuat dan benar - jadi RAII versus pengecualian murah merupakan tradeoff yang C ++ putuskan untuk RAII.

  • Pada awal C ++, banyak kode tidak ditulis dengan cara pengecualian-aman: kecuali Anda benar-benar menggunakan RAII, mudah bocor memori dan sumber daya lainnya. Jadi, melempar pengecualian akan membuat kode itu salah. Ini tidak lagi masuk akal karena bahkan pustaka standar C ++ menggunakan pengecualian: Anda tidak bisa berpura-pura pengecualian tidak ada. Namun, pengecualian masih menjadi masalah saat menggabungkan kode C dengan C ++.

  • Di Jawa, setiap pengecualian memiliki jejak tumpukan yang terkait. Jejak tumpukan sangat berharga saat men-debug kesalahan, tetapi usaha yang sia-sia ketika pengecualian tidak pernah dicetak, misalnya karena itu hanya digunakan untuk aliran kontrol.

Jadi dalam bahasa-bahasa itu pengecualian "terlalu mahal" untuk digunakan sebagai aliran kontrol. Dalam Python ini kurang dari masalah dan pengecualian jauh lebih murah. Selain itu, bahasa Python sudah menderita beberapa overhead yang membuat biaya pengecualian tidak terlihat dibandingkan dengan konstruksi aliran kontrol lainnya: misalnya memeriksa apakah entri dikt ada dengan tes keanggotaan eksplisitif key in the_dict: ... umumnya sama cepatnya dengan hanya mengakses entri the_dict[key]; ...dan memeriksa apakah Anda dapatkan KeyError. Beberapa fitur bahasa integral (misalnya generator) dirancang dalam hal pengecualian.

Jadi, sementara tidak ada alasan teknis untuk secara khusus menghindari pengecualian dalam Python, masih ada pertanyaan apakah Anda harus menggunakannya alih-alih mengembalikan nilai. Masalah tingkat desain dengan pengecualian adalah:

  • sama sekali tidak jelas. Anda tidak dapat dengan mudah melihat fungsi dan melihat pengecualian yang dilemparkannya, sehingga Anda tidak selalu tahu harus menangkap apa. Nilai kembali cenderung lebih didefinisikan dengan baik.

  • pengecualian adalah aliran kontrol non-lokal yang memperumit kode Anda. Saat Anda melempar pengecualian, Anda tidak tahu di mana aliran kontrol akan dilanjutkan. Untuk kesalahan yang tidak dapat segera ditangani, ini mungkin ide yang baik, ketika memberi tahu penelepon Anda tentang kondisi ini sama sekali tidak perlu.

Budaya Python umumnya miring mendukung pengecualian, tetapi mudah untuk berlebihan. Bayangkan sebuah list_contains(the_list, item)fungsi yang memeriksa apakah daftar tersebut berisi item yang sama dengan item itu. Jika hasilnya dikomunikasikan melalui pengecualian yang benar-benar menjengkelkan, karena kita harus menyebutnya seperti ini:

try:
  list_contains(invited_guests, person_at_door)
except Found:
  print("Oh, hello {}!".format(person_at_door))
except NotFound:
  print("Who are you?")

Mengembalikan bool akan jauh lebih jelas:

if list_contains(invited_guests, person_at_door):
  print("Oh, hello {}!".format(person_at_door))
else:
  print("Who are you?")

Jika fungsi sudah seharusnya mengembalikan nilai, maka mengembalikan nilai khusus untuk kondisi khusus agak rawan kesalahan, karena orang akan lupa untuk memeriksa nilai ini (itu mungkin penyebab 1/3 dari masalah di C). Pengecualian biasanya lebih tepat.

Contoh yang baik adalah pos = find_string(haystack, needle)fungsi yang mencari kemunculan pertama needlestring dalam string `haystack, dan mengembalikan posisi awal. Tetapi bagaimana jika tali jerami tidak mengandung benang jarum?

Solusi oleh C dan ditiru oleh Python adalah mengembalikan nilai khusus. Dalam C ini adalah pointer nol, dengan Python ini -1. Ini akan menghasilkan hasil yang mengejutkan ketika posisi digunakan sebagai indeks string tanpa memeriksa, terutama seperti -1indeks yang valid dalam Python. Di C, pointer NULL Anda setidaknya akan memberi Anda segfault.

Dalam PHP, nilai khusus dari jenis yang berbeda dikembalikan: boolean FALSEbukan bilangan bulat. Ternyata ini sebenarnya tidak lebih baik karena aturan konversi implisit bahasa (tetapi perhatikan bahwa dalam Python juga boolean dapat digunakan sebagai ints!). Fungsi yang tidak mengembalikan tipe yang konsisten umumnya dianggap sangat membingungkan.

Varian yang lebih kuat adalah dengan melemparkan pengecualian ketika string tidak dapat ditemukan, yang memastikan bahwa selama aliran kontrol normal tidak mungkin untuk secara tidak sengaja menggunakan nilai khusus sebagai pengganti nilai biasa:

 try:
   pos = find_string(haystack, needle)
   do_something_with(pos)
 except NotFound:
   ...

Atau, selalu mengembalikan jenis yang tidak dapat digunakan secara langsung tetapi harus dibuka dulu dapat digunakan, misalnya tuple bool hasil di mana boolean menunjukkan apakah pengecualian terjadi atau jika hasilnya dapat digunakan. Kemudian:

pos, ok = find_string(haystack, needle)
if not ok:
  ...
do_something_with(pos)

Ini memaksa Anda untuk menangani masalah dengan segera, tetapi itu menjengkelkan dengan sangat cepat. Ini juga mencegah Anda dari fungsi chaining dengan mudah. Setiap panggilan fungsi sekarang membutuhkan tiga baris kode. Golang adalah bahasa yang menganggap gangguan ini layak untuk keselamatan.

Jadi untuk meringkas, pengecualian tidak sepenuhnya tanpa masalah dan dapat digunakan secara berlebihan, terutama ketika mereka mengganti nilai pengembalian "normal". Tetapi ketika digunakan untuk memberi sinyal kondisi khusus (tidak harus hanya kesalahan), maka pengecualian dapat membantu Anda mengembangkan API yang bersih, intuitif, mudah digunakan, dan sulit disalahgunakan.

amon
sumber
1
Terima kasih atas jawaban mendalamnya. Saya berasal dari latar belakang bahasa lain seperti Jawa di mana "pengecualian hanya untuk kondisi luar biasa," jadi ini baru bagi saya. Apakah ada pengembang inti Python yang pernah menyatakan seperti ini dengan pedoman Python lainnya seperti EAFP, EIBTI, dll. (Di milis, posting blog, dll.) Saya ingin dapat mengutip sesuatu yang resmi jika dev / bos lain bertanya. Terima kasih!
JB
Dengan membebani metode fillInStackTrace dari kelas pengecualian kustom Anda untuk langsung kembali, Anda bisa membuatnya sangat murah untuk dinaikkan, karena stack-walking yang mahal dan ini adalah tempat dilakukannya.
John Cowan
Peluru pertama Anda di intro Anda membingungkan pada awalnya. Mungkin Anda bisa mengubahnya menjadi sesuatu seperti "Pengecualian penanganan kode dalam C ++ modern adalah nol biaya, tetapi melemparkan pengecualian itu mahal"? (untuk lurkers: stackoverflow.com/questions/13835817/… ) [sunting: edit yang diusulkan]
phresnel
Perhatikan bahwa untuk operasi pencarian kamus menggunakan collections.defaultdictatau my_dict.get(key, default)membuat kode lebih jelas daripadatry: my_dict[key] except: return default
Steve Barnes
11

TIDAK! - tidak secara umum - pengecualian tidak dianggap sebagai praktik kontrol aliran yang baik dengan pengecualian kelas kode tunggal. Satu tempat di mana pengecualian dianggap sebagai cara yang masuk akal, atau bahkan lebih baik, untuk memberi sinyal suatu kondisi adalah operasi generator atau iterator. Operasi ini dapat mengembalikan nilai yang mungkin sebagai hasil yang valid sehingga diperlukan mekanisme untuk memberi sinyal penyelesaian.

Pertimbangkan untuk membaca file biner stream satu byte pada satu waktu - benar-benar nilai apa pun adalah hasil yang berpotensi valid tetapi kita masih perlu memberi sinyal akhir file. Jadi kita punya pilihan, kembalikan dua nilai, (nilai byte & tanda yang valid), setiap kali atau ajukan pengecualian ketika tidak ada lagi yang harus dilakukan. Dalam dua kasus kode konsumsi dapat terlihat seperti:

# Using validity flag
valid, val = readbyte(source)
while valid:
    processbyte(val)
    valid, val = readbyte(source)
tidy_up()

kalau tidak:

# With exceptions
try:
   val = readbyte(source)
   processbyte(val) # Note if a problem occurs here it will also raise an exception
except Exception: # Use a specific exception here!
   tidy_up()

Tapi ini sudah, sejak PEP 343 diimplementasikan & porting kembali, semua rapi terbungkus dalam withpernyataan. Di atas menjadi, sangat pythonic:

with open(source) as input:
    for val in input.readbyte(): # This line will raise a StopIteration exception an call input.__exit__()
        processbyte(val) # Not called if there is nothing read

Dalam python3 ini menjadi:

for val in open(source, 'rb').read():
     processbyte(val)

Saya sangat mendorong Anda untuk membaca PEP 343 yang memberikan latar belakang, alasan, contoh, dll.

Juga biasa menggunakan pengecualian untuk menandai akhir pemrosesan saat menggunakan fungsi generator untuk memberi sinyal akhir.

Saya ingin menambahkan bahwa contoh pencari Anda hampir pasti mundur, fungsi tersebut harus menjadi generator, mengembalikan kecocokan pertama pada panggilan pertama, kemudian panggilan substituen mengembalikan kecocokan berikutnya, dan memunculkan NotFoundpengecualian ketika tidak ada lagi kecocokan.

Steve Barnes
sumber
Anda mengatakan: "TIDAK! - tidak secara umum - pengecualian tidak dianggap sebagai praktik kontrol aliran yang baik dengan pengecualian kelas kode tunggal". Di mana alasan untuk pernyataan empatik ini? Jawaban ini tampaknya berfokus sempit pada kasus iterasi. Ada banyak kasus lain di mana orang mungkin berpikir untuk menggunakan pengecualian. Apa masalah dengan melakukannya dalam kasus-kasus lain?
Daniel Waltrip
@DanielWaltrip Saya melihat kode terlalu banyak, dalam berbagai bahasa pemrograman, di mana pengecualian digunakan, sering terlalu luas, ketika sederhana ifakan lebih baik. Ketika datang untuk debugging & pengujian atau bahkan alasan tentang perilaku kode Anda, lebih baik tidak mengandalkan pengecualian - mereka harus menjadi pengecualian. Saya telah melihat, dalam kode produksi, perhitungan yang dikembalikan melalui lemparan / tangkapan alih-alih pengembalian sederhana ini berarti bahwa ketika perhitungan mencapai pembagian dengan kesalahan nol, ia mengembalikan nilai acak daripada menabrak di mana kesalahan terjadi hal ini menyebabkan beberapa jam debugging.
Steve Barnes
Hai @SteveBarnes, contoh pembagian dengan kesalahan nol terdengar seperti pemrograman yang buruk (dengan tangkapan yang terlalu umum yang tidak menaikkan kembali pengecualian).
wTyeRogers
Jawaban Anda tampaknya bermuara pada: A) "TIDAK!" B) ada contoh yang valid di mana aliran kontrol melalui pengecualian bekerja lebih baik daripada alternatif C) koreksi kode pertanyaan untuk menggunakan generator (yang menggunakan pengecualian sebagai aliran kontrol, dan melakukannya dengan baik). Walaupun jawaban Anda umumnya informatif, tidak ada argumen yang muncul di sana untuk mendukung item A, yang, dengan berani dan tampaknya merupakan pernyataan utama, saya mengharapkan beberapa dukungan untuk. Jika didukung, saya tidak akan menurunkan jawaban Anda. Sial ...
wTyeRogers