Kapan dan bagaimana saya harus menggunakan pengecualian?

20

Pengaturan

Saya sering mengalami kesulitan menentukan kapan dan bagaimana menggunakan pengecualian. Mari kita perhatikan contoh sederhana: misalkan saya sedang menggores halaman web, katakan " http://www.abevigoda.com/ ", untuk menentukan apakah Abe Vigoda masih hidup. Untuk melakukan ini, yang perlu kita lakukan adalah mengunduh halaman dan mencari waktu di mana frasa "Abe Vigoda" muncul. Kami mengembalikan penampilan pertama, karena itu termasuk status Abe. Secara konseptual, akan terlihat seperti ini:

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

Di mana parse_abe_status(s)mengambil string dari bentuk "Abe Vigoda adalah sesuatu " dan mengembalikan bagian " sesuatu ".

Sebelum Anda berargumen bahwa ada banyak cara yang lebih baik dan lebih kuat untuk menggores halaman ini untuk status Abe, ingat bahwa ini hanyalah contoh sederhana dan dibuat-buat yang digunakan untuk menyoroti situasi umum yang saya alami.

Sekarang, di mana kode ini dapat mengalami masalah? Di antara kesalahan lain, beberapa kesalahan "yang diharapkan" adalah:

  • download_pagemungkin tidak dapat mengunduh halaman, dan melempar IOError.
  • URL mungkin tidak mengarah ke halaman kanan, atau halaman tersebut diunduh secara salah, sehingga tidak ada klik. hitsadalah daftar kosong, lalu.
  • Halaman web telah diubah, mungkin membuat asumsi kami tentang halaman yang salah. Mungkin kita mengharapkan 4 menyebutkan Abe Vigoda, tetapi sekarang kita menemukan 5.
  • Untuk beberapa alasan, hits[0]mungkin bukan string dari bentuk "Abe Vigoda adalah sesuatu ", dan karenanya tidak dapat diuraikan dengan benar.

Kasus pertama sebenarnya bukan masalah bagi saya: sebuah IOErrordilemparkan dan dapat ditangani oleh penelepon fungsi saya. Jadi mari kita pertimbangkan kasus-kasus lain dan bagaimana saya bisa menanganinya. Tetapi pertama-tama, mari kita asumsikan bahwa kita menerapkan parse_abe_statusdengan cara paling bodoh yang mungkin:

def parse_abe_status(s):
    return s[13:]

Yaitu, tidak melakukan pengecekan kesalahan. Sekarang, ke opsi:

Opsi 1: Kembali None

Saya dapat memberi tahu penelepon bahwa ada yang tidak beres dengan kembali None:

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    if not hits:
        return None

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

Jika penelepon menerima Nonedari fungsi saya, ia harus berasumsi bahwa tidak ada yang menyebutkan Abe Vigoda, dan ada yang salah. Tapi ini agak kabur, kan? Dan itu tidak membantu kasus di mana hits[0]bukan apa yang kita pikirkan.

Di sisi lain, kita dapat memasukkan beberapa pengecualian:

Opsi 2: Menggunakan Pengecualian

Jika hitskosong, sebuah IndexErrorakan terlempar ketika kita mencoba hits[0]. Tapi si penelepon seharusnya tidak diharapkan menangani masalah yang IndexErrordilemparkan oleh fungsi saya, karena dia tidak tahu dari mana IndexErrordatangnya; itu bisa saja dibuang find_all_mentions, untuk semua yang dia tahu. Jadi kami akan membuat kelas pengecualian khusus untuk menangani ini:

class NotFoundError(Exception):
    """Throw this when something can't be found on a page."""

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    try:
        hits[0]
    except IndexError:
        raise NotFoundError("No mentions found.")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

Sekarang bagaimana jika halaman telah berubah dan ada jumlah hit yang tidak terduga? Ini bukan bencana, karena kodenya mungkin masih berfungsi, tetapi pemanggil mungkin ingin ekstra hati - hati, atau dia mungkin ingin mencatat peringatan. Jadi saya akan memberikan peringatan:

