41. Debugging dan Pengujian Kode Anda
Menulis kode baru setengah dari perjuangan. Setengah lainnya adalah memastikan kode Anda bekerja dengan benar dan menemukan masalah saat tidak bekerja. Setiap programmer, dari pemula hingga ahli, menulis kode yang punya bug. Bedanya adalah programmer berpengalaman sudah mengembangkan pendekatan yang sistematis untuk menemukan dan memperbaiki bug tersebut.
Di bab ini, Anda akan mempelajari teknik debugging praktis yang membantu Anda memahami apa yang sebenarnya dilakukan kode Anda, menemukan masalah dengan cepat, dan memverifikasi bahwa kode Anda bekerja sesuai yang diinginkan. Keterampilan ini akan membuat Anda jadi programmer yang lebih percaya diri dan efektif.
41.1) Membaca Tracebacks untuk Menemukan Letak Error (Ulasan Singkat)
Seperti yang kita pelajari di Bab 24, Python menyediakan pesan error yang detail yang disebut tracebacks ketika sesuatu berjalan salah. Mari kita ulas cara membacanya dengan efektif, karena ini adalah lini pertahanan pertama Anda saat debugging.
41.1.1) Anatomi Sebuah Traceback
Ketika Python menemui error, Python menunjukkan tepat di mana masalah terjadi dan jenis error-nya. Berikut contoh traceback yang umum:
def calculate_average(numbers):
total = sum(numbers)
count = len(numbers)
return total / count
def process_student_grades(grades):
average = calculate_average(grades)
return f"Average: {average:.1f}"
# Ini akan menyebabkan error
student_grades = []
result = process_student_grades(student_grades)
print(result)Output:
Traceback (most recent call last):
File "grades.py", line 12, in <module>
result = process_student_grades(student_grades)
File "grades.py", line 7, in process_student_grades
average = calculate_average(grades)
File "grades.py", line 4, in calculate_average
return total / count
~~~~~~^~~~~~~
ZeroDivisionError: division by zeroMari kita uraikan apa yang diberitahu traceback ini:
Membaca dari bawah ke atas:
- Jenis error dan pesannya (paling bawah):
ZeroDivisionError: division by zeromemberi tahu kita dengan tepat apa yang salah - Baris yang tepat tempat error terjadi:
return total / countpada baris 4 - Rantai pemanggilan (call chain) yang menunjukkan bagaimana kita sampai ke sana: mulai di baris 12, lewat baris 7, berakhir di baris 4
41.1.2) Menggunakan Tracebacks untuk Menemukan Akar Masalah
Traceback menunjukkan gejala (di mana error terjadi), tetapi Anda perlu menemukan penyebabnya (kenapa itu terjadi). Mari telusuri masalahnya:
# Error terjadi di sini
return total / count # count bernilai 0
# Tapi masalah sebenarnya ada di sini
student_grades = [] # List kosong diteruskan ke fungsiPembagian dengan nol terjadi karena kita meneruskan list kosong. Traceback menunjuk ke baris 4, tetapi perbaikannya perlu dilakukan lebih awal—entah dengan memvalidasi input atau menangani kasus list kosong:
def calculate_average(numbers):
"""Mengembalikan rata-rata dari numbers, atau None jika list kosong."""
if not numbers:
return None
return sum(numbers) / len(numbers)
def process_student_grades(grades):
"""Memproses nilai siswa dan mengembalikan string yang sudah diformat."""
average = calculate_average(grades)
if average is None:
return "No grades to process"
return f"Average: {average:.1f}"
# Sekarang ini bekerja dengan aman
student_grades = []
result = process_student_grades(student_grades)
print(result) # Output: No grades to process
# Dan ini juga bekerja
student_grades = [85, 92, 78, 90]
result = process_student_grades(student_grades)
print(result) # Output: Average: 86.2Poin Penting:
- Baca tracebacks dari bawah ke atas
- Lokasi error (gejala) tidak selalu merupakan akar penyebabnya
- Validasi input sejak awal untuk mencegah error di kemudian hari
- Gunakan defensive programming (
.get(), pemeriksaan panjang) untuk kode yang lebih aman
Berbagai jenis error menghasilkan traceback yang berbeda, tetapi proses membacanya selalu sama: mulai dari bawah untuk melihat apa yang salah, lalu telusuri ke atas untuk memahami bagaimana Anda sampai ke sana. Jika Anda perlu mengingat kembali tipe exception tertentu, lihat lagi Bab 24.
Sekarang Anda bisa membaca tracebacks dengan efektif, mari pelajari cara menelusuri kode secara mental untuk memahami apa yang dilakukannya langkah demi langkah.
41.2) Menelusuri Eksekusi Kode Secara Mental
Kadang Anda menemukan bug tetapi tidak bisa langsung menjalankan kodenya—mungkin Anda sedang meninjau kode di kertas, membaca pull request orang lain, atau mencoba memahami kenapa sebuah fungsi(function) berperilaku tidak seperti yang diharapkan. Dalam situasi ini, eksekusi mental (mental execution)—menelusuri kode baris demi baris di kepala Anda, melacak apa yang terjadi pada setiap variabel—menjadi sangat berharga.
Bahkan programmer berpengalaman memakai teknik ini secara rutin. Sebelum menambahkan print statement atau menjalankan debugger, mereka sering menelusuri beberapa iterasi secara mental untuk membentuk hipotesis tentang di mana kira-kira masalahnya. Ini lebih cepat daripada trial-and-error dan membantu Anda memahami kode lebih dalam.
Eksekusi mental sangat berguna ketika:
- Membaca kode yang tidak familiar untuk memahami apa yang dilakukannya
- Meninjau fungsi(function) kecil (5-15 baris) sebelum menjalankannya
- Debugging error logika ketika kode berjalan tetapi menghasilkan hasil yang salah
- Memahami perilaku loop (loop) ketika polanya tidak langsung jelas
- Code review ketika Anda tidak mudah menjalankan kodenya sendiri
Untuk kode yang lebih besar atau lebih kompleks, Anda akan menggabungkan penelusuran mental dengan teknik lain yang akan kita bahas nanti di bab ini. Namun menguasai keterampilan ini akan membuat Anda jadi debugger yang jauh lebih efektif.
41.2.1) Proses Eksekusi Mental
Ketika Anda mengeksekusi kode secara mental, Anda berperan sebagai interpreter Python, mengikuti aturan yang sama seperti Python. Mari berlatih dengan contoh sederhana:
def find_maximum(numbers):
max_value = numbers[0]
for num in numbers:
if num > max_value:
max_value = num
return max_value
result = find_maximum([3, 7, 2, 9, 5])
print(result) # Output: 9Berikut cara menelusuri kode ini:
Penelusuran langkah demi langkah:
Initial state:
numbers = [3, 7, 2, 9, 5]
max_value = 3 (numbers[0])
Iteration 1: num = 3
Check: 3 > 3? → False
max_value remains 3
Iteration 2: num = 7
Check: 7 > 3? → True
max_value = 7 ✓
Iteration 3: num = 2
Check: 2 > 7? → False
max_value remains 7
Iteration 4: num = 9
Check: 9 > 7? → True
max_value = 9 ✓
Iteration 5: num = 5
Check: 5 > 9? → False
max_value remains 9
Return: 941.2.2) Membuat Tabel Jejak (Trace Table)
Untuk kode yang lebih kompleks, buat tabel jejak (trace table) yang menunjukkan bagaimana variabel berubah seiring waktu. Ini sangat membantu untuk loop dan struktur bertingkat:
def calculate_running_totals(numbers):
totals = []
running_sum = 0
for num in numbers:
running_sum += num
totals.append(running_sum)
return totals
result = calculate_running_totals([10, 20, 30, 40])
print(result) # Output: [10, 30, 60, 100]Tabel jejak:
Tabel ini menunjukkan keadaan variabel di setiap langkah. Perhatikan bagaimana running_sum berubah dari "sebelum" ke "sesudah" setiap penjumlahan:
| Iteration | num | running_sum (before) | running_sum (after) | totals |
|---|---|---|---|---|
| Start | - | 0 | 0 | [] |
| 1 | 10 | 0 | 10 | [10] |
| 2 | 20 | 10 | 30 | [10, 30] |
| 3 | 30 | 30 | 60 | [10, 30, 60] |
| 4 | 40 | 60 | 100 | [10, 30, 60, 100] |
Membuat tabel ini membantu Anda melihat secara persis bagaimana data mengalir melalui kode Anda. Jika output tidak sesuai yang Anda harapkan, Anda bisa menunjukkan dengan tepat di mana semuanya mulai salah.
41.2.3) Menelusuri Logika Kondisional
Pernyataan kondisional membutuhkan perhatian yang cermat terhadap cabang mana yang dieksekusi. Mari telusuri contoh yang lebih kompleks:
def categorize_grade(score):
if score >= 90:
category = "Excellent"
bonus = 10
elif score >= 80:
category = "Good"
bonus = 5
elif score >= 70:
category = "Satisfactory"
bonus = 0
else:
category = "Needs Improvement"
bonus = 0
final_score = score + bonus
return category, final_score
result = categorize_grade(85)
print(result) # Output: ('Good', 90)Jejak mental untuk score = 85:
- Cek
85 >= 90→ False, lewati blok pertama - Cek
85 >= 80→ True, masuk blok kedua - Set
category = "Good"danbonus = 5 - Lewati blok elif dan else sisanya (sudah menemukan yang cocok)
- Hitung
final_score = 85 + 5 = 90 - Return
("Good", 90)
41.2.4) Menelusuri Pemanggilan Fungsi dan Return
Ketika fungsi(function) memanggil fungsi lain, Anda perlu melacak call stack—urutan pemanggilan fungsi dan variabel lokalnya:
def calculate_tax(amount, rate):
tax = amount * rate
return tax
def calculate_total(price, quantity, tax_rate):
subtotal = price * quantity
tax = calculate_tax(subtotal, tax_rate)
total = subtotal + tax
return total
result = calculate_total(50, 3, 0.08)
print(f"Total: ${result:.2f}") # Output: Total: $162.00Jejak dengan call stack:
┌─ calculate_total(50, 3, 0.08)
│ price = 50, quantity = 3, tax_rate = 0.08
│ subtotal = 150
│
│ ┌─ calculate_tax(150, 0.08)
│ │ amount = 150, rate = 0.08
│ │ tax = 12.0
│ │ return 12.0
│ └─
│
│ tax = 12.0 (from calculate_tax)
│ total = 162.0
│ return 162.0
└─
result = 162.0Jejak langkah demi langkah ini menunjukkan secara persis bagaimana data mengalir antar fungsi. Saat debugging, jika hasil akhirnya salah, Anda bisa menelusuri balik untuk melihat fungsi mana yang menghasilkan nilai perantara yang keliru.
Penelusuran mental itu kuat, tetapi untuk kode kompleks bisa terasa melelahkan. Di bagian berikutnya, kita akan belajar cara menggunakan print statement secara strategis untuk melihat apa yang benar-benar terjadi saat kode berjalan, yang sering kali lebih cepat dan lebih andal daripada eksekusi mental saja.
41.3) Debugging dengan Print: f"{var=}" dan repr()
Walaupun eksekusi mental bekerja dengan baik untuk fungsi kecil, itu jadi tidak praktis untuk kode yang lebih besar atau lebih kompleks. Saat Anda tidak yakin apa yang terjadi di dalam sebuah loop, atau ketika sebuah perhitungan menghasilkan hasil yang tidak terduga, cara tercepat untuk menyelidiki sering kali adalah menambahkan print() secara strategis.
Print debugging punya beberapa keunggulan dibanding teknik lain:
- Tidak perlu tools khusus: Bekerja di lingkungan Python mana pun
- Cepat diterapkan: Tambahkan print statement dalam hitungan detik
- Output jelas: Anda melihat persis apa yang Anda minta
- Mudah dihapus: Hapus print setelah selesai
Developer profesional menggunakan print debugging setiap saat—ini bukan teknik "pemula". Mari pelajari cara memakainya dengan efektif.
41.3.1) Print Debugging Dasar
Pendekatan debugging paling sederhana adalah mencetak nilai variabel di titik-titik penting dalam kode Anda:
def process_order(items, discount_rate):
print(f"Starting process_order")
print(f"Items: {items}")
print(f"Discount rate: {discount_rate}")
subtotal = sum(item['price'] * item['quantity'] for item in items)
print(f"Subtotal: {subtotal}")
discount = subtotal * discount_rate
print(f"Discount amount: {discount}")
total = subtotal - discount
print(f"Final total: {total}")
return total
order_items = [
{'name': 'Book', 'price': 25.99, 'quantity': 2},
{'name': 'Pen', 'price': 3.50, 'quantity': 5}
]
result = process_order(order_items, 0.10)Output:
Starting process_order
Items: [{'name': 'Book', 'price': 25.99, 'quantity': 2}, {'name': 'Pen', 'price': 3.5, 'quantity': 5}]
Discount rate: 0.1
Subtotal: 69.47999999999999
Discount amount: 6.9479999999999995
Final total: 62.53199999999999Print statement ini menunjukkan alur eksekusi dan nilainya di setiap langkah. Jika hasil akhirnya salah, Anda bisa melihat dengan tepat di mana perhitungannya mulai melenceng.
41.3.2) Menggunakan f"{var=}" untuk Inspeksi Cepat
Python 3.8 memperkenalkan sintaks debugging yang praktis: f"{var=}". Ini mencetak nama variabel sekaligus nilainya:
def calculate_compound_interest(principal, rate, years):
# Pendekatan tradisional
print(f"principal: {principal}")
print(f"rate: {rate}")
print(f"years: {years}")
# Pendekatan yang lebih rapi dengan f"{var=}"
print(f"{principal=}")
print(f"{rate=}")
print(f"{years=}")
# Anda bisa memakai ekspresi, bukan hanya variabel
print(f"{principal * rate=}")
print(f"{(1 + rate) ** years=}")
amount = principal * (1 + rate) ** years
print(f"{amount=}")
return amount
result = calculate_compound_interest(1000, 0.05, 10)Output:
principal: 1000
rate: 0.05
years: 10
principal=1000
rate=0.05
years=10
principal * rate=50.0
(1 + rate) ** years=1.628894626777442
amount=1628.89462677744241.3.3) Menggunakan repr() untuk Melihat Bentuk Data yang Sebenarnya
Kadang apa yang Anda lihat saat dicetak bukanlah yang Anda kira. Fungsi repr() menunjukkan representasi persis dari sebuah objek, termasuk karakter tersembunyi:
# String ini terlihat sama ketika dicetak
text1 = "Hello"
text2 = "Hello\n" # Punya newline di akhir
print("Using print():")
print(f"text1: {text1}")
print(f"text2: {text2}")
print("\nUsing repr():")
print(f"text1: {repr(text1)}")
print(f"text2: {repr(text2)}")Output:
Using print():
text1: Hello
text2: Hello
Using repr():
text1: 'Hello'
text2: 'Hello\n'Output repr() menunjukkan bahwa text2 punya karakter newline tersembunyi. Ini sangat krusial saat debugging pemrosesan string:
def clean_user_input():
# Input pengguna sering punya whitespace tersembunyi
username = input("Enter username: ") # Pengguna mengetik "Alice "
print(f"Username with print(): {username}")
print(f"Username with repr(): {repr(username)}")
# Bersihkan input
cleaned = username.strip()
print(f"Cleaned with repr(): {repr(cleaned)}")
return cleanedJika pengguna mengetik "Alice" lalu spasi dan menekan Enter, Anda mungkin melihat:
Output:
Enter username: Alice
Username with print(): Alice
Username with repr(): 'Alice '
Cleaned with repr(): 'Alice'Output repr() mengungkap spasi di akhir yang tidak ditampilkan dengan jelas oleh print().
Kapan memakai repr() vs str():
repr() dirancang untuk developer—ia menunjukkan representasi string "resmi" yang bisa merekonstruksi objek. str() (yang dipakai print() secara default) dirancang untuk end user—ia menunjukkan versi yang mudah dibaca dan ramah.
Untuk debugging, repr() biasanya lebih membantu karena ia mengungkap struktur data yang sebenarnya.
41.3.4) Penempatan Print yang Strategis
Jangan asal menyebar print statement di mana-mana. Letakkan secara strategis:
def calculate_shipping_cost(weight, distance, express=False):
print(f"=== calculate_shipping_cost called ===")
print(f"Input: {weight=}, {distance=}, {express=}")
# Hitung biaya dasar
base_rate = 0.50
base_cost = weight * distance * base_rate
print(f"Calculated: {base_cost=}")
# Terapkan biaya tambahan express
if express:
surcharge = base_cost * 0.50
print(f"Express surcharge: {surcharge=}")
total = base_cost + surcharge
else:
print("No express surcharge")
total = base_cost
print(f"Final: {total=}")
print(f"=== calculate_shipping_cost returning ===\n")
return total
# Uji berbagai skenario
cost1 = calculate_shipping_cost(10, 500, express=True)
cost2 = calculate_shipping_cost(5, 200, express=False)Output:
=== calculate_shipping_cost called ===
Input: weight=10, distance=500, express=True
Calculated: base_cost=2500.0
Express surcharge: surcharge=1250.0
Final: total=3750.0
=== calculate_shipping_cost returning ===
=== calculate_shipping_cost called ===
Input: weight=5, distance=200, express=False
Calculated: base_cost=500.0
No express surcharge
Final: total=500.0
=== calculate_shipping_cost returning ===Penanda yang jelas (===) dan output yang terorganisir membuat alur eksekusi mudah diikuti.
41.3.5) Menghapus Print Debug
Setelah Anda menemukan dan memperbaiki bug, ingat untuk menghapus print debug Anda. Berikut beberapa strateginya:
Strategi 1: Gunakan prefix yang khas
# Mudah ditemukan dan dihapus dengan search/replace
print(f"DEBUG: {total=}")
print(f"DEBUG: {items=}")Strategi 2: Gunakan debug flag
DEBUG = True
def calculate_total(items):
if DEBUG:
print(f"Processing {len(items)} items")
total = sum(item['price'] for item in items)
if DEBUG:
print(f"{total=}")
return total
# Matikan semua output debug sekaligus
DEBUG = FalseStrategi 3: Comment out tapi simpan
def process_data(data):
# print(f"DEBUG: {data=}") # Berguna untuk debugging di masa depan
result = transform(data)
# print(f"DEBUG: {result=}")
return resultUntuk logging yang lebih canggih dan bisa Anda biarkan di kode produksi, Python punya modul logging, tetapi print statement sederhana sangat cocok untuk debugging cepat saat development.
Print debugging menunjukkan nilai variabel, tetapi kadang Anda perlu memahami struktur sebuah objek—method apa yang dimilikinya, tipe apa itu, dan apa yang bisa dilakukannya. Di bagian berikutnya, kita akan belajar cara menginspeksi objek dengan type() dan dir().
41.4) Menginspeksi Objek: type() dan dir()
Print debugging menunjukkan nilai variabel Anda, tetapi kadang masalahnya bukan nilainya—melainkan tipe objek yang Anda gunakan. Anda mungkin mengira itu list tetapi ternyata string, atau Anda bekerja dengan objek yang tidak familiar dan tidak tahu method apa saja yang didukung.
Python menyediakan tool bawaan untuk menginspeksi objek: type() memberi tahu Anda objek itu jenis apa, dan dir() menunjukkan operasi apa saja yang didukung. Fungsi ini penting ketika:
- Debugging error terkait tipe (TypeError, AttributeError)
- Bekerja dengan library atau API yang tidak familiar
- Memahami objek yang dikembalikan oleh kode pihak ketiga
- Memverifikasi bahwa kode Anda menerima tipe yang diharapkan
Mari pelajari cara menggunakan tool inspeksi ini dengan efektif.
41.4.1) Menggunakan type() untuk Mengidentifikasi Tipe Objek
Fungsi type() memberi tahu Anda dengan tepat objek itu jenis apa. Ini krusial saat debugging error terkait tipe:
def process_data(data):
print(f"Received data: {data}")
print(f"Data type: {type(data)}")
if isinstance(data, list):
print("Processing as list")
return sum(data)
elif isinstance(data, dict):
print("Processing as dictionary")
return sum(data.values())
else:
print("Unexpected type!")
return None
# Uji dengan tipe yang berbeda
result1 = process_data([10, 20, 30])
print(f"Result: {result1}\n")
result2 = process_data({'a': 10, 'b': 20, 'c': 30})
print(f"Result: {result2}\n")
result3 = process_data("123")
print(f"Result: {result3}")Output:
Received data: [10, 20, 30]
Data type: <class 'list'>
Processing as list
Result: 60
Received data: {'a': 10, 'b': 20, 'c': 30}
Data type: <class 'dict'>
Processing as dictionary
Result: 60
Received data: 123
Data type: <class 'str'>
Unexpected type!
Result: None41.4.2) Debugging Kebingungan Tipe
Kebingungan tipe adalah sumber bug yang umum, terutama ketika bekerja dengan fungsi(function) yang bisa menerima data dari berbagai sumber—input pengguna, pembacaan file, respons API, atau fungsi lain. Anda mungkin mengira menerima list angka tetapi ternyata string, atau mengira dictionary tetapi yang diterima list.
Menggunakan type() membantu mengidentifikasi kapan Anda punya tipe yang salah. Dengan mencetak tipe sejak awal fungsi, Anda bisa langsung melihat ketidaksesuaian tipe sebelum itu memicu pesan error yang membingungkan lebih dalam di kode Anda:
def calculate_average(numbers):
print(f"{type(numbers)=}")
print(f"{numbers=}") # Tunjukkan apa yang sebenarnya kita terima
# Ini akan gagal jika numbers bukan list berisi angka
total = sum(numbers)
count = len(numbers)
return total / count
# Kesalahan umum: lupa mengonversi string ke list
scores = "85" # Seharusnya [85] atau cukup 85
try:
avg = calculate_average(scores)
print(f"Average: {avg}")
except TypeError as e:
print(f"TypeError: {e}")
print(f"Expected list of numbers, got {type(scores)}")
print(f"The string contains: {repr(scores)}")Output:
type(numbers)=<class 'str'>
numbers='85'
TypeError: unsupported operand type(s) for +: 'int' and 'str'
Expected list of numbers, got <class 'str'>
The string contains: '85'Pengecekan type() langsung mengungkap masalahnya: kita mengirim string padahal yang dibutuhkan list. Tanpa output debug ini, Anda mungkin menghabiskan waktu untuk mencoba memahami kenapa sum() gagal, padahal masalah sebenarnya adalah tipe data yang salah masuk ke fungsi sejak awal.
41.4.3) Menggunakan dir() untuk Menemukan Method yang Tersedia
Saat bekerja dengan objek yang tidak familiar—entah dari library yang sedang Anda pelajari, respons API, atau bahkan tipe bawaan Python—Anda sering perlu tahu: "Apa yang bisa saya lakukan dengan objek ini?" Fungsi dir() menjawab pertanyaan ini dengan menampilkan semua atribut dan method yang tersedia pada objek.
Ini sangat berharga ketika:
- Anda sedang mengeksplorasi library baru dan ingin melihat method apa yang disediakan oleh sebuah objek
- Anda menerima objek dari kode pihak ketiga dan perlu memahami kemampuannya
- Anda lupa nama tepat sebuah method yang ingin digunakan
- Anda sedang debugging dan ingin memverifikasi bahwa objek memiliki method yang Anda harapkan
Mari eksplor method apa saja yang dimiliki string:
# Mengeksplor method apa saja yang dimiliki string
text = "Python Programming"
print(f"Type: {type(text)}")
print(f"\nAvailable string methods (showing first 10):")
methods = [m for m in dir(text) if not m.startswith('_')]
for method in methods[:10]: # Tampilkan 10 pertama
print(f" {method}")
print(f" ... and {len(methods) - 10} more")Output:
Type: <class 'str'>
Available string methods (showing first 10):
capitalize
casefold
center
count
encode
endswith
expandtabs
find
format
format_map
... and 37 moreSekarang Anda bisa melihat semua operasi yang tersedia pada string. Jika Anda tidak yakin apakah string punya method count atau endswith, dir() menunjukkan bahwa keduanya ada. Anda lalu bisa menggunakan fungsi help() Python untuk mempelajari lebih jauh method tertentu:
# Pelajari lebih lanjut tentang method tertentu
help(text.count)Ini akan menampilkan dokumentasi untuk method count:
Help on built-in function count:
count(sub[, start[, end]], /) method of builtins.str instance
Return the number of non-overlapping occurrences of substring sub in string S[start:end].
Optional arguments start and end are interpreted as in slice notation.Fungsi dir() seperti punya dokumentasi yang tertanam langsung di Python—ia menunjukkan apa saja yang mungkin dilakukan dengan objek apa pun yang sedang Anda pakai.
41.4.4) Menginspeksi Objek Kustom
Saat bekerja dengan class kustom, type() dan dir() membantu Anda memahami apa yang sedang Anda hadapi. Selain itu, Python menyediakan hasattr() untuk mengecek apakah sebuah objek punya atribut tertentu sebelum mencoba mengaksesnya—ini mencegah exception AttributeError.
class Student:
def __init__(self, name, grade):
self.name = name
self.grade = grade
def get_status(self):
return "Passing" if self.grade >= 60 else "Failing"
student = Student("Alice", 85)
print(f"Object type: {type(student)}")
print(f"\nAvailable attributes and methods:")
for attr in dir(student):
if not attr.startswith('_'):
print(f" {attr}")
# Cek apakah atribut tertentu ada
print(f"\nHas 'name' attribute: {hasattr(student, 'name')}")
print(f"Has 'age' attribute: {hasattr(student, 'age')}")
print(f"Has 'get_status' method: {hasattr(student, 'get_status')}")
# Sekarang kita bisa mengakses atribut yang kita tahu ada dengan aman
if hasattr(student, 'name'):
print(f"\nStudent name: {student.name}")
else:
print("\nNo name attribute found")
if hasattr(student, 'get_status'):
print(f"Status: {student.get_status()}")
else:
print("No get_status method found")
# Ini mencegah error seperti ini:
# print(student.age) # Would raise AttributeError!Output:
Object type: <class '__main__.Student'>
Available attributes and methods:
get_status
grade
name
Has 'name' attribute: True
Has 'age' attribute: False
Has 'get_status' method: True
Student name: Alice
Status: PassingFungsi hasattr() penting untuk menulis kode defensif—kode yang mengecek apakah operasi aman sebelum menjalankannya. Fungsi ini mengembalikan True jika atribut ada, False jika tidak—memungkinkan Anda mengambil keputusan sebelum mencoba mengakses atribut. Ini sangat penting saat bekerja dengan objek dari library eksternal atau input pengguna ketika Anda tidak bisa menjamin atribut apa saja yang akan ada.
41.4.5) Menggunakan getattr() untuk Akses Atribut yang Aman
Saat Anda tidak yakin apakah suatu atribut ada, gunakan getattr() dengan nilai default:
def display_student_info(student):
"""Menampilkan info siswa dengan aman walaupun beberapa atribut tidak ada."""
print(f"Type: {type(student)}")
# Akses atribut aman dengan default
name = getattr(student, 'name', 'Unknown')
grade = getattr(student, 'grade', 0)
age = getattr(student, 'age', 'Not specified')
print(f"Name: {name}")
print(f"Grade: {grade}")
print(f"Age: {age}")
# Cek apakah method ada sebelum memanggil
if hasattr(student, 'get_status'):
status = student.get_status()
print(f"Status: {status}")
# Menggunakan class Student yang sama seperti di atas
student = Student("Bob", 72)
display_student_info(student)Output:
Type: <class '__main__.Student'>
Name: Bob
Grade: 72
Age: Not specified
Status: PassingPendekatan ini mencegah exception AttributeError saat bekerja dengan objek yang mungkin tidak punya semua atribut yang diharapkan. Fungsi getattr() sangat berguna ketika:
- Bekerja dengan objek dari API eksternal yang mungkin memiliki versi berbeda
- Menangani atribut opsional dalam class Anda sendiri
- Membangun kode defensif yang menangani data yang hilang dengan elegan
Memahami tipe objek yang Anda miliki dan method apa yang didukung sangat krusial untuk debugging. Namun terkadang Anda perlu memverifikasi bukan hanya bahwa kode berjalan, tetapi juga bahwa ia menghasilkan hasil yang benar. Di bagian berikutnya, kita akan belajar cara menggunakan pernyataan assert untuk menguji asumsi Anda dan menangkap bug lebih awal.
41.5) Menguji dengan Pernyataan assert
Kita sudah belajar cara debugging kode ketika sesuatu berjalan salah—membaca tracebacks, menelusuri eksekusi secara mental, menggunakan print statement, dan menginspeksi objek. Namun ada pendekatan yang lebih baik daripada memperbaiki bug setelah muncul: mencegahnya sejak awal melalui pengujian.
Pernyataan assert adalah tool pengujian paling sederhana di Python. Ia memungkinkan Anda memverifikasi bahwa kode Anda berperilaku benar dengan mengecek asumsi di titik-titik kritis. Ketika sebuah assertion gagal, Python langsung memberi tahu persis apa yang salah dan di mana, sehingga jauh lebih mudah menangkap bug lebih awal—sering kali bahkan sebelum Anda menjalankan program utama.
Assertion sangat berharga untuk:
- Memverifikasi bahwa fungsi menghasilkan hasil yang diharapkan
- Mengecek bahwa input memenuhi kebutuhan Anda
- Menguji edge case yang bisa merusak kode Anda
- Mendokumentasikan asumsi yang diandalkan kode Anda
Anggap assertion sebagai pemeriksaan otomatis yang terus-menerus memverifikasi bahwa kode Anda bekerja sesuai yang diinginkan. Mari pelajari cara menggunakannya dengan efektif.
41.5.1) Apa yang Dilakukan assert
Pernyataan assert mengecek apakah sebuah kondisi bernilai true. Jika kondisinya true, tidak terjadi apa pun—kode berjalan normal. Jika false, Python melempar AssertionError dan menghentikan eksekusi.
Sintaks:
assert condition, "Optional error message"condition: Ekspresi apa pun yang dievaluasi menjadi True atau False"Optional error message": Teks bantuan yang ditampilkan ketika assertion gagal
Berikut cara kerjanya dalam praktik:
# Assertion sederhana
x = 10
assert x > 0 # Lolos tanpa suara (x memang > 0)
assert x < 5 # Gagal! Memunculkan AssertionError
# Dengan pesan error (jauh lebih membantu!)
assert x > 0, f"x must be positive, got {x}"
assert x < 5, f"x must be less than 5, got {x}" # Gagal dengan pesan yang jelasSekarang mari lihat assertion di fungsi nyata:
def calculate_discount(price, discount_percent):
# Verifikasi input valid
assert price >= 0, "Price cannot be negative"
assert 0 <= discount_percent <= 100, "Discount must be between 0 and 100"
discount_amount = price * (discount_percent / 100)
final_price = price - discount_amount
# Verifikasi output masuk akal
assert final_price >= 0, "Final price cannot be negative"
return final_price
# Input valid berjalan baik
result = calculate_discount(100, 20)
print(f"Price after 20% discount: ${result}") # Output: Price after 20% discount: $80.0
# Input tidak valid memicu assertion
try:
result = calculate_discount(-50, 20)
except AssertionError as e:
print(f"Assertion failed: {e}") # Output: Assertion failed: Price cannot be negative
try:
result = calculate_discount(100, 150)
except AssertionError as e:
print(f"Assertion failed: {e}") # Output: Assertion failed: Discount must be between 0 and 10041.5.2) Menggunakan Assertion untuk Memverifikasi Perilaku Fungsi
Assertion sangat bagus untuk menguji bahwa fungsi menghasilkan hasil yang diharapkan:
def calculate_average(numbers):
if not numbers:
return 0.0
return sum(numbers) / len(numbers)
# Uji dengan berbagai input
result = calculate_average([10, 20, 30])
assert result == 20.0, f"Expected 20.0, got {result}"
print(f"Test 1 passed: average of [10, 20, 30] = {result}")
result = calculate_average([5, 5, 5, 5])
assert result == 5.0, f"Expected 5.0, got {result}"
print(f"Test 2 passed: average of [5, 5, 5, 5] = {result}")
result = calculate_average([])
assert result == 0.0, f"Expected 0.0 for empty list, got {result}"
print(f"Test 3 passed: average of [] = {result}")
result = calculate_average([100])
assert result == 100.0, f"Expected 100.0, got {result}"
print(f"Test 4 passed: average of [100] = {result}")Output:
Test 1 passed: average of [10, 20, 30] = 20.0
Test 2 passed: average of [5, 5, 5, 5] = 5.0
Test 3 passed: average of [] = 0.0
Test 4 passed: average of [100] = 100.0Jika ada assertion yang gagal, Anda langsung tahu test case mana yang mengungkap masalahnya.
41.5.3) Menguji Edge Case
Edge case adalah input pada batas-batas dari apa yang seharusnya bisa ditangani fungsi Anda. Menguji ini mengungkap bug yang mungkin luput dari input normal:
def get_first_and_last(items):
"""Mengembalikan item pertama dan terakhir dari sebuah sequence."""
assert len(items) > 0, "Cannot get first and last from empty sequence"
return items[0], items[-1]
# Uji kasus normal
result = get_first_and_last([1, 2, 3, 4, 5])
assert result == (1, 5), f"Expected (1, 5), got {result}"
print(f"Normal case: {result}")
# Uji edge case: satu item
result = get_first_and_last([42])
assert result == (42, 42), f"Expected (42, 42), got {result}"
print(f"Single item: {result}")
# Uji edge case: dua item
result = get_first_and_last([10, 20])
assert result == (10, 20), f"Expected (10, 20), got {result}"
print(f"Two items: {result}")
# Uji edge case: sequence kosong (seharusnya gagal)
try:
result = get_first_and_last([])
print("ERROR: Should have raised AssertionError for empty list")
except AssertionError as e:
print(f"Empty list correctly rejected: {e}")Output:
Normal case: (1, 5)
Single item: (42, 42)
Two items: (10, 20)
Empty list correctly rejected: Cannot get first and last from empty sequence41.5.4) Menguji Transformasi Data
Ketika fungsi Anda mentransformasikan data, pastikan transformasinya benar:
def remove_duplicates(items):
"""Menghapus duplikat sambil mempertahankan urutan."""
seen = set()
result = []
for item in items:
if item not in seen:
seen.add(item)
result.append(item)
return result
# Uji penghapusan duplikat dasar
input_data = [1, 2, 2, 3, 1, 4, 3, 5]
result = remove_duplicates(input_data)
expected = [1, 2, 3, 4, 5]
assert result == expected, f"Expected {expected}, got {result}"
print(f"Test 1 passed: {input_data} -> {result}")
# Uji bahwa urutan dipertahankan
input_data = [3, 1, 2, 1, 3, 2]
result = remove_duplicates(input_data)
expected = [3, 1, 2]
assert result == expected, f"Expected {expected}, got {result}"
print(f"Test 2 passed: {input_data} -> {result}")
# Uji tanpa duplikat
input_data = [1, 2, 3, 4, 5]
result = remove_duplicates(input_data)
assert result == input_data, f"Expected {input_data}, got {result}"
print(f"Test 3 passed: {input_data} -> {result}")
# Uji semua duplikat
input_data = [5, 5, 5, 5]
result = remove_duplicates(input_data)
expected = [5]
assert result == expected, f"Expected {expected}, got {result}"
print(f"Test 4 passed: {input_data} -> {result}")Output:
Test 1 passed: [1, 2, 2, 3, 1, 4, 3, 5] -> [1, 2, 3, 4, 5]
Test 2 passed: [3, 1, 2, 1, 3, 2] -> [3, 1, 2]
Test 3 passed: [1, 2, 3, 4, 5] -> [1, 2, 3, 4, 5]
Test 4 passed: [5, 5, 5, 5] -> [5]41.5.5) Membuat Fungsi Pengujian Sederhana
Seiring kode Anda bertambah besar, menyebarkan pernyataan assert di seluruh kode utama menjadi berantakan dan sulit dikelola. Pendekatan yang lebih baik adalah mengorganisasi pengujian Anda ke dalam fungsi pengujian khusus. Ini memisahkan kode pengujian dari kode produksi dan membuat Anda mudah menjalankan semua pengujian sekaligus.
Kenapa memakai fungsi pengujian khusus?
- Organisasi: Semua test untuk sebuah fungsi ada di satu tempat
- Dapat digunakan ulang: Jalankan test kapan pun Anda mengubah kode
- Dokumentasi: Test menunjukkan bagaimana fungsi seharusnya berperilaku
- Debugging: Saat test gagal, Anda langsung tahu skenario mana yang rusak
- Alur kerja development: Test dulu, lalu implementasikan atau perbaiki kodenya
Mari lihat praktiknya:
def calculate_grade(score):
"""Mengonversi nilai numerik menjadi nilai huruf."""
if score >= 90:
return 'A'
elif score >= 80:
return 'B'
elif score >= 70:
return 'C'
elif score >= 60:
return 'D'
else:
return 'F'
def test_calculate_grade():
"""Menguji fungsi calculate_grade.
Fungsi ini menguji semua perilaku yang diharapkan:
- Setiap rentang nilai (A, B, C, D, F)
- Nilai batas (90, 80, 70, 60)
- Edge case (sedikit di bawah setiap batas)
"""
print("Testing calculate_grade...")
# Uji nilai A
assert calculate_grade(95) == 'A', "95 should be A"
assert calculate_grade(90) == 'A', "90 should be A (boundary)"
print(" ✓ A grades: passed")
# Uji nilai B
assert calculate_grade(85) == 'B', "85 should be B"
assert calculate_grade(80) == 'B', "80 should be B (boundary)"
print(" ✓ B grades: passed")
# Uji nilai C
assert calculate_grade(75) == 'C', "75 should be C"
assert calculate_grade(70) == 'C', "70 should be C (boundary)"
print(" ✓ C grades: passed")
# Uji nilai D
assert calculate_grade(65) == 'D', "65 should be D"
assert calculate_grade(60) == 'D', "60 should be D (boundary)"
print(" ✓ D grades: passed")
# Uji nilai F
assert calculate_grade(55) == 'F', "55 should be F"
assert calculate_grade(0) == 'F', "0 should be F"
print(" ✓ F grades: passed")
# Uji edge case batas (satu di bawah tiap ambang)
assert calculate_grade(89) == 'B', "89 should be B (just below A)"
assert calculate_grade(79) == 'C', "79 should be C (just below B)"
assert calculate_grade(69) == 'D', "69 should be D (just below C)"
assert calculate_grade(59) == 'F', "59 should be F (just below D)"
print(" ✓ Boundary cases: passed")
print("All tests passed! ✓\n")
# Run the tests
test_calculate_grade()
# Sekarang Anda bisa memakai fungsinya dengan percaya diri
student_score = 87
grade = calculate_grade(student_score)
print(f"Student score {student_score} = Grade {grade}")Output:
Testing calculate_grade...
✓ A grades: passed
✓ B grades: passed
✓ C grades: passed
✓ D grades: passed
✓ F grades: passed
✓ Boundary cases: passed
All tests passed! ✓
Student score 87 = Grade BManfaat pendekatan ini:
- Organisasi test yang jelas: Anda bisa melihat semua test case sekilas
- Mudah dijalankan: Cukup panggil
test_calculate_grade()kapan pun Anda mengubah fungsi - Umpan balik progresif: Anda bisa melihat grup test mana yang lulus saat fungsi berjalan
- Self-documenting: Fungsi test menunjukkan persis bagaimana
calculate_grade()seharusnya bekerja
Kapan menjalankan test Anda:
- Sebelum membuat perubahan: Pastikan test Anda lulus pada kode saat ini
- Setelah membuat perubahan: Verifikasi Anda tidak merusak apa pun
- Saat menambahkan fitur: Tulis test untuk fitur baru terlebih dahulu (test-driven development)
- Saat memperbaiki bug: Tambahkan test yang mereproduksi bug, lalu perbaiki
Pola sederhana ini—menulis fungsi pengujian dengan assertion—adalah fondasi dari pengujian software profesional. Seiring Anda maju, Anda akan belajar tentang framework pengujian seperti pytest dan unittest, tetapi ide utamanya tetap sama: tulis fungsi yang memverifikasi kode Anda bekerja dengan benar.
41.5.6) Kapan Menggunakan Assertions vs Exceptions
Memahami kapan harus menggunakan assertion versus exception itu krusial. Keduanya punya tujuan yang fundamentally berbeda:
Assertion digunakan untuk menemukan bug selama development:
- Mereka mengecek hal-hal yang seharusnya tidak pernah false jika kode Anda ditulis dengan benar
- Mereka memverifikasi asumsi internal dan logika dari kode Anda sendiri
- Mereka membantu Anda menangkap kesalahan pemrograman saat Anda menulis dan menguji kode
- Contoh: "Pada titik ini dalam fungsi saya, list ini seharusnya tidak pernah kosong"
- Contoh: "Semua item di list ini seharusnya integer karena saya baru saja memfilternya"
Exception digunakan untuk menangani error yang bisa terjadi saat operasi normal:
- Mereka menangani kondisi eksternal yang tidak bisa Anda kontrol
- Mereka menangani situasi yang mungkin terjadi bahkan ketika kode Anda sempurna
- Mereka memungkinkan program Anda pulih dengan elegan atau gagal dengan informasi yang jelas
- Contoh: Pengguna memasukkan teks padahal Anda mengharapkan angka
- Contoh: File yang kode Anda coba buka tidak ada
- Contoh: Network request timeout
Perbedaan kuncinya: Assertion berkata "ini seharusnya mustahil," sementara exception berkata "ini mungkin terjadi, dan begini cara kita menanganinya."
Mari lihat praktiknya:
# Contoh 1: Fungsi digunakan dengan INPUT PENGGUNA
# Pengguna bisa memasukkan apa saja, termasuk 0
def calculate_user_ratio(numerator, denominator):
"""Menghitung rasio dari angka yang diberikan pengguna."""
# Pengguna bisa memasukkan 0, jadi gunakan penanganan exception
if denominator == 0:
raise ValueError("Denominator cannot be zero")
return numerator / denominator
# Contoh 2: Perhitungan internal di mana 0 seharusnya mustahil
def calculate_percentage(part, total):
"""Menghitung berapa persen 'part' dari 'total'."""
# Ini dipanggil secara internal setelah kita memverifikasi total > 0
# Jika total adalah 0, itu bug pemrograman di kode kita
assert total > 0, "total must be positive - check calling code"
return (part / total) * 100Contoh lain tentang apa yang seharusnya ditangani masing-masing:
| Situation | Use Assertion | Use Exception |
|---|---|---|
| User enters invalid input | ❌ No | ✅ Yes |
| File doesn't exist | ❌ No | ✅ Yes |
| Network request fails | ❌ No | ✅ Yes |
| Function gets wrong parameter type from your code | ✅ Yes | ❌ No |
| List should have items but is empty due to logic error | ✅ Yes | ❌ No |
| Data structure in unexpected state due to bug | ✅ Yes | ❌ No |
| Database connection fails | ❌ No | ✅ Yes |
| API returns unexpected format | ❌ No | ✅ Yes |
| Your algorithm produces mathematically impossible result | ✅ Yes | ❌ No |
Keterbatasan kritis assertion:
Assertion bisa dinonaktifkan sepenuhnya saat Python berjalan dengan optimisasi:
python -O script.py # All assert statements are ignored!Saat assertion dinonaktifkan, mereka benar-benar hilang—Python tidak mengeceknya sama sekali. Ini berarti:
- ❌ Jangan pernah gunakan assertion untuk memvalidasi input pengguna
- ❌ Jangan pernah gunakan assertion untuk pemeriksaan keamanan
- ❌ Jangan pernah gunakan assertion untuk apa pun yang harus selalu bekerja di produksi
# BERBAHAYA - JANGAN LAKUKAN INI:
def process_payment(amount):
assert amount > 0, "Amount must be positive" # SALAH! Dinonaktifkan dengan -O
# Process payment...
# BENAR - LAKUKAN INI:
def process_payment(amount):
if amount <= 0:
raise ValueError("Amount must be positive") # Selalu dicek!
# Process payment...Ringkasnya:
-
Assertion = "Saya mengecek kode saya sendiri untuk bug selama development"
- Pikirkan: "Ini seharusnya mustahil jika saya ngoding dengan benar"
- Mereka membantu Anda menemukan kesalahan dalam logika
-
Exception = "Saya menangani kondisi dunia nyata yang memang bisa terjadi"
- Pikirkan: "Ini bisa terjadi saat penggunaan normal, dan saya perlu menanganinya"
- Mereka membantu program Anda menangani situasi yang tidak terduga
Assertion adalah alat development dan debugging yang membantu Anda menulis kode yang benar. Exception adalah alat produksi yang membantu program Anda menangani realitas yang berantakan dari input pengguna, file system, network, dan faktor eksternal lainnya yang tidak bisa Anda kontrol.
Anda sekarang sudah mempelajari teknik debugging dan pengujian penting yang akan berguna sepanjang perjalanan pemrograman Anda:
- Membaca tracebacks untuk cepat menemukan di mana error terjadi
- Menelusuri kode secara mental untuk memahami apa yang dilakukan kode Anda langkah demi langkah
- Menggunakan print statement secara strategis untuk melihat nilai dan alur saat runtime
- Menginspeksi objek dengan
type()dandir()untuk memahami apa yang sedang Anda gunakan - Menguji dengan assertions untuk memverifikasi kode Anda bekerja dan menangkap bug lebih awal
Keterampilan ini bekerja bersama sebagai toolkit debugging yang lengkap. Saat Anda menemui masalah:
- Baca traceback untuk menemukan di mana ia gagal
- Gunakan print debugging atau penelusuran mental untuk memahami kenapa
- Gunakan inspeksi type/dir ketika Anda tidak yakin apa yang bisa dilakukan sebuah objek
- Tulis assertion untuk mencegah bug itu kembali
Dengan latihan, Anda akan mengembangkan intuisi tentang teknik mana yang dipakai di setiap situasi. Ingat: setiap programmer melakukan debugging—bedanya adalah programmer berpengalaman melakukannya secara sistematis dan efisien. Teknik-teknik ini akan membuat Anda jadi salah satunya.