Hierarki exception dan raise

Hierarki exception bawaan, pernyataan raise, kelas exception kustom, dan exception chaining.

Daftar isi

Semua exception pada Python adalah objek kelas yang tersusun dalam satu pohon pewarisan, sehingga penangkapannya dapat ditempatkan pada tingkat yang paling tepat. Topik ini membahas hubungan antar kelas exception bawaan, pernyataan raise untuk membangkitkan exception secara manual, pembuatan kelas exception khusus aplikasi, serta rantai exception (exception chaining) yang menyimpan konteks kegagalan sebelumnya. Setelah topik ini dibaca, kegagalan aplikasi dapat dilaporkan dengan jenis yang tepat dan ditangani tanpa kehilangan jejak penyebabnya.

Hierarki exception bawaan#

Kelas paling atas pada pohon tersebut adalah BaseException. Hampir seluruh exception sehari-hari berada di bawah Exception, sedangkan SystemExit, KeyboardInterrupt, dan GeneratorExit menjadi anak langsung BaseException. Pemisahan ini penting karena except Exception menangkap kesalahan program, tetapi membiarkan sinyal penghentian lewat. Diagram berikut menampilkan potongan pohon yang paling sering ditemui.

BaseExceptionSystemExitKeyboardInterruptExceptionGeneratorExitArithmeticErrorLookupErrorValueErrorTypeErrorOSErrorZeroDivisionErrorKeyErrorFileNotFoundError
Potongan hierarki exception bawaan Python

Hubungan induk dan anak dapat dibuktikan dengan fungsi issubclass(). Akibat langsungnya terasa pada klausa except, karena menangkap induk berarti menangkap seluruh anaknya.

python Contoh: hubungan antar kelas exception
print(issubclass(ZeroDivisionError, ArithmeticError))
print(issubclass(ArithmeticError, Exception))
print(issubclass(FileNotFoundError, OSError))
print(issubclass(ValueError, Exception))
text Keluaran
True
True
True
True
python Contoh: menangkap induk ikut menangkap anak
try:
    hasil = 5 / 0
except ArithmeticError as e:
    print(type(e).__name__, "tertangkap oleh except ArithmeticError")
text Keluaran
ZeroDivisionError tertangkap oleh except ArithmeticError

ZeroDivisionError adalah anak ArithmeticError, sehingga klausa induknya menangkapnya. Cara ini praktis apabila beberapa jenis ingin diperlakukan sama. Namun, semakin tinggi induk yang dipilih, semakin banyak jenis yang ikut tertangkap, sehingga kegagalan yang berbeda menjadi sulit dibedakan. Pilih induk setinggi yang benar-benar dibutuhkan, tidak lebih.

Perilaku pada catatan di atas dapat dibuktikan dengan program kecil.

python Contoh: Exception tidak menangkap KeyboardInterrupt
try:
    raise KeyboardInterrupt
except Exception:
    print("tidak pernah dicetak")
except KeyboardInterrupt:
    print("KeyboardInterrupt tertangkap secara eksplisit")
text Keluaran
KeyboardInterrupt tertangkap secara eksplisit

Klausa pertama tidak cocok karena KeyboardInterrupt berada di luar pohon Exception, sehingga pemeriksaan berlanjut ke klausa kedua yang menyebutnya secara eksplisit. Inilah alasan penangkap yang terlalu umum tetap membiarkan sinyal penghentian lewat.

Pernyataan raise#

Exception tidak selalu berasal dari interpreter. Pernyataan raise membangkitkan exception secara manual, dengan menyebut kelasnya atau instance-nya yang sudah dilengkapi pesan. Inilah cara sebuah fungsi melaporkan bahwa keadaan yang diterimanya tidak dapat diproses, misalnya argumen yang berada di luar rentang yang sah. Kelas yang dipilih sebaiknya menggambarkan jenis kegagalannya, bukan sekadar Exception generik.

python Contoh: raise dan raise ulang
def validasi_usia(usia):
    if usia < 0:
        raise ValueError(f"usia tidak boleh negatif: {usia}")
    return usia


try:
    try:
        validasi_usia(-3)
    except ValueError:
        print("except dalam: pesan dicatat, lalu exception dilempar ulang")
        raise
except ValueError as e:
    print("except luar:", e)
text Keluaran
except dalam: pesan dicatat, lalu exception dilempar ulang
except luar: usia tidak boleh negatif: -3

Fungsi validasi_usia() menolak masukan negatif dengan raise ValueError yang disertai pesan. Klausa except di dalam mencatat kejadian, lalu pernyataan raise tanpa argumen melempar ulang exception yang sedang ditangani, sehingga pemanggilnya tetap mendapat kesempatan menangani. Pola ini dipakai ketika sebuah lapisan kode hanya ingin mencatat kegagalan tanpa mengambil alih penanganannya.

Penyebutan kelas pada raise, misalnya raise ValueError, otomatis membuat instance tanpa pesan, sedangkan raise ValueError(pesan) menyertakan pesan. Bentuk kedua lebih informatif dan menjadi kebiasaan yang dianjurkan.

Kelas exception kustom#

Ketika aplikasi memiliki kondisi gagal yang khas, kondisi itu sebaiknya memiliki kelas exception sendiri. Kelas kustom dibuat dengan mewarisi Exception, dan konstruktornya bebas menyimpan data tambahan yang berguna bagi penangan. Dengan nama yang spesifik, klausa except menjadi ekspresif karena maksud kegagalannya terbaca langsung dari nama kelasnya.