class NotFoundError(Exception):
    """Throw this when something can't be found on a page."""

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    try:
        hits[0]
    except IndexError:
        raise NotFoundError("No mentions found.")

    # say we expect four hits...
    if len(hits) != 4:
        raise Warning("An unexpected number of hits.")
        logger.warning("An unexpected number of hits.")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

Terakhir, kita mungkin menemukan bahwa statusitu tidak hidup atau mati. Mungkin, untuk beberapa alasan aneh, hari ini ternyata demikian comatose. Maka saya tidak ingin kembali False, karena itu menyiratkan bahwa Abe sudah mati. Apa yang harus saya lakukan di sini? Lempar pengecualian, mungkin. Tapi jenis apa? Haruskah saya membuat kelas pengecualian khusus?

class NotFoundError(Exception):
    """Throw this when something can't be found on a page."""

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    try:
        hits[0]
    except IndexError:
        raise NotFoundError("No mentions found.")

    # say we expect four hits...
    if len(hits) != 4:
        raise Warning("An unexpected number of hits.")
        logger.warning("An unexpected number of hits.")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    if status not in ['alive', 'dead']:
        raise SomeTypeOfError("Status is an unexpected value.")

    # he's either alive or dead
    return status == "alive"

Opsi 3: Di suatu tempat di antara keduanya

Saya pikir metode kedua, dengan pengecualian, lebih disukai, tetapi saya tidak yakin apakah saya menggunakan pengecualian dengan benar di dalamnya. Saya ingin tahu bagaimana programmer yang lebih berpengalaman akan menangani ini.

jme
sumber

Jawaban:

17

Rekomendasi dalam Python adalah menggunakan pengecualian untuk menunjukkan kegagalan. Ini benar bahkan jika Anda mengharapkan kegagalan secara teratur.

Lihatlah dari sudut pandang penelepon kode Anda:

my_status = get_abe_status(my_url)

Bagaimana jika kita kembali? Jika penelepon tidak secara khusus menangani kasus yang get_abe_status gagal, itu hanya akan mencoba untuk melanjutkan dengan my_stats menjadi Tidak Ada. Itu mungkin menghasilkan bug yang sulit untuk didiagnosis di kemudian hari. Bahkan jika Anda memeriksa Tidak Ada, kode ini tidak memiliki petunjuk mengapa get_abe_status () gagal.

Tetapi bagaimana jika kita mengajukan pengecualian? Jika penelepon tidak secara khusus menangani kasus ini, pengecualian akan merambat ke atas pada akhirnya mengenai penangan pengecualian default. Itu mungkin bukan yang Anda inginkan, tetapi lebih baik daripada memperkenalkan bug halus di tempat lain dalam program ini. Selain itu, pengecualian memberikan informasi tentang apa yang salah yang hilang di versi pertama.

Dari sudut pandang penelepon, lebih mudah untuk mendapatkan pengecualian daripada nilai balik. Dan itulah gaya python, untuk menggunakan pengecualian untuk menunjukkan kondisi kegagalan tidak mengembalikan nilai.

Beberapa akan mengambil perspektif yang berbeda dan berpendapat bahwa Anda hanya boleh menggunakan pengecualian untuk kasus-kasus yang tidak pernah Anda harapkan terjadi. Mereka berpendapat bahwa biasanya menjalankan berlari tidak boleh menimbulkan pengecualian. Salah satu alasan yang diberikan untuk ini adalah bahwa pengecualian sangat tidak efisien, tetapi itu tidak benar untuk Python.

Beberapa poin pada kode Anda:

try:
    hits[0]
except IndexError:
    raise NotFoundError("No mentions found.")

Itu cara yang sangat membingungkan untuk memeriksa daftar kosong. Jangan memaksakan pengecualian hanya untuk memeriksa sesuatu. Gunakan if.

# say we expect four hits...
if len(hits) != 4:
    raise Warning("An unexpected number of hits.")
    logger.warning("An unexpected number of hits.")

Anda menyadari bahwa baris logger.warning tidak akan pernah berjalan dengan benar?

