Back to IF5100 Pemrograman untuk Data Analitik

Exception sebagai Kontrol Alur dan Assertion

Questions/Cues

  • Mengapa raise exception lebih baik daripada mengembalikan nilai khusus?
  • Pada studi kasus get_stats/avg, di mana error muncul dan mengapa?
  • Apa tiga opsi menangani mahasiswa tanpa nilai, dan apa konsekuensinya?
  • Apa itu assert dan apa yang terjadi bila kondisinya gagal?
  • Bagaimana assertion mendukung defensive programming?
  • Di mana assertion sebaiknya dipakai, dan apa bedanya dari exception handling?

Reference Points

  • IF5100 — Exceptions and Assertions (Slides 9-20)

Exception sebagai Kontrol Alur

Exception juga bisa dipakai untuk mengatur alur eksekusi. Alih-alih mengembalikan nilai khusus saat terjadi error lalu memaksa pemanggil mengecek “apakah nilai error dikembalikan”, lebih baik lempar exception ketika fungsi tidak mampu menghasilkan hasil yang konsisten dengan spesifikasinya.

Contoh #1: get_ratios

Contoh pertama pada slide 10: fungsi yang membagi dua list elemen demi elemen. Pembagian nol dianggap hasil yang sah (diberi nan), sedangkan error lain dianggap argumen yang buruk sehingga fungsi melempar exception-nya sendiri (ValueError) untuk mengatur alur program:

def get_ratios(L1, L2):
    """ Assumes: L1 and L2 are lists of equal length of numbers
        Returns: a list containing L1[i]/L2[i] """
    ratios = []
    for index in range(len(L1)):
        try:
            ratios.append(L1[index]/L2[index])
        except ZeroDivisionError:
            ratios.append(float('nan'))  # nan = not a number
        except:
            raise ValueError('get_ratios called with bad arg')
    return ratios

Contoh: get_ratios([1, 4, 3], [2, 0, 3]) menghasilkan [0.5, nan, 1.0]; get_ratios([1, 2], ['a', 1]) atau list dengan panjang tidak sama melempar ValueError: get_ratios called with bad arg.

Studi Kasus: get_stats dan avg

Diberikan daftar kelas sebuah mata kuliah; tiap entri adalah list dua bagian — list nama depan/belakang, dan list nilai tugas:

def get_stats(class_list):
    new_stats = []
    for elt in class_list:
        new_stats.append([elt[0], elt[1], avg(elt[1])])
    return new_stats
 
def avg(grades):
    return sum(grades) / len(grades)

Dengan data sederhana, hasilnya sesuai harapan:

test_grades = [[['peter', 'parker'], [80.0, 70.0, 85.0]],
               [['bruce', 'wayne'], [100.0, 80.0, 74.0]]]
# get_stats(test_grades) ->
# [[['peter', 'parker'], [80.0, 70.0, 85.0], 78.33333],
#  [['bruce', 'wayne'], [100.0, 80.0, 74.0], 84.666667]]

Masalahnya: bila ada entri seperti [['deadpool'], []] (tanpa nilai), avg menjalankan sum([]) / len([]) → ZeroDivisionError: float division by zero. Data uji pada slide 12 sengaja memuat entri tak wajar:

test_grades = [[['peter', 'parker'], [10.0, 5.0, 85.0]],
               [['bruce', 'wayne'], [10.0, 8.0, 74.0]],
               [['captain', 'america'], [8.0, 10.0, 96.0], 17.5],  # entri 3 elemen (cacat)
               [['deadpool'], []]]

Entri captain america punya elemen ketiga (17.5) yang diabaikan get_stats karena hanya memakai elt[0] dan elt[1]; yang menggagalkan program tetap deadpool.

Keluaran contoh pada slide tidak cocok dengan data 15.41666666, 13.83333334, dan 17.5 untuk data di atas, padahal dihitung langsung avg menghasilkan 33.33, 30.67, dan 38.0 (mis. (10 + 5 + 85) / 3 = 33.33). Angka di slide tampaknya berasal dari data lain; yang penting dari contoh ini adalah bentuk keluarannya: deadpool bernilai None (opsi 1) atau 0.0 (opsi 2), disertai warning: no grades data.

Slide menampilkan rata-rata

Tiga Opsi Penanganan

