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.
Hubungan induk dan anak dapat dibuktikan dengan fungsi issubclass(). Akibat langsungnya terasa pada klausa except, karena menangkap induk berarti menangkap seluruh anaknya.
print(issubclass(ZeroDivisionError, ArithmeticError))
print(issubclass(ArithmeticError, Exception))
print(issubclass(FileNotFoundError, OSError))
print(issubclass(ValueError, Exception))True
True
True
Truetry:
hasil = 5 / 0
except ArithmeticError as e:
print(type(e).__name__, "tertangkap oleh except ArithmeticError")ZeroDivisionError tertangkap oleh except ArithmeticErrorZeroDivisionError 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.
try:
raise KeyboardInterrupt
except Exception:
print("tidak pernah dicetak")
except KeyboardInterrupt:
print("KeyboardInterrupt tertangkap secara eksplisit")KeyboardInterrupt tertangkap secara eksplisitKlausa 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.
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)except dalam: pesan dicatat, lalu exception dilempar ulang
except luar: usia tidak boleh negatif: -3Fungsi 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.
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)saldo akhir: 30000
gagal: saldo 50000 tidak mencukupi tarikan 80000
saldo tercatat: 50000Simpan berkas di atas di direktori kerja, lalu jalankan dengan perintah berikut.
# menjalankan contoh kelas exception kustom
python3 saldo.pysaldo akhir: 30000
gagal: saldo 50000 tidak mencukupi tarikan 80000
saldo tercatat: 50000Kelas 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.
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__)exception akhir: ValueError
__cause__: ZeroDivisionError
__context__: ZeroDivisionErrorAtribut __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.
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__)__cause__: None
__context__: ZeroDivisionError
__suppress_context__: TrueBentuk 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.
try:
raise SystemExit(0)
except BaseException:
print("SystemExit ikut tertangkap, program tidak pernah berhenti")SystemExit ikut tertangkap, program tidak pernah berhentiKesalahan 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, denganExceptionsebagai induk mayoritas, sehingga menangkap induk berarti menangkap semua anaknya. - Pernyataan
raisemembangkitkan exception secara manual, danraisetanpa argumen melempar ulang exception yang sedang ditangani. - Kelas exception kustom mewarisi
Exceptiondan bebas menyimpan atribut tambahan bagi penangan yang lebih kaya. - Exception yang dibangkitkan di dalam penangan membawa asalnya pada
__context__, bentukraise ... from ...menetapkannya pada__cause__, danfrom Nonemenyembunyikannya.
Referensi#
- Built-in Exceptions — Python Software Foundation — Pohon hierarki exception bawaan beserta penjelasan setiap kelasnya.
- The raise statement — Python Software Foundation — Referensi bahasa untuk bentuk raise, termasuk from untuk rantai exception.
- Errors and Exceptions — Python Software Foundation — Tutorial resmi tentang raise, exception kustom, dan rantai exception.