Winston Ewert
sumber
1
Terima kasih (terlambat) atas tanggapan Anda. Itu, bersama dengan melihat kode yang diterbitkan, telah meningkatkan perasaan saya tentang kapan dan bagaimana membuang pengecualian.
jme
4

Jawaban yang diterima layak diterima dan menjawab pertanyaan, saya menulis ini hanya untuk memberikan sedikit latar belakang tambahan.

Salah satu kredo Python adalah: lebih mudah untuk meminta maaf daripada izin. Ini berarti bahwa biasanya Anda hanya melakukan sesuatu, dan jika Anda mengharapkan pengecualian, Anda menanganinya. Berbeda dengan melakukan jika memeriksa sebelumnya untuk memastikan Anda tidak akan mendapatkan pengecualian.

Saya ingin memberikan contoh untuk menunjukkan seberapa dramatis perbedaan mentalitas dari C ++ / Java. A for loop di C ++ biasanya terlihat seperti:

for(int i = 0; i != myvector.size(); ++i) ...

Cara untuk berpikir tentang ini: mengakses di myvector[k]mana k> = myvector.size () akan menyebabkan pengecualian. Jadi pada prinsipnya Anda bisa menulis ini (sangat canggung) sebagai try-catch.

    for(int i = 0; ; ++i)  {
        try {
           ...
        } catch (& std::out_of_range)
             break

Atau yang serupa. Sekarang, pertimbangkan apa yang terjadi dalam python untuk loop:

for i in range(1):
    ...

Bagaimana cara kerjanya? For loop mengambil hasil dari range (1) dan memanggil iter () di atasnya, meraih iterator ke sana.

b = range(1).__iter__()

Kemudian ia memanggil selanjutnya di setiap iterasi loop, sampai ...:

>>> next(b)
0
>>> next(b)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Dengan kata lain, for for in python sebenarnya adalah try-kecuali dalam penyamaran.

Sejauh pertanyaan konkret, ingat bahwa pengecualian menghentikan eksekusi fungsi normal dan harus ditangani secara terpisah. Dengan Python, Anda harus dengan bebas membuangnya kapan pun tidak ada gunanya mengeksekusi sisa kode di fungsi Anda, dan / atau tidak ada pengembalian yang benar mencerminkan apa yang terjadi dalam fungsi. Perhatikan bahwa kembali lebih awal dari suatu fungsi berbeda: kembali lebih awal berarti Anda sudah menemukan jawabannya dan tidak perlu kode lainnya untuk mengetahui jawabannya. Saya mengatakan bahwa pengecualian harus dilemparkan ketika jawabannya tidak diketahui, dan kode lainnya untuk menentukan jawaban tidak dapat dijalankan secara wajar. Sekarang, "mencerminkan dengan benar" itu sendiri, seperti pengecualian yang Anda pilih untuk dilempar, semua adalah masalah dokumentasi.

Dalam hal kode khusus Anda, saya akan mengatakan situasi apa pun yang menyebabkan hit menjadi daftar kosong harus dibuang. Mengapa? Nah, cara fungsi Anda diatur, tidak ada cara untuk menentukan jawaban tanpa menguraikan hits. Jadi jika hit tidak dapat diuraikan, baik karena URL buruk, atau karena hit kosong, maka fungsinya tidak dapat menjawab pertanyaan, dan pada kenyataannya bahkan tidak dapat benar-benar mencobanya.

Dalam kasus khusus ini, saya berpendapat bahwa bahkan jika Anda berhasil mengurai dan tidak mendapatkan jawaban yang masuk akal (hidup atau mati), maka Anda harus tetap melempar. Mengapa? Karena, fungsi mengembalikan boolean. Kembali Tidak ada yang sangat berbahaya bagi klien Anda. Jika mereka melakukan jika centang pada Tidak ada, tidak akan ada kegagalan, itu hanya akan diam-diam diperlakukan sebagai Salah. Jadi, klien Anda pada dasarnya akan selalu harus melakukan jika tidak ada yang memeriksa jika ia tidak ingin kegagalan diam ... jadi Anda mungkin harus melempar saja.

Nir Friedman
sumber
2

Anda harus menggunakan pengecualian saat sesuatu yang luar biasa terjadi. Artinya, sesuatu yang seharusnya tidak terjadi diberikan penggunaan aplikasi yang tepat. Jika diizinkan dan diharapkan bagi konsumen metode Anda untuk mencari sesuatu yang tidak akan ditemukan, maka "tidak ditemukan" bukanlah kasus yang luar biasa. Dalam hal ini, Anda harus mengembalikan nol atau "Tidak Ada" atau {}, atau sesuatu yang menunjukkan set pengembalian kosong.

Jika, di sisi lain, Anda benar-benar mengharapkan konsumen metode Anda untuk selalu (kecuali mereka entah bagaimana gagal) menemukan apa yang sedang dicari, maka tidak menemukan itu akan menjadi pengecualian dan Anda harus pergi dengan itu.

Kuncinya adalah bahwa penanganan pengecualian bisa mahal - pengecualian diharapkan untuk mengumpulkan informasi tentang keadaan aplikasi Anda saat itu terjadi, seperti jejak tumpukan, untuk membantu orang menguraikan mengapa itu terjadi. Saya tidak berpikir itu yang Anda coba lakukan.

Matthew Flynn
sumber
1
Jika Anda memutuskan bahwa tidak menemukan nilai diperbolehkan, berhati-hatilah dengan apa yang Anda gunakan untuk menunjukkan bahwa itulah yang terjadi. Jika metode Anda seharusnya mengembalikan a Stringdan Anda memilih "Tidak Ada" sebagai indikator Anda, ini berarti Anda harus berhati-hati bahwa "Tidak Ada" tidak akan pernah menjadi nilai yang valid. Perhatikan juga bahwa ada perbedaan antara melihat data dan tidak menemukan nilai dan tidak dapat mengambil data, sehingga kami tidak dapat menemukan data. Memiliki hasil yang sama untuk dua kasus ini berarti Anda tidak memiliki visibilitas begitu Anda tidak mendapatkan nilai ketika Anda berharap akan ada satu.
unholysampler
Blok kode sebaris ditandai dengan backticks (`), mungkin itu yang Anda maksud dengan" Tidak Ada "?
Izkata
3
Saya khawatir ini benar-benar salah dengan Python. Anda menerapkan alasan gaya C ++ / Java ke bahasa lain. Python menggunakan pengecualian untuk menunjukkan akhir dari for for loop; itu sangat tidak biasa.
Nir Friedman
2

Jika saya sedang menulis fungsi

 def abe_is_alive():

Saya akan menulisnya ke return Trueatau Falsedalam kasus-kasus di mana saya benar-benar yakin akan satu atau yang lain, dan raisekesalahan dalam kasus lain (misalnya raise ValueError("Status neither 'dead' nor 'alive'")). Ini karena fungsi memanggil tambang mengharapkan boolean, dan jika saya tidak bisa memastikannya, aliran program reguler seharusnya tidak dilanjutkan.

Sesuatu seperti contoh Anda mendapatkan jumlah "hit" yang berbeda dari yang diharapkan, saya mungkin akan mengabaikan; selama salah satu hit masih cocok dengan pola saya "Abe Vigoda is {dead | hidup}", tidak apa-apa. Ini memungkinkan halaman untuk diatur ulang tetapi masih mendapatkan informasi yang sesuai.

Daripada

try:
    hits[0] 
except IndexError:
    raise NotFoundError

Saya akan memeriksa secara eksplisit:

if not hits:
    raise NotFoundError

karena ini cenderung "lebih murah" kemudian menyiapkan try.

Saya setuju dengan Anda pada IOError; Saya juga tidak akan mencoba untuk menangani kesalahan menghubungkan ke situs web - jika kita tidak bisa, karena alasan tertentu, ini bukan tempat yang tepat untuk menanganinya (karena tidak membantu kami menjawab pertanyaan kami) dan itu harus lulus keluar ke fungsi panggilan.

Jonrsharpe
sumber