flowchart TD
    Z["avg(grades) memicu ZeroDivisionError"] --> O1["Opsi 1: flag dengan pesan<br/>print warning, kembalikan None"]
    Z --> O2["Opsi 2: ubah kebijakan<br/>print warning, return 0.0"]
    Z --> O3["Opsi 3: re-raise<br/>print warning, raise e"]
    O3 --> C["Pemanggil wajib menangani;<br/>jika tidak, unhandled exception"]

Opsi 1 — tandai error dengan mencetak pesan. Beri tahu bahwa ada yang salah:

def avg(grades):
    try:
        return sum(grades) / len(grades)
    except ZeroDivisionError:
        print('warning: no grades data')

Hasilnya entri deadpool mendapat nilai None (fungsi tanpa return eksplisit).

Opsi 2 — ubah kebijakan. Putuskan bahwa mahasiswa tanpa nilai mendapat nol:

def avg(grades):
    try:
        return sum(grades) / len(grades)
    except ZeroDivisionError:
        print('warning: no grades data')
        return 0.0

Opsi 3 — re-raise. Catat/cetak error lalu biarkan pemanggil ikut menanganinya:

def avg(grades):
    try:
        return sum(grades) / len(grades)
    except ZeroDivisionError as e:
        print('warning: no grades data')
        raise e

Ini tidak fail silently — error tetap disinyalkan setelah dicatat. Pemanggil (atau pemanggil dari pemanggil, dst.) harus menangani exception itu; jika tidak, terjadi unhandled exception. Contoh penanganan di sisi pemanggil:

try:
    stats = get_stats(test_grades)
except ZeroDivisionError:
    print('class list contains a student without grades')

Dengan data di atas, keluarannya warning: no grades data (dari avg) diikuti class list contains a student without grades (dari pemanggil); stats tidak pernah terisi.

Assertion

Kita ingin memastikan asumsi tentang keadaan komputasi sesuai harapan. Statement assert melempar exception AssertionError bila asumsi tidak terpenuhi — contoh defensive programming yang baik:

def avg(grades):
    assert len(grades) > 0, "no grades data"
    return sum(grades) / len(grades)

Fungsi berhenti seketika bila assertion tidak terpenuhi — melempar AssertionError (dengan pesan opsional) bila grades berupa list kosong; selain itu berjalan normal.

Assertion sebagai Defensive Programming

  • Assertion tidak memberi programmer kendali atas respons terhadap kondisi tak terduga — untuk itu gunakan exception handling.
  • Assertion memastikan eksekusi berhenti kapan pun kondisi yang diharapkan tidak terpenuhi.
  • Biasanya dipakai untuk memeriksa input fungsi, tetapi bisa di mana saja.
  • Bisa dipakai untuk memeriksa output fungsi, mencegah nilai buruk merambat.
  • Membuat lokasi sumber bug lebih mudah ditemukan.

Di Mana Memakai Assertion

Tujuannya menemukan bug sedini mungkin dan memperjelas di mana ia terjadi; dipakai sebagai pelengkap testing. Gunakan assertion untuk:

  • memeriksa tipe argumen atau nilai;
  • memeriksa bahwa invarian struktur data terpenuhi;
  • memeriksa batasan pada nilai return;
  • memeriksa pelanggaran constraint pada prosedur (mis. tidak ada duplikat dalam list).

Terpisah dari assertion, slide 20 juga menyebut: raise exception bila pengguna memberi data input yang buruk — validasi data dari pengguna memakai exception (if ...: raise ValueError(...)), bukan assert, karena assert ditujukan untuk memeriksa asumsi programmer dan dapat dinonaktifkan.

Summary

Exception sebagai kontrol alur: daripada mengembalikan nilai khusus yang harus dicek pemanggil, raise exception saat fungsi tak bisa memenuhi spesifikasinya. Pada studi kasus get_stats/avg, entri tanpa nilai (deadpool) memicu ZeroDivisionError di sum(grades)/len(grades). Tiga opsi: (1) cetak warning dan kembalikan None; (2) ubah kebijakan → return 0.0; (3) re-raise (raise e) — tidak fail silently, tetapi pemanggil wajib menangani. Assertion (assert kondisi, "pesan") melempar AssertionError dan menghentikan eksekusi seketika bila asumsi gagal — defensive programming. Bedanya dari exception handling: assertion tidak mengatur respons terhadap kondisi tak terduga, ia hanya menjamin berhenti saat kondisi harapan dilanggar. Pakai untuk mengecek input/output, tipe, invarian struktur data, dan constraint — sebagai pelengkap testing untuk menemukan bug sedini mungkin.