Kalkulator kita kini ramah terhadap pengguna, tetapi kodenya belum ramah terhadap pengujian: operasi aritmatika hidup di dalam function tambah() sampai bagi() yang terikat dictionary OPERASI, dan bagi() masih melakukan dua pekerjaan sekaligus — menghitung sekaligus mencetak pesan galat lalu mengembalikan None. Pada bagian penutup ini kita melakukan refactor: seluruh logika hitung dipadatkan menjadi satu fungsi murni hitung(), error dilaporkan lewat exception alih-alih nilai None, dan kebenaran logika dibuktikan dengan rangkaian test pytest.
Memisahkan Logika dari Input/Output #
Fungsi murni — materi Chapter Pure Functions — hanya menghitung hasil dari argument yang diterima, tanpa membaca input() dan tanpa mencetak apa pun. Fungsi semacam ini gampang diuji: panggil dengan nilai yang diketahui, bandingkan hasilnya. Rancangan fungsinya:
def hitung(a: float, op: str, b: float) -> float:Function hitung() menerima dua angka dan satu simbol operator, lalu mengembalikan hasilnya. Dua aturan galat melekat pada rancangan ini:
opyang bukan salah satu dari+ - * /membangkitkanValueError,- pembagian dengan nol membangkitkan
ZeroDivisionError.
Menariknya, untuk aturan kedua kita tidak perlu menulis pemeriksaan apa pun: ekspresi a / b dengan b bernilai 0 sudah otomatis membangkitkan ZeroDivisionError di dalam interpreter Python. Mengapa memilih exception, bukan lagi None? Karena keputusan atas galat kini berpindah ke pemanggil: main() menangkap exception itu dan mencetak pesan ramah untuk pengguna, sementara test menangkap exception yang sama untuk membuktikan perilakunya. Satu sumber kebenaran, dua cara merespons.
Kode Lengkap Bagian 4 #
"""Kalkulator Terminal - penanganan error dan testing.
Bagian 4: logika hitung dipisahkan sebagai fungsi murni,
error ditangani pada satu lapisan, dan seluruh logika diuji dengan pytest.
"""
def hitung(a: float, op: str, b: float) -> float:
"""Kembalikan hasil a <op> b untuk op salah satu dari + - * /.
Membangkitkan ZeroDivisionError bila op adalah "/" dan b bernilai 0,
serta ValueError bila op tidak dikenal.
"""
if op == "+":
return a + b
if op == "-":
return a - b
if op == "*":
return a * b
if op == "/":
return a / b
raise ValueError(f"operator tidak dikenal: {op!r}")
def tampilkan_menu():
"""Cetak daftar operasi yang tersedia."""
print("=== Kalkulator Terminal ===")
print("1. Tambah")
print("2. Kurang")
print("3. Kali")
print("4. Bagi")
print("5. Keluar")
PILIHAN_KE_OPERATOR = {
"1": "+",
"2": "-",
"3": "*",
"4": "/",
}
def baca_pilihan():
"""Baca nomor operasi; ulangi sampai pengguna memasukkan 1-5."""
while True:
pilihan = input("Pilih operasi (1-5): ").strip()
if pilihan in ("1", "2", "3", "4", "5"):
return pilihan
print(f"Pilihan '{pilihan}' tidak dikenal. Masukkan angka 1-5.")
def baca_angka(label):
"""Baca satu angka; ulangi sampai input dapat dikonversi menjadi float."""
while True:
teks = input(f"{label}: ").strip()
try:
return float(teks)
except ValueError:
print(f"'{teks}' bukan angka yang valid. Coba lagi.")
def tanya_lagi():
"""Tanyakan apakah pengguna ingin menghitung lagi; terima hanya y atau t."""
while True:
jawaban = input("Hitung lagi? (y/t): ").strip().lower()
if jawaban in ("y", "t"):
return jawaban == "y"
print("Jawaban harus 'y' atau 't'.")
def main():
"""Titik masuk program: koordinasi input, hitung, dan output."""
while True:
tampilkan_menu()
pilihan = baca_pilihan()
if pilihan == "5":
break
op = PILIHAN_KE_OPERATOR[pilihan]
a = baca_angka("Angka pertama")
b = baca_angka("Angka kedua")
try:
hasil = hitung(a, op, b)
except ZeroDivisionError:
print("Error: pembagian dengan nol tidak diperbolehkan.")
else:
print(f"Hasil: {a:g} {op} {b:g} = {hasil:g}")
if not tanya_lagi():
break
print("Terima kasih telah menggunakan Kalkulator Terminal.")
if __name__ == "__main__":
main()Empat function tambah() sampai bagi() beserta dictionary OPERASI lenyap, digantikan rancangan baru. Mari kita bedah perubahannya:
- Baris 8-22 (
def hitung(...)): type hints pada signature mengikuti konvensi Chapter Type Hints pada Fungsi. Rantaiifmemeriksa satu per satu operator yang sah; cabang/langsung menjalankana / btanpa pemeriksaan nol —ZeroDivisionErrordibangkitkan interpreter Python sendiri ketikabbernilai0. - Baris 22 (
raise ValueError(...)): operator yang tidak dikenal adalah kesalahan pemrograman, jadi dilaporkan denganraise— materi Chapter raise. Format{op!r}menampilkan nilai dengan tanda kutipnya, sehingga pesan galat terbacaoperator tidak dikenal: '%'. - Baris 35-40 (
PILIHAN_KE_OPERATOR): dictionary menyusut menjadi pemetaan nomor menu ke simbol operator; logika hitung sudah tidak lagi tinggal di sini. - Baris 71-94 (
def main():): fungsi koordinator kini bernamamain()— konvensi penamaan titik masuk program Python. Bloktry/except/elsedi baris 84-89 mengikuti pola Chapter else dan finally: hasil hanya dicetak di blokelseyang berjalan ketika tidak ada exception. Seluruh penanganan error pengguna kini hidup di satu lapisan ini;hitung()tetap bersih. - Baris 97-98 (
if __name__ == "__main__":): baris ini menjadi syarat testing — tanpa penjaga ini, mengimpormainakan menjalankan loop interaktif.
Test dengan pytest #
Buat berkas test_main.py di folder yang sama dengan main.py:
"""Test untuk fungsi murni hitung() pada Kalkulator Terminal."""
import pytest
from main import hitung
def test_tambah():
assert hitung(10, "+", 5) == 15
def test_kurang():
assert hitung(10, "-", 4) == 6
def test_kali():
assert hitung(6, "*", 7) == 42
def test_bagi():
assert hitung(10, "/", 4) == 2.5
def test_bagi_dengan_nol():
with pytest.raises(ZeroDivisionError):
hitung(5, "/", 0)
def test_operator_tidak_dikenal():
with pytest.raises(ValueError):
hitung(5, "%", 2)
def test_bilangan_negatif():
assert hitung(-4, "+", 9) == 5
assert hitung(-3, "*", -2) == 6
def test_bilangan_desimal():
assert hitung(2.5, "*", 4) == 10
assert hitung(7.5, "-", 0.5) == 7Struktur test ini mengikuti Chapter Menulis Test dengan pytest: setiap function berawalan test_ menjadi satu test mandiri, dan statement assert membandingkan hasil nyata dengan hasil yang diharapkan. Dua test terakhir memakai dua assertion sekaligus untuk memeriksa satu perilaku yang sama — bilangan negatif dan bilangan desimal — bukan dua perilaku berbeda.
Dua test pertengahan berkas memakai pytest.raises dari Chapter Testing Exceptions:
- Baris 24-26 (
test_bagi_dengan_nol): context managerpytest.raises(ZeroDivisionError)menggagalkan test justru ketika exception yang diharapkan tidak muncul. Memanggilhitung(5, "/", 0)di dalam blokwithberarti menuntutZeroDivisionErrorterjadi. - Baris 29-31 (
test_operator_tidak_dikenal): pola yang sama untukValueError— operator%harus ditolakhitung().
Jalankan test dari folder yang berisi kedua berkas:
python3 -m pytest test_main.py -vOutput yang Dihasilkan:
============================= test session starts ==============================
platform linux -- Python 3.12.15, pytest-9.0.2, pluggy-1.6.0
rootdir: /tmp/kalkulator-terminal
collected 8 items
test_main.py::test_tambah PASSED [ 12%]
test_main.py::test_kurang PASSED [ 25%]
test_main.py::test_kali PASSED [ 37%]
test_main.py::test_bagi PASSED [ 50%]
test_main.py::test_bagi_dengan_nol PASSED [ 62%]
test_main.py::test_operator_tidak_dikenal PASSED [ 75%]
test_main.py::test_bilangan_negatif PASSED [ 87%]
test_main.py::test_bilangan_desimal PASSED [100%]
============================== 8 passed in 0.01s ===============================Baris platform dan rootdir mengikuti mesin serta lokasi folder proyek masing-masing. Delapan test lulus dalam sepersekian detik — perbandingan hitung(10, "+", 5) == 15 bernilai True karena Python menganggap 15.0 sama dengan 15.
Sebagai bukti terakhir bahwa logika kini dapat dipakai tanpa menjalankan program interaktif, impor langsung dari interpreter:
python3 -c "from main import hitung; print(hitung(8, '*', 3))"Output yang Dihasilkan:
24Mencoba Program Akhir #
Sesi lengkap program akhir — pembagian lalu penjumlahan:
printf '4\n10\n4\ny\n1\n2.5\n1.5\nt\n' | python3 main.pyOutput yang Dihasilkan:
=== Kalkulator Terminal ===
1. Tambah
2. Kurang
3. Kali
4. Bagi
5. Keluar
Pilih operasi (1-5): Angka pertama: Angka kedua: Hasil: 10 / 4 = 2.5
Hitung lagi? (y/t): === Kalkulator Terminal ===
1. Tambah
2. Kurang
3. Kali
4. Bagi
5. Keluar
Pilih operasi (1-5): Angka pertama: Angka kedua: Hasil: 2.5 + 1.5 = 4
Hitung lagi? (y/t): Terima kasih telah menggunakan Kalkulator Terminal.Pembagian dengan nol kini ditangani lapisan main(), bukan lagi oleh function hitung:
printf '4\n10\n0\nt\n' | python3 main.pyOutput yang Dihasilkan:
=== Kalkulator Terminal ===
1. Tambah
2. Kurang
3. Kali
4. Bagi
5. Keluar
Pilih operasi (1-5): Angka pertama: Angka kedua: Error: pembagian dengan nol tidak diperbolehkan.
Hitung lagi? (y/t): Terima kasih telah menggunakan Kalkulator Terminal.Ringkasan dan Penutup Proyek #
Empat bagian proyek kini utuh: kerangka menu dan struktur function dari Bagian 1, dispatch dictionary dan operasi aritmatika dari Bagian 2, validasi input dengan coba-ulang dari Bagian 3, serta fungsi murni hitung(), penanganan error terpusat, dan delapan test pytest dari bagian ini. Program yang dihasilnya kecil, tetapi arsitekturnya mengikuti pola proyek Python sungguhan: logika terpisah dari I/O, error dilaporkan lewat exception, dan perubahan apa pun tinggal dijaga dengan menjalankan python3 -m pytest.
Sebagai latihan lanjutan, coba tambahkan operasi pangkat (**) atau akar, daftar riwayat perhitungan yang disimpan ke list, atau parsing ekspresi lengkap seperti 2 + 3 * 4 — kandidat alami berikutnya adalah proyek dengan argparse pada Section CLI dan Automation. Berkas lengkap bagian ini dapat diunduh di bagian bawah halaman.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
main.py
Unduh berkas"""Kalkulator Terminal - penanganan error dan testing.
Bagian 4: logika hitung dipisahkan sebagai fungsi murni,
error ditangani pada satu lapisan, dan seluruh logika diuji dengan pytest.
"""
def hitung(a: float, op: str, b: float) -> float:
"""Kembalikan hasil a <op> b untuk op salah satu dari + - * /.
Membangkitkan ZeroDivisionError bila op adalah "/" dan b bernilai 0,
serta ValueError bila op tidak dikenal.
"""
if op == "+":
return a + b
if op == "-":
return a - b
if op == "*":
return a * b
if op == "/":
return a / b
raise ValueError(f"operator tidak dikenal: {op!r}")
def tampilkan_menu():
"""Cetak daftar operasi yang tersedia."""
print("=== Kalkulator Terminal ===")
print("1. Tambah")
print("2. Kurang")
print("3. Kali")
print("4. Bagi")
print("5. Keluar")
PILIHAN_KE_OPERATOR = {
"1": "+",
"2": "-",
"3": "*",
"4": "/",
}
def baca_pilihan():
"""Baca nomor operasi; ulangi sampai pengguna memasukkan 1-5."""
while True:
pilihan = input("Pilih operasi (1-5): ").strip()
if pilihan in ("1", "2", "3", "4", "5"):
return pilihan
print(f"Pilihan '{pilihan}' tidak dikenal. Masukkan angka 1-5.")
def baca_angka(label):
"""Baca satu angka; ulangi sampai input dapat dikonversi menjadi float."""
while True:
teks = input(f"{label}: ").strip()
try:
return float(teks)
except ValueError:
print(f"'{teks}' bukan angka yang valid. Coba lagi.")
def tanya_lagi():
"""Tanyakan apakah pengguna ingin menghitung lagi; terima hanya y atau t."""
while True:
jawaban = input("Hitung lagi? (y/t): ").strip().lower()
if jawaban in ("y", "t"):
return jawaban == "y"
print("Jawaban harus 'y' atau 't'.")
def main():
"""Titik masuk program: koordinasi input, hitung, dan output."""
while True:
tampilkan_menu()
pilihan = baca_pilihan()
if pilihan == "5":
break
op = PILIHAN_KE_OPERATOR[pilihan]
a = baca_angka("Angka pertama")
b = baca_angka("Angka kedua")
try:
hasil = hitung(a, op, b)
except ZeroDivisionError:
print("Error: pembagian dengan nol tidak diperbolehkan.")
else:
print(f"Hasil: {a:g} {op} {b:g} = {hasil:g}")
if not tanya_lagi():
break
print("Terima kasih telah menggunakan Kalkulator Terminal.")
if __name__ == "__main__":
main()
test_main.py
Unduh berkas"""Test untuk fungsi murni hitung() pada Kalkulator Terminal."""
import pytest
from main import hitung
def test_tambah():
assert hitung(10, "+", 5) == 15
def test_kurang():
assert hitung(10, "-", 4) == 6
def test_kali():
assert hitung(6, "*", 7) == 42
def test_bagi():
assert hitung(10, "/", 4) == 2.5
def test_bagi_dengan_nol():
with pytest.raises(ZeroDivisionError):
hitung(5, "/", 0)
def test_operator_tidak_dikenal():
with pytest.raises(ValueError):
hitung(5, "%", 2)
def test_bilangan_negatif():
assert hitung(-4, "+", 9) == 5
assert hitung(-3, "*", -2) == 6
def test_bilangan_desimal():
assert hitung(2.5, "*", 4) == 10
assert hitung(7.5, "-", 0.5) == 7