python saldo.py
class SaldoTidakCukupError(Exception):
    """Dibangkitkan ketika penarikan melebihi saldo yang tersedia."""

    def __init__(self, saldo, tarikan):
        super().__init__(f"saldo {saldo} tidak mencukupi tarikan {tarikan}")
        self.saldo = saldo
        self.tarikan = tarikan


def tarik(saldo, jumlah):
    if jumlah > saldo:
        raise SaldoTidakCukupError(saldo, jumlah)
    return saldo - jumlah


print("saldo akhir:", tarik(50_000, 20_000))
try:
    tarik(50_000, 80_000)
except SaldoTidakCukupError as e:
    print("gagal:", e)
    print("saldo tercatat:", e.saldo)
text Keluaran
saldo akhir: 30000
gagal: saldo 50000 tidak mencukupi tarikan 80000
saldo tercatat: 50000

Simpan berkas di atas di direktori kerja, lalu jalankan dengan perintah berikut.

bash
# menjalankan contoh kelas exception kustom
python3 saldo.py
text Keluaran
saldo akhir: 30000
gagal: saldo 50000 tidak mencukupi tarikan 80000
saldo tercatat: 50000

Kelas SaldoTidakCukupError menyimpan saldo dan tarikan sebagai atribut, sehingga penangan membaca keduanya tanpa mengurai pesan. Pesan lengkapnya tetap tersedia karena konstruktor meneruskannya ke Exception melalui super(). Perhatikan juga bahwa tarik() tidak pernah menangkap exceptionnya sendiri. Fungsi itu hanya membangkitkan, sedangkan penanganan berada di lapisan pemanggil.

Rantai exception#

Exception yang dibangkitkan di dalam blok except membawa konteks exception asalnya. Python menyimpan exception asal pada atribut __context__. Apabila exception baru memang merupakan akibat dari exception lama, bentuk raise ... from ... mengisi atribut __cause__ dan menandai hubungan sebab-akibatnya secara eksplisit. Kedua atribut terlihat pada traceback sebagai keterangan tambahan di antara dua bagian laporan.

python Contoh: atribut __cause__ dan __context__
try:
    try:
        1 / 0
    except ZeroDivisionError as asal:
        raise ValueError("data tidak dapat diproses") from asal
except ValueError as akhir:
    print("exception akhir:", type(akhir).__name__)
    print("__cause__:", type(akhir.__cause__).__name__)
    print("__context__:", type(akhir.__context__).__name__)
text Keluaran
exception akhir: ValueError
__cause__: ZeroDivisionError
__context__: ZeroDivisionError

Atribut __cause__ berisi ZeroDivisionError karena bentuk from dipakai. Atribut __context__ berisi exception yang sama, tetapi atribut ini terisi juga tanpa from, yaitu ketika exception kedua dibangkitkan tanpa sengaja di dalam penangan. Perbedaan keduanya terlihat pada traceback, dan bentuk eksplisit menyatakan maksud penulisnya dengan lebih jelas.

python Contoh: menyembunyikan konteks dengan from None
try:
    try:
        1 / 0
    except ZeroDivisionError:
        raise ValueError("data tidak dapat diproses") from None
except ValueError as akhir:
    print("__cause__:", akhir.__cause__)
    print("__context__:", type(akhir.__context__).__name__)
    print("__suppress_context__:", akhir.__suppress_context__)
text Keluaran
__cause__: None
__context__: ZeroDivisionError
__suppress_context__: True

Bentuk from None menandai exception baru yang berdiri sendiri. Atribut __suppress_context__ bernilai True, dan traceback tidak lagi menampilkan bagian exception lama. Bentuk ini dipilih apabila exception asal hanyalah detail implementasi yang tidak bermanfaat bagi pembaca log.

Kesalahan umum#

Kesalahan pertama adalah membangkitkan Exception secara generik. Pernyataan raise Exception("pesan") sah secara sintaks, tetapi pemanggil tidak dapat membedakan kegagalan yang satu dengan yang lain kecuali mengurai pesannya. Kelas yang spesifik, bawaan maupun kustom, menjaga klausa except tetap presisi.

Kesalahan kedua adalah menangkap BaseException. Klausa tersebut lebih luas lagi daripada except Exception, sehingga sinyal penghentian ikut tertelan.

python Contoh: BaseException menangkap SystemExit
try:
    raise SystemExit(0)
except BaseException:
    print("SystemExit ikut tertangkap, program tidak pernah berhenti")
text Keluaran
SystemExit ikut tertangkap, program tidak pernah berhenti

Kesalahan ketiga adalah kehilangan konteks ketika menerjemahkan exception. Exception baru yang dibangkitkan di dalam except tanpa bentuk from tetap dirangkai melalui __context__, tetapi hubungan sebab-akibatnya tidak dinyatakan. Menggunakan from membuat maksudnya terbaca pada traceback, sedangkan from None hanya dipilih apabila konteks lama memang tidak relevan.

Ringkasan#

  • Seluruh exception bawaan tersusun di bawah BaseException, dengan Exception sebagai induk mayoritas, sehingga menangkap induk berarti menangkap semua anaknya.
  • Pernyataan raise membangkitkan exception secara manual, dan raise tanpa argumen melempar ulang exception yang sedang ditangani.
  • Kelas exception kustom mewarisi Exception dan bebas menyimpan atribut tambahan bagi penangan yang lebih kaya.
  • Exception yang dibangkitkan di dalam penangan membawa asalnya pada __context__, bentuk raise ... from ... menetapkannya pada __cause__, dan from None menyembunyikannya.

Referensi#