Membaca traceback dan debugging

Membaca anatomi traceback serta teknik debugging dengan print dan debugger pdb.

Daftar isi

Ketika exception tidak tertangani, Python menghentikan program dan mencetak laporan yang disebut traceback. Laporan itu bukan tanda kekacauan, melainkan peta kejadian yang menyebut berkas, baris, dan fungsi tempat kegagalan berlangsung. Topik ini membahas anatomi traceback, bentuk traceback berantai, debugging dengan print, perekaman traceback dengan modul traceback, serta pemakaian debugger pdb.

Anatomi traceback#

Program berikut sengaja memanggil fungsi bagi() dengan pembagi nol agar exception yang ditimbulkan tidak tertangani. Komentar pada baris pertama adalah penanda tutorial dan tidak berpengaruh pada jalannya program.

python bagi-nol.py
# tidak-dijalankan: berkas ini sengaja error, jalankan lewat terminal
def bagi(a, b):
    return a / b

print("mulai")
print(bagi(10, 0))
print("tidak pernah tercetak")

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

bash
# menjalankan program yang exceptionnya tidak tertangani
python3 bagi-nol.py
text Keluaran
mulai
Traceback (most recent call last):
  File "bagi-nol.py", line 6, in <module>
    print(bagi(10, 0))
          ^^^^^^^^^^^
  File "bagi-nol.py", line 3, in bagi
    return a / b
           ~~^~~
ZeroDivisionError: division by zero

Traceback dibaca dari bawah ke atas. Baris paling bawah menyebut jenis exception beserta pesannya, yaitu ZeroDivisionError: division by zero, dan baris itulah yang langsung menyebabkan program berhenti. Baris-baris di atasnya merupakan rantai pemanggilan (call stack), masing-masing menyebut berkas, nomor baris, dan nama fungsinya, lalu menampilkan potongan kode sumbernya. Karena tajuknya berbunyi Traceback (most recent call last), pemanggilan terbaru berada paling bawah, sehingga pembacaan dimulai dari situ dan bergerak ke luar menuju pemanggil.

Baris tanda sirkumfleks menyorot bagian ekspresi yang persis memicu kegagalan, fitur yang tersedia sejak Python 3.11. Pada keluaran di atas, tanda tersebut menunjuk pemanggilan bagi(10, 0) di baris pemanggil dan ekspresi a / b di dalam fungsinya. Pernyataan terakhir pada berkas tidak pernah dijalankan karena eksekusi sudah berhenti lebih dahulu.

Traceback berantai#

Ketika exception dibangkitkan di dalam penanganan exception lain, traceback memuat dua bagian yang dihubungkan keterangan penghubung. Berkas berikut meneruskan KeyError dari pembacaan konfigurasi sebagai RuntimeError dengan bentuk from.

python rantai.py
# tidak-dijalankan: berkas ini sengaja error, jalankan lewat terminal
def baca_konfigurasi(kamus):
    return kamus["mode"]


def jalankan():
    try:
        return baca_konfigurasi({"pilihan": "cepat"})
    except KeyError as e:
        raise RuntimeError("konfigurasi tidak lengkap") from e


jalankan()
bash
# menjalankan program dengan exception berantai
python3 rantai.py
text Keluaran
Traceback (most recent call last):
  File "rantai.py", line 8, in jalankan
    return baca_konfigurasi({"pilihan": "cepat"})
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "rantai.py", line 3, in baca_konfigurasi
    return kamus["mode"]
           ~~~~~^^^^^^^^
KeyError: 'mode'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "rantai.py", line 13, in <module>
    jalankan()
  File "rantai.py", line 10, in jalankan
    raise RuntimeError("konfigurasi tidak lengkap") from e
RuntimeError: konfigurasi tidak lengkap

Bagian pertama menampilkan KeyError sampai ke fungsinya, yaitu baca_konfigurasi(). Keterangan The above exception was the direct cause of the following exception menegaskan bahwa exception kedua dibangkitkan secara eksplisit dari yang pertama, sesuai bentuk raise ... from yang ditulis di jalankan(). Bagian kedua lalu menampilkan RuntimeError pada baris raise-nya. Apabila from tidak dipakai, keterangan penghubungnya berbunyi During handling of the above exception, another exception occurred.

Debugging dengan print#

Cara debugging yang paling langsung adalah mencetak nilai variabel pada titik yang dicurigai, sebelum baris yang gagal dijalankan. Teknik ini sederhana dan cukup untuk mayoritas bug kecil, selama cetakannya dibersihkan setelah masalah ditemukan.

python Contoh: print pada titik yang dicurigai
def rata_rata(nilai, pembagi):
    print("debug: nilai =", nilai)
    print("debug: pembagi =", pembagi)
    print("debug: type(pembagi) =", type(pembagi).__name__)
    return sum(nilai) / pembagi


try:
    rata_rata([80, 75, 90], 0)
except ZeroDivisionError as e:
    print("error:", type(e).__name__, "-", e)
text Keluaran
debug: nilai = [80, 75, 90]
debug: pembagi = 0
debug: type(pembagi) = int
error: ZeroDivisionError - division by zero

Cetakan menunjukkan pembagi bernilai 0 dan bertipe int, sehingga penyebab ZeroDivisionError langsung terlihat. Tanpa cetakan itu, penyebabnya harus ditebak dari traceback yang hanya menunjukkan lokasinya. Pola pencetakan tipe seperti ini cepat mengungkap kesalahan seperti string yang lolos masuk sebagai angka.

Sejak Python 3.8, f-string memiliki penentu berupa tanda sama dengan (=) yang mencetak ekspresi beserta nilainya sekaligus.

python Contoh: penentu = pada f-string untuk debug
pembagi = 0
nilai = [80, 75, 90]
print(f"{pembagi=} {sum(nilai)=}")
text Keluaran
pembagi=0 sum(nilai)=245

Bentuk f"{pembagi=}" setara dengan mencetak nama variabel dan nilainya sekaligus, sehingga baris debug menjadi lebih ringkas dan seragam. Penentunya bekerja untuk ekspresi apa pun, termasuk sum(nilai) pada contoh di atas.

Modul traceback#

Terkadang program harus mencatat traceback tanpa berhenti, misalnya untuk log kegagalan yang ditangani. Fungsi traceback.format_exc() mengembalikan laporan traceback exception yang sedang ditangani sebagai string, sehingga laporan yang sama dengan keluaran interpreter dapat disimpan atau diproses lebih lanjut.

python Contoh: merekam traceback sebagai string
import traceback


def bagi(a, b):
    return a / b


try:
    bagi(10, 0)
except ZeroDivisionError:
    baris = traceback.format_exc().strip().splitlines()
    print("baris pertama:", baris[0])
    print("baris terakhir:", baris[-1])
text Keluaran
baris pertama: Traceback (most recent call last):
baris terakhir: ZeroDivisionError: division by zero

Contoh hanya mencetak baris pertama dan terakhirnya, tetapi isi penuhnya memuat seluruh rantai pemanggilan seperti pada bagian anatomi. Modul ini juga menyediakan traceback.print_exc() yang mencetak langsung, serta format_exception() untuk pemformatan yang lebih rinci.

Debugger pdb#

print menunjukkan nilai pada satu titik, sedangkan debugger menghentikan program, melangkahinya per pernyataan, dan memeriksanya secara interaktif. Debugger bawaan Python bernama pdb, dan cara paling ringkas memicunya dari dalam kode adalah fungsi breakpoint().

python Contoh: memicu debugger dengan breakpoint()
# tidak-dijalankan (pdb interaktif menunggu masukan di terminal)
def bagi(a, b):
    return a / b


breakpoint()
print(bagi(10, 0))

Ketika breakpoint() tercapai, terminal menampilkan prompt (Pdb) dan menunggu perintah. Perintah p a mencetak nilai a, perintah n melangkahi satu pernyataan, perintah s masuk ke dalam fungsi yang dipanggil, perintah c melanjutkan eksekusi, dan perintah q keluar dari debugger.

Debugger juga dipanggil dari terminal dengan python3 -m pdb bagi-nol.py. Agar sesinya dapat direkam, demo berikut mengirim perintahnya melalui pipe, dengan breakpoint di baris 3 yang berisi return a / b.

bash
# mengirim perintah pdb melalui pipe, diakhiri perintah q
printf 'b 3\nc\np a\np b\nq\n' | python3 -m pdb bagi-nol.py
text Keluaran
> /home/kal/my-project/site/content/python/exception/traceback-dan-debugging/bagi-nol.py(2)<module>()
-> def bagi(a, b):
(Pdb) Breakpoint 1 at /home/kal/my-project/site/content/python/exception/traceback-dan-debugging/bagi-nol.py:3
(Pdb) mulai
> /home/kal/my-project/site/content/python/exception/traceback-dan-debugging/bagi-nol.py(3)bagi()
-> return a / b
(Pdb) 10
(Pdb) 0
(Pdb)

Jalur berkas yang panjang pada keluaran adalah lokasi berkas di komputer yang menjalankan contoh ini. Transkripnya memperlihatkan siklus kerja pdb, yaitu b 3 memasang breakpoint di baris 3, c menjalankan program sampai breakpointnya tercapai, lalu prompt (Pdb) menunggu perintah di situ. Perintah p a dan p b mencetak nilai kedua parameter sebelum baris return dijalankan, sehingga pembagi 0 sudah terlihat sebelum kegagalan terjadi. Perintah q mengakhiri debugger.

Kesalahan umum#

Kesalahan pertama adalah membaca traceback dari atas ke bawah. Rangkaiannya menyusun pemanggilan tertua di atas, sehingga baris teratas sering berupa pemanggil yang tidak bersalah. Kebiasaan yang tepat adalah membaca jenis exception pada baris paling bawah terlebih dahulu, lalu naik ke frame terdalam yang relevan, yaitu lingkup eksekusi satu fungsi, dan baru menelusuri pemanggilnya apabila perlu.

Kesalahan kedua adalah membiarkan cetakan debug tinggal di kode produksi. Baris print yang tertinggal mengotori keluaran program dan dapat membocorkan isi data. Setelah akar masalah ditemukan, hapus cetakannya, atau pindahkan ke modul logging yang tingkatnya dapat diatur.

Kesalahan ketiga adalah tertukar antara perintah n dan s pada pdb. Perintah n mengeksekusi seluruh pemanggilan fungsi dalam satu langkah, sedangkan s masuk ke dalam fungsinya. Menggunakan s pada fungsi pustaka standar membawa debugger ke kode yang tidak relevan, sehingga n menjadi pilihan utama dan s dipakai hanya ketika isi fungsi memang ingin diperiksa.

Ringkasan#

  • Traceback dibaca dari bawah ke atas, dengan jenis dan pesan exception pada baris terakhir, lalu rantai berkas, baris, dan fungsi di atasnya.
  • Exception berantai menghasilkan traceback dua bagian yang dihubungkan keterangan direct cause atau During handling.
  • Fungsi traceback.format_exc() merekam traceback exception yang sedang ditangani sebagai string untuk kebutuhan log.
  • Debugger pdb menghentikan program pada breakpoint, memeriksa nilai dengan p, melangkah dengan n dan s, serta keluar dengan q.

Referensi#