Lampiran D — Kunci Jawaban Latihan Terpilih

Tidak ada yang lebih baik dari mencoba sendiri dulu. Lampiran ini berisi 40 latihan terpilih dari 19 bab (terutama Bab 5 sampai 25, ditambah beberapa dari Bab 26 sampai 34 yang jawabannya bisa dijalankan cepat) beserta jawabannya. Tiap soal dikutip persis dari bab asalnya, dan tiap jawaban berupa skrip yang benar-benar dijalankan; keluaran di bawahnya adalah keluaran asli.

PeringatanAWAS

Yang Anda baca di sini adalah salah satu jawaban yang benar, bukan satu-satunya. Jawaban Anda boleh berbeda (nama variabel, urutan, atau cara menghitung) asalkan hasilnya sama. Latihan yang meminta “ubah skrip bab …” tidak dipilih di sini, karena jawabannya bergantung pada skrip itu; yang dipilih adalah soal yang bisa dikerjakan berdiri sendiri.

Semua skrip lampiran ini ada di folder kode/bab74/. Di versi web, Anda juga bisa mengunduh semuanya sekaligus (zip). Soal yang memakai input() dijawab dengan ketikan yang disiapkan dalam berkas .input di samping skripnya, sehingga keluarannya tampak seperti Anda sendiri yang mengetik.

CatatanTIPS

Cara memakai lampiran: (1) kerjakan soal sendiri; (2) jalankan hasil Anda; (3) baru bandingkan dengan jawaban di sini. Bila hasilnya sama tetapi caranya berbeda, itu bagus. Bila berbeda, baca penjelasannya sebelum melihat kodenya.

Daftar jawaban

Bab Latihan terjawab Skrip
5 Latihan 1, 2, 3 01_bab05.py
6 Latihan 1, 2, 4 02_bab06.py
7 Latihan 1, 3 03_bab07.py
8 Latihan 1, 2, 4 04_bab08.py
9 Latihan 1, 2, 3 05_bab09.py
10 Latihan 1, 2, 4 06_bab10.py
11 Latihan 1, 2 07_bab11.py
12 Latihan 1, 4 08_bab12.py
13 Latihan 1, 2, 4 09_bab13.py
14 Latihan 1, 2 10_bab14.py
16 Latihan 2 11_bab16.py
22 Latihan 1, 2 12_bab22.py
23 Latihan 1, 2 13_bab23.py
26 Latihan 4 14_bab26.py
30 Latihan 1 15_bab30.py
31 Latihan 1, 2, 4 16_bab31.py
32 Latihan 1, 4 17_bab32.py
33 dan 34 Latihan 2 / 1 18_bab33_34.py

Jumlah seluruhnya 40 latihan dari 19 bab. Latihan non-kode (misalnya “berapa baris dan mengapa”) dijawab dengan penalaran yang dibuktikan keluaran skrip.

Bab 5 — Variabel dan Tipe Data

Latihan 1. Buat variabel untuk satu petak sawah: nama varietas padi (teks), luas dalam hektare (desimal), umur tanaman dalam hari (bulat), dan apakah sudah dipupuk (benar/salah). Cetak keempatnya dengan f-string.

Latihan 2. Minta pengguna mengetik jumlah telur yang dikumpulkan hari ini, lalu cetak jumlah untuk seminggu (dikali 7). Jangan lupa jebakan input()!

Latihan 3. Tantangan: hitung luas lingkaran tajuk pohon berdiameter 6,4 meter (rumus: 3,14 × r × r), dan tampilkan dengan dua angka di belakang koma.

01_bab05.py
# Bab 74 - Kunci jawaban: latihan Bab 5 (variabel dan tipe data)

print("--- Latihan 1 ---")
varietas = "Ciherang"        # teks (str)
luas_ha = 0.75               # desimal (float)
umur_hari = 62               # bulat (int)
sudah_dipupuk = True         # benar/salah (bool)
print(f"Varietas {varietas}, luas {luas_ha} ha, umur {umur_hari} hari, dipupuk: {sudah_dipupuk}")

print("--- Latihan 2 ---")
telur_hari_ini = int(input("Telur hari ini: "))    # input() selalu teks, jadi diubah ke int
print(f"Telur seminggu: {telur_hari_ini * 7}")

print("--- Latihan 3 (tantangan) ---")
diameter_m = 6.4
jari_jari = diameter_m / 2                          # r = d / 2 = 3.2
luas_tajuk = 3.14 * jari_jari * jari_jari
print(f"Luas tajuk: {luas_tajuk:.2f} m2")
--- Latihan 1 ---
Varietas Ciherang, luas 0.75 ha, umur 62 hari, dipupuk: True
--- Latihan 2 ---
Telur hari ini: 43
Telur seminggu: 301
--- Latihan 3 (tantangan) ---
Luas tajuk: 32.15 m2

Penjelasan.

  • Latihan 1: Empat nilai, empat tipe: str, float, int, bool. f-string menempelkan semuanya ke dalam satu kalimat; True tercetak persis dengan T besar.
  • Latihan 2: input() selalu memberi teks. Setelah dibungkus int(...), 43 menjadi angka dan 43 x 7 = 301. (Tanpa int, "43" * 7 hanya mengulang teks tujuh kali, bukan menghitung.)
  • Latihan 3: Diameter 6,4 m berarti jari-jari 3,2 m. 3,14 x 3,2 x 3,2 = 32,1536, dan :.2f membulatkan tampilannya menjadi 32,15 (nilai di memori tidak berubah).

Bab 6 — Operator dan Input/Output

Latihan 1. Seorang petani memanen 3 blok kebun: 245,5 kg, 310 kg, dan 198,25 kg. Hitung total panen dan rata-rata per blok, lalu cetak dengan f-string.

Latihan 2. Sebuah kandang berisi 1.000 butir telur akan dikemas dalam rak berisi 30 butir. Dengan // dan %, hitung berapa rak penuh dan berapa butir telur sisa.

Latihan 4. Tantangan: tulis satu ekspresi yang bernilai True bila sebuah pohon layak ditebang: diameter minimal 40 cm dan sehat, atau tinggi lebih dari 25 m. Uji dengan beberapa angka. Hati-hati dengan urutan and dan or; pakai tanda kurung.

02_bab06.py
# Bab 74 - Kunci jawaban: latihan Bab 6 (operator)

print("--- Latihan 1 ---")
blok = [245.5, 310, 198.25]
total = sum(blok)
rata = total / len(blok)
print(f"Total panen: {total} kg, rata-rata per blok: {rata} kg")

print("--- Latihan 2 ---")
telur, isi_rak = 1000, 30
print(f"Rak penuh: {telur // isi_rak}, telur sisa: {telur % isi_rak}")

print("--- Latihan 4 (tantangan) ---")
def layak_tebang(diameter_cm, sehat, tinggi_m):
    return (diameter_cm >= 40 and sehat) or tinggi_m > 25

uji = [(45, True, 20), (45, False, 20), (35, True, 20), (35, False, 27), (45, False, 27)]
for diameter, sehat, tinggi in uji:
    print(diameter, sehat, tinggi, "->", layak_tebang(diameter, sehat, tinggi))
--- Latihan 1 ---
Total panen: 753.75 kg, rata-rata per blok: 251.25 kg
--- Latihan 2 ---
Rak penuh: 33, telur sisa: 10
--- Latihan 4 (tantangan) ---
45 True 20 -> True
45 False 20 -> False
35 True 20 -> False
35 False 27 -> True
45 False 27 -> True

Penjelasan.

  • Latihan 1: Total 245,5 + 310 + 198,25 = 753,75 kg; rata-rata 753,75 / 3 = 251,25 kg. Perhatikan sum() menerima campuran int dan float.
  • Latihan 2: 1000 // 30 memberi 33 rak penuh (33 x 30 = 990 butir) dan 1000 % 30 memberi sisa 10 butir.
  • Latihan 4: Ekspresinya (diameter >= 40 and sehat) or tinggi > 25. Python mengerjakan and lebih dulu daripada or, tetapi kurung membuat maksudnya terbaca. Kelima uji cocok dengan penalaran: diameter 45 yang sehat lolos; diameter 45 yang tidak sehat dan pendek tidak lolos; diameter 35 yang pendek tidak lolos; dan pohon setinggi 27 m lolos walau tidak sehat, karena syarat tinggi > 25 berdiri sendiri.

Bab 7 — Kondisional

Latihan 1. Tulis program yang menanyakan suhu air kolam (angka desimal) dan mencetak “Terlalu dingin” bila di bawah 25, “Terlalu panas” bila di atas 32, dan “Suhu baik” selainnya. (Batas 25 dan 32 adalah angka latihan.)

Latihan 3. Buat program yang menanyakan umur tanaman dalam hari dan mencetak fase: “Semai” (0–14), “Vegetatif” (15–45), “Berbunga” (46–70), atau “Panen” (di atas 70). Ini angka rekaan untuk latihan.

03_bab07.py
# Bab 74 - Kunci jawaban: latihan Bab 7 (kondisional)

print("--- Latihan 1 ---")
def periksa_suhu():
    suhu = float(input("Suhu air kolam (C): "))
    if suhu < 25:
        print("Terlalu dingin")
    elif suhu > 32:
        print("Terlalu panas")
    else:
        print("Suhu baik")

for _ in range(3):          # tiga kali dijalankan supaya ketiga cabang tampak
    periksa_suhu()

print("--- Latihan 3 ---")
def tentukan_fase():
    umur = int(input("Umur tanaman (hari): "))
    if umur <= 14:
        print("Semai")
    elif umur <= 45:
        print("Vegetatif")
    elif umur <= 70:
        print("Berbunga")
    else:
        print("Panen")

for _ in range(5):          # lima umur di sekitar batas fase: 10, 14, 15, 70, 71
    tentukan_fase()
--- Latihan 1 ---
Suhu air kolam (C): 22.5
Terlalu dingin
Suhu air kolam (C): 28
Suhu baik
Suhu air kolam (C): 34
Terlalu panas
--- Latihan 3 ---
Umur tanaman (hari): 10
Semai
Umur tanaman (hari): 14
Semai
Umur tanaman (hari): 15
Vegetatif
Umur tanaman (hari): 70
Berbunga
Umur tanaman (hari): 71
Panen

Penjelasan.

  • Latihan 1: Tiga pemanggilan menyentuh tiga cabang: 22,5 (di bawah 25) = Terlalu dingin, 28 = Suhu baik, 34 (di atas 32) = Terlalu panas. Dengan syarat suhu < 25 dan suhu > 32, suhu tepat 25 dan tepat 32 jatuh ke “Suhu baik” (ini penalaran dari kodenya; keduanya tidak diuji di sini).
  • Latihan 3: Lima umur di sekitar batas: 10 dan 14 = Semai, 15 = Vegetatif, 70 = Berbunga, 71 = Panen. Urutan elif dari kecil ke besar membuat tiap syarat cukup memeriksa batas atasnya saja. Angka fase ini rekaan untuk latihan.

Bab 8 — Perulangan

Latihan 1. Cetak tabel perkalian 7 (7 × 1 sampai 7 × 10) dengan for dan range().

Latihan 2. Diberikan bobot sapi [350, 412.5, 389.0, 401.5], hitung total dan rata-ratanya dengan pola penampung.

Latihan 4. Tantangan: dari daftar [480, 0, 510, 530, 0, 495], hitung jumlah timbangan yang valid (bukan 0) dan rata-rata bobot yang valid, memakai continue.

04_bab08.py
# Bab 74 - Kunci jawaban: latihan Bab 8 (perulangan)

print("--- Latihan 1 ---")
for i in range(1, 11):
    print(f"7 x {i} = {7 * i}")

print("--- Latihan 2 ---")
bobot_sapi = [350, 412.5, 389.0, 401.5]
total = 0                     # pola penampung: mulai dari nol
for bobot in bobot_sapi:
    total += bobot
print(f"Total {total} kg, rata-rata {total / len(bobot_sapi)} kg")

print("--- Latihan 4 (tantangan) ---")
timbangan = [480, 0, 510, 530, 0, 495]
jumlah_valid = 0
total_valid = 0
for bobot in timbangan:
    if bobot == 0:
        continue              # lewati timbangan yang tidak valid
    jumlah_valid += 1
    total_valid += bobot
print(f"Valid: {jumlah_valid} dari {len(timbangan)}, rata-rata {total_valid / jumlah_valid} g")
--- Latihan 1 ---
7 x 1 = 7
7 x 2 = 14
7 x 3 = 21
7 x 4 = 28
7 x 5 = 35
7 x 6 = 42
7 x 7 = 49
7 x 8 = 56
7 x 9 = 63
7 x 10 = 70
--- Latihan 2 ---
Total 1553.0 kg, rata-rata 388.25 kg
--- Latihan 4 (tantangan) ---
Valid: 4 dari 6, rata-rata 503.75 g

Penjelasan.

  • Latihan 1: range(1, 11) menghasilkan 1 sampai 10 (angka 11 tidak ikut). Tiap putaran mencetak 7 x i = hasil.
  • Latihan 2: Pola penampung: mulai dari 0, tambahkan satu per satu. Total 350 + 412,5 + 389 + 401,5 = 1553,0 kg, rata-rata 1553,0 / 4 = 388,25 kg.
  • Latihan 4: continue melompati sisa isi putaran untuk timbangan 0, jadi hanya 4 dari 6 pembacaan yang dihitung: (480 + 510 + 530 + 495) / 4 = 503,75 g. Kalau nol ikut dihitung, rata-ratanya jatuh menjadi sekitar 335,8 g (2015 / 6), jauh dari kenyataan.

Bab 9 — Struktur Data Bawaan

Latihan 1. Buat list berisi bobot lima ekor sapi (angka bebas). Cetak bobot terberat, bobot teringan, dan rata-ratanya.

Latihan 2. Dari list [480, 500, 510, 520, 530, 540], ambil dengan slicing: tiga terakhir, dan tiga angka pertama dibalik.

Latihan 3. Buat dict untuk satu kolam dengan label nama, jumlah_ikan, bobot_rata_g, dan siap_panen (hasil perbandingan). Cetak semua isinya dengan for dan .items().

05_bab09.py
# Bab 74 - Kunci jawaban: latihan Bab 9 (struktur data)

print("--- Latihan 1 ---")
bobot = [350, 412.5, 389.0, 401.5, 365.0]
print("Terberat:", max(bobot), "| teringan:", min(bobot), "| rata-rata:", sum(bobot) / len(bobot))

print("--- Latihan 2 ---")
angka = [480, 500, 510, 520, 530, 540]
print("Tiga terakhir:", angka[-3:])
print("Tiga pertama dibalik:", angka[:3][::-1])

print("--- Latihan 3 ---")
kolam = {"nama": "A", "jumlah_ikan": 800, "bobot_rata_g": 540.5}
kolam["siap_panen"] = kolam["bobot_rata_g"] >= 500       # hasil perbandingan: True/False
for label, nilai in kolam.items():
    print(f"{label}: {nilai}")
--- Latihan 1 ---
Terberat: 412.5 | teringan: 350 | rata-rata: 383.6
--- Latihan 2 ---
Tiga terakhir: [520, 530, 540]
Tiga pertama dibalik: [510, 500, 480]
--- Latihan 3 ---
nama: A
jumlah_ikan: 800
bobot_rata_g: 540.5
siap_panen: True

Penjelasan.

  • Latihan 1: Lima bobot contoh (350, 412,5, 389, 401,5, 365): terberat 412,5, teringan 350, rata-rata 1918 / 5 = 383,6 kg.
  • Latihan 2: angka[-3:] mengambil tiga dari belakang: [520, 530, 540]. angka[:3][::-1] mengambil tiga pertama [480, 500, 510] lalu membaliknya: [510, 500, 480].
  • Latihan 3: Label siap_panen diisi hasil perbandingan 540.5 >= 500, yaitu True. Perulangan for label, nilai in kolam.items() mencetak setiap pasangan kunci dan nilai.

Bab 10 — Fungsi

Latihan 1. Tulis fungsi luas_lingkaran_m2(jari_jari_m) yang mengembalikan luas (pakai 3.14 untuk π). Panggil untuk jari-jari 3 dan 5.

Latihan 2. Tulis fungsi status_panen(bobot_g) yang mengembalikan "siap" bila bobot ≥ 500 dan "belum" bila tidak. Pakai di dalam perulangan atas list [480, 520, 505, 495].

Latihan 4. Tantangan: tulis fungsi rata_rata_aman(angka) yang mengembalikan None bila daftarnya kosong, sehingga tidak kena ZeroDivisionError. Gunakan if dari Bab 7.

06_bab10.py
# Bab 74 - Kunci jawaban: latihan Bab 10 (fungsi)

print("--- Latihan 1 ---")
def luas_lingkaran_m2(jari_jari_m):
    return round(3.14 * jari_jari_m * jari_jari_m, 2)    # round() menghindari 28.259999999999998

print(luas_lingkaran_m2(3), luas_lingkaran_m2(5))

print("--- Latihan 2 ---")
def status_panen(bobot_g):
    if bobot_g >= 500:
        return "siap"
    return "belum"

for bobot in [480, 520, 505, 495]:
    print(bobot, "->", status_panen(bobot))

print("--- Latihan 4 (tantangan) ---")
def rata_rata_aman(angka):
    if len(angka) == 0:
        return None
    return sum(angka) / len(angka)

print(rata_rata_aman([480, 520]), rata_rata_aman([]))
--- Latihan 1 ---
28.26 78.5
--- Latihan 2 ---
480 -> belum
520 -> siap
505 -> siap
495 -> belum
--- Latihan 4 (tantangan) ---
500.0 None

Penjelasan.

  • Latihan 1: Luas jari-jari 3 = 28,26 dan jari-jari 5 = 78,5. Tanpa round(..., 2), Python mencetak 28.259999999999998 karena bilangan desimal disimpan sebagai pendekatan biner; pembulatan membuat keluarannya rapi.
  • Latihan 2: Fungsi mengembalikan teks, jadi pemanggil bebas memakainya: 480 dan 495 belum siap, 520 dan 505 siap (batasnya >= 500).
  • Latihan 4: Daftar kosong dijaga dengan if len(angka) == 0: return None, jadi tidak ada pembagian dengan nol. Hasil None harus diperiksa pemanggil sebelum dipakai untuk berhitung.

Bab 11 — Modul dan Package

Latihan 1. Pakai datetime: hitung tanggal panen untuk kopi yang ditanam 15 Januari 2026 dan dipanen 270 hari kemudian, lalu cetak dengan format dd-mm-yyyy.

Latihan 2. Dengan random.seed(7), pilih 3 nomor kolam berbeda dari 1 sampai 12 memakai random.sample. Jalankan dua kali dan pastikan hasilnya sama.

07_bab11.py
# Bab 74 - Kunci jawaban: latihan Bab 11 (modul)
import random
from datetime import date, timedelta

print("--- Latihan 1 ---")
tanam = date(2026, 1, 15)
panen = tanam + timedelta(days=270)
print("Tanggal panen kopi:", panen.strftime("%d-%m-%Y"))

print("--- Latihan 2 ---")
for putaran in (1, 2):
    random.seed(7)                                   # benih sama -> urutan acak sama
    print(f"Putaran {putaran}:", random.sample(range(1, 13), 3))
--- Latihan 1 ---
Tanggal panen kopi: 12-10-2026
--- Latihan 2 ---
Putaran 1: [6, 3, 7]
Putaran 2: [6, 3, 7]

Penjelasan.

  • Latihan 1: 15 Januari 2026 ditambah 270 hari jatuh pada 12 Oktober 2026, dicetak 12-10-2026 dengan strftime("%d-%m-%Y").
  • Latihan 2: Setiap random.seed(7) mengembalikan generator acak ke keadaan awal, jadi kedua putaran memberi [6, 3, 7]. Angka persisnya bergantung pada versi Python (di sini 3.14); yang tetap benar adalah kedua putaran sama.

Bab 12 — Penanganan Error

Latihan 1. Tulis fungsi ke_angka(teks) yang mengembalikan float dari teks, atau None bila teksnya bukan angka. Uji dengan "12,5", "abc", dan "".

Latihan 4. Tantangan: buat kelas error PhTidakWajar(DataLapanganError) dan fungsi periksa_kolam(daftar) yang mengumpulkan semua kolam bermasalah (bukan berhenti di yang pertama), lalu mencetak ringkasan.

08_bab12.py
# Bab 74 - Kunci jawaban: latihan Bab 12 (error)

print("--- Latihan 1 ---")
def ke_angka(teks):
    try:
        return float(teks.replace(",", "."))     # koma desimal gaya Indonesia
    except ValueError:
        return None

for teks in ["12,5", "abc", ""]:
    print(repr(teks), "->", ke_angka(teks))

print("--- Latihan 4 (tantangan) ---")
class DataLapanganError(Exception):
    """Induk semua error data lapangan."""

class PhTidakWajar(DataLapanganError):
    pass

def periksa_kolam(daftar):
    masalah = []
    for kolam in daftar:
        try:
            if not 0 <= kolam["ph_air"] <= 14:
                raise PhTidakWajar(f"Kolam {kolam['kolam']}: pH {kolam['ph_air']} mustahil")
            if not 6.5 <= kolam["ph_air"] <= 8.5:
                raise PhTidakWajar(f"Kolam {kolam['kolam']}: pH {kolam['ph_air']} di luar 6,5-8,5")
        except PhTidakWajar as e:
            masalah.append(str(e))                # kumpulkan, jangan berhenti
    return masalah

kolam_uji = [
    {"kolam": "A", "ph_air": 7.2},
    {"kolam": "B", "ph_air": 5.1},
    {"kolam": "C", "ph_air": 19.0},
    {"kolam": "D", "ph_air": 8.9},
]
hasil = periksa_kolam(kolam_uji)
print(f"{len(hasil)} dari {len(kolam_uji)} kolam bermasalah:")
for baris in hasil:
    print(" -", baris)
--- Latihan 1 ---
'12,5' -> 12.5
'abc' -> None
'' -> None
--- Latihan 4 (tantangan) ---
3 dari 4 kolam bermasalah:
 - Kolam B: pH 5.1 di luar 6,5-8,5
 - Kolam C: pH 19.0 mustahil
 - Kolam D: pH 8.9 di luar 6,5-8,5

Penjelasan.

  • Latihan 1: float() menolak koma, jadi fungsi menukar koma menjadi titik lebih dulu: “12,5” menjadi 12.5. “abc” dan teks kosong membuat float() melempar ValueError, yang ditangkap lalu diganti None. Menukar koma adalah keputusan saya agar sesuai tulisan Indonesia; cara lain yang sah adalah menolak koma.
  • Latihan 4: Tantangannya: mengumpulkan semua masalah, bukan berhenti di yang pertama. Kuncinya try/except di dalam perulangan: raise hanya membuat error untuk satu kolam, except PhTidakWajar menangkapnya, lalu pesan masuk ke daftar dan perulangan lanjut. Hasilnya 3 dari 4 kolam bermasalah (B pH 5,1; C pH 19,0 yang mustahil karena skala pH hanya 0 sampai 14; D pH 8,9), sedangkan A (7,2) aman. Batas 6,5 sampai 8,5 adalah angka contoh. Karena PhTidakWajar turunan DataLapanganError, kelak Anda bisa menangkap semua jenis error data lapangan sekaligus lewat induknya.

Bab 13 — File dan I/O Dasar

Latihan 1. Tulis skrip yang menulis angka 1 sampai 10 (satu per baris) ke angka.txt, lalu membacanya kembali dan mencetak jumlahnya.

Latihan 2. Jalankan skrip pada latihan 1 dua kali dengan mode "a" (bukan "w"). Berapa baris isi berkasnya, dan mengapa?

Latihan 4. Tantangan: tulis fungsi catat_panen(berkas, tanggal, kg) yang menambah satu baris tanggal|kg ke berkas (mode "a"), dan fungsi total_panen(berkas) yang menjumlahkan kolom kg sambil melewati baris rusak.

09_bab13.py
# Bab 74 - Kunci jawaban: latihan Bab 13 (berkas)
from pathlib import Path

tmp = Path(__file__).parent / "_tmp"
tmp.mkdir(exist_ok=True)

print("--- Latihan 1 ---")
berkas = tmp / "angka.txt"
with open(berkas, "w", encoding="utf-8") as f:
    for n in range(1, 11):
        f.write(f"{n}\n")
with open(berkas, encoding="utf-8") as f:
    jumlah = sum(int(baris) for baris in f)
print("Jumlah:", jumlah)

print("--- Latihan 2 ---")
berkas_a = tmp / "angka_tambah.txt"
berkas_a.unlink(missing_ok=True)             # mulai dari berkas yang belum ada
for putaran in (1, 2):                       # sama dengan menjalankan skrip dua kali
    with open(berkas_a, "a", encoding="utf-8") as f:
        for n in range(1, 11):
            f.write(f"{n}\n")
    print(f"Setelah putaran {putaran}: {len(berkas_a.read_text(encoding='utf-8').splitlines())} baris")

print("--- Latihan 4 (tantangan) ---")
def catat_panen(berkas, tanggal, kg):
    with open(berkas, "a", encoding="utf-8") as f:
        f.write(f"{tanggal}|{kg}\n")

def total_panen(berkas):
    total = 0.0
    for baris in Path(berkas).read_text(encoding="utf-8").splitlines():
        bagian = baris.split("|")
        try:
            total += float(bagian[1])
        except (IndexError, ValueError):
            print("  baris rusak dilewati:", repr(baris))
    return total

panen = tmp / "panen.txt"
panen.unlink(missing_ok=True)
catat_panen(panen, "2026-10-01", 245.5)
catat_panen(panen, "2026-10-02", 310)
with open(panen, "a", encoding="utf-8") as f:
    f.write("baris rusak tanpa pemisah\n")
    f.write("2026-10-03|abc\n")
catat_panen(panen, "2026-10-04", 198.25)
print("Total panen:", total_panen(panen), "kg")
--- Latihan 1 ---
Jumlah: 55
--- Latihan 2 ---
Setelah putaran 1: 10 baris
Setelah putaran 2: 20 baris
--- Latihan 4 (tantangan) ---
  baris rusak dilewati: 'baris rusak tanpa pemisah'
  baris rusak dilewati: '2026-10-03|abc'
Total panen: 753.75 kg

Penjelasan.

  • Latihan 1: Satu angka per baris ("1\n", "2\n", …) lalu dibaca kembali dan dijumlahkan: 1 + 2 + … + 10 = 55. Setiap baris yang dibaca masih berupa teks, jadi dibungkus int().
  • Latihan 2: Isinya 20 baris: mode "a" (append) menambah di akhir berkas tanpa menghapus isi lama, sehingga tiap kali dijalankan 10 baris baru ditumpuk (10 setelah putaran pertama, 20 setelah kedua). Mode "w" justru menimpa dan akan selalu menyisakan 10 baris. Di skrip, putaran kedua diwakili perulangan; hasilnya sama dengan menjalankan skrip dua kali dari berkas yang belum ada.
  • Latihan 4: catat_panen memakai mode "a" supaya catatan lama aman. total_panen membaca baris demi baris, memecah dengan |, dan membungkus float(...) dalam try: baris tanpa pemisah memicu IndexError, dan nilai abc memicu ValueError. Keduanya ditangkap, baris rusak dilaporkan, dan perhitungan jalan terus. Total 245,5 + 310 + 198,25 = 753,75 kg dari tiga baris yang sehat.

Bab 14 — Format Data

Latihan 1. Tulis data ternak ke ternak.csv, baca kembali dengan DictReader, dan cetak total bobot_kg (jangan lupa mengubah tipenya).

Latihan 2. Simpan dict {"nama": "Kolam Ø1", "suhu": "28 °C"} ke JSON dengan dan tanpa ensure_ascii=False. Bandingkan isi berkasnya.

10_bab14.py
# Bab 74 - Kunci jawaban: latihan Bab 14 (CSV dan JSON)
import csv
import json
from pathlib import Path

tmp = Path(__file__).parent / "_tmp"
tmp.mkdir(exist_ok=True)

print("--- Latihan 1 ---")
ternak = [
    {"kandang": "K1", "jenis": "sapi", "umur_bulan": 36, "bobot_kg": 350.0},
    {"kandang": "K1", "jenis": "sapi", "umur_bulan": 30, "bobot_kg": 412.5},
    {"kandang": "K2", "jenis": "sapi", "umur_bulan": 42, "bobot_kg": 389.0},
    {"kandang": "K3", "jenis": "kambing", "umur_bulan": 14, "bobot_kg": 38.5},
]
with open(tmp / "ternak.csv", "w", newline="", encoding="utf-8") as f:
    penulis = csv.DictWriter(f, fieldnames=list(ternak[0]))
    penulis.writeheader()
    penulis.writerows(ternak)
with open(tmp / "ternak.csv", newline="", encoding="utf-8") as f:
    baris = list(csv.DictReader(f))
print("Nilai pertama dibaca sebagai:", repr(baris[0]["bobot_kg"]), "(teks!)")
print("Total bobot_kg:", sum(float(b["bobot_kg"]) for b in baris))

print("--- Latihan 2 ---")
data = {"nama": "Kolam Ø1", "suhu": "28 °C"}
for ascii_saja in (True, False):
    berkas = tmp / f"kolam_ascii_{ascii_saja}.json"
    with open(berkas, "w", encoding="utf-8") as f:
        json.dump(data, f, ensure_ascii=ascii_saja)
    print(f"ensure_ascii={ascii_saja}:", berkas.read_text(encoding="utf-8"))
--- Latihan 1 ---
Nilai pertama dibaca sebagai: '350.0' (teks!)
Total bobot_kg: 1190.0
--- Latihan 2 ---
ensure_ascii=True: {"nama": "Kolam \u00d81", "suhu": "28 \u00b0C"}
ensure_ascii=False: {"nama": "Kolam Ø1", "suhu": "28 °C"}

Penjelasan.

  • Latihan 1: Nilai pertama dibaca sebagai '350.0', yaitu teks: CSV tidak punya tipe. Karena itu float(...) wajib dipakai sebelum menjumlahkan. Total 350 + 412,5 + 389 + 38,5 = 1190,0 kg.
  • Latihan 2: Dengan ensure_ascii=True (bawaan), karakter di luar ASCII ditulis sebagai kode \u00d8 dan \u00b0; dengan False ditulis apa adanya (Ø dan °). Keduanya JSON yang sah dan menghasilkan data yang sama saat dibaca; False hanya lebih mudah dibaca manusia. Syaratnya berkas dibuka dengan encoding="utf-8".

Bab 16 — OOP

Latihan 2. Buat kelas Kebun sendiri (bukan turunan) dengan nama dan daftar panen_kg, lalu isi __len__ dan __str__. Cetak satu objek dan panggil len() padanya.

11_bab16.py
# Bab 74 - Kunci jawaban: latihan Bab 16 (OOP)

class Kebun:
    """Kebun tersendiri (bukan turunan kelas lain)."""

    def __init__(self, nama, panen_kg):
        self.nama = nama
        self.panen_kg = panen_kg

    def __len__(self):                       # dipanggil oleh len(objek)
        return len(self.panen_kg)

    def __str__(self):                       # dipanggil oleh print(objek)
        return f"Kebun {self.nama}: {len(self)} panen, total {sum(self.panen_kg)} kg"

kebun = Kebun("Kopi Utara", [245.5, 310.0, 198.5])
print(kebun)
print("len:", len(kebun))
Kebun Kopi Utara: 3 panen, total 754.0 kg
len: 3

Penjelasan.

  • Latihan 2: __len__ dipanggil oleh len(kebun) (3 panen) dan __str__ oleh print(kebun). Pada __str__, len(self) ikut memakai __len__. Total 245,5 + 310 + 198,5 = 754,0 kg.

Bab 22 — Regex dan Pengolahan Teks

Latihan 1. Tulis pola yang menemukan semua kode seperti JT-0042 di "JT-0042, MR-0107, xx-12". (Hasilnya hanya dua.)

Latihan 2. Dengan re.sub, ubah "Plot A,Plot B , Plot C" menjadi "Plot A, Plot B, Plot C".

12_bab22.py
# Bab 74 - Kunci jawaban: latihan Bab 22 (regex)
import re

print("--- Latihan 1 ---")
teks = "JT-0042, MR-0107, xx-12"
pola = r"\b[A-Z]{2}-\d{4}\b"          # dua huruf besar, tanda hubung, empat angka
print(re.findall(pola, teks))

print("--- Latihan 2 ---")
kotor = "Plot  A,Plot B ,  Plot C"
langkah1 = re.sub(r"\s*,\s*", ", ", kotor)      # koma: tanpa spasi sebelum, satu spasi sesudah
bersih = re.sub(r"\s{2,}", " ", langkah1)       # spasi ganda menjadi satu
print(repr(langkah1))
print(repr(bersih))
--- Latihan 1 ---
['JT-0042', 'MR-0107']
--- Latihan 2 ---
'Plot  A, Plot B, Plot C'
'Plot A, Plot B, Plot C'

Penjelasan.

  • Latihan 1: Pola \b[A-Z]{2}-\d{4}\b berarti: dua huruf besar, tanda hubung, empat angka, dengan batas kata di kedua ujung. Hasilnya hanya dua (JT-0042, MR-0107); xx-12 ditolak karena hurufnya kecil dan angkanya cuma dua.
  • Latihan 2: Dua langkah. Pertama \s*,\s* mengganti koma beserta spasi di sekitarnya dengan ", " (tepat satu spasi di belakang koma). Kedua \s{2,} mengganti spasi ganda menjadi satu. Hasil akhir sama persis dengan yang diminta.

Bab 23 — Pengantar numpy dan pandas

Latihan 1. Dari array np.array([480, 500, 510, 520, 530]), hitung bobot rata-rata dan pilih yang berbobot di atas 505 dengan topeng.

Latihan 2. Buat DataFrame ikan dengan kolom kolam, jenis, jumlah, bobot_rata_g, lalu hitung biomassa tiap baris (jumlah * bobot_rata_g / 1000) sebagai kolom baru.

13_bab23.py
# Bab 74 - Kunci jawaban: latihan Bab 23 (numpy dan pandas)
import numpy as np
import pandas as pd

print("--- Latihan 1 ---")
bobot = np.array([480, 500, 510, 520, 530])
print("Rata-rata:", bobot.mean())
topeng = bobot > 505                     # larik True/False
print("Topeng:", topeng)
print("Di atas 505:", bobot[topeng])

print("--- Latihan 2 ---")
ikan = pd.DataFrame({
    "kolam": ["A", "B", "C"],
    "jenis": ["lele", "nila", "lele"],
    "jumlah": [800, 1250, 600],
    "bobot_rata_g": [540.5, 300.0, 480.0],
})
ikan["biomassa_kg"] = ikan["jumlah"] * ikan["bobot_rata_g"] / 1000
print(ikan)
--- Latihan 1 ---
Rata-rata: 508.0
Topeng: [False False  True  True  True]
Di atas 505: [510 520 530]
--- Latihan 2 ---
  kolam jenis  jumlah  bobot_rata_g  biomassa_kg
0     A  lele     800         540.5        432.4
1     B  nila    1250         300.0        375.0
2     C  lele     600         480.0        288.0

Penjelasan.

  • Latihan 1: Rata-rata 508,0. bobot > 505 menghasilkan larik benar/salah (topeng) [False False True True True], dan bobot[topeng] hanya memilih yang bernilai True: 510, 520, 530.
  • Latihan 2: jumlah * bobot_rata_g / 1000 dihitung untuk semua baris sekaligus (tanpa perulangan): 800 x 540,5 / 1000 = 432,4; 1250 x 300 / 1000 = 375,0; 600 x 480 / 1000 = 288,0 kg.

Bab 26 — Statistika dengan Python

Latihan 4. Tantangan: buat data untuk dua plot dengan rata-rata sama persis, ulang uji t 1000 kali dengan benih berbeda, dan hitung berapa persen uji menghasilkan p di bawah 0,05. Apakah mendekati 5%? Apa artinya?

14_bab26.py
# Bab 74 - Kunci jawaban: latihan Bab 26 (statistika), tantangan
import numpy as np
from scipy import stats

ulangan = 1000
p_kecil = 0
for benih in range(ulangan):
    rng = np.random.default_rng(benih)             # benih berbeda tiap ulangan
    plot_1 = rng.normal(loc=30, scale=5, size=30)  # dua plot, rata-rata populasi SAMA (30)
    plot_2 = rng.normal(loc=30, scale=5, size=30)
    p = stats.ttest_ind(plot_1, plot_2).pvalue
    if p < 0.05:
        p_kecil += 1
print(f"Uji dengan p < 0,05: {p_kecil} dari {ulangan} ({p_kecil / ulangan * 100:.1f}%)")
Uji dengan p < 0,05: 56 dari 1000 (5.6%)

Penjelasan.

  • Latihan 4: Kedua plot diambil dari populasi yang sama persis (rata-rata 30, simpangan baku 5), jadi tidak ada perbedaan sungguhan. Meski begitu uji t menyatakan “berbeda” (p di bawah 0,05) pada 56 dari 1000 ulangan, yaitu 5,6%, dekat sekali dengan 5%. Artinya: batas 0,05 memang berarti “saya rela salah sekitar 5% dari waktu”; di antara uji pada data yang tidak berbeda, sekitar 1 dari 20 tetap tampak “nyata”. Itulah mengapa satu hasil p kecil saja tidak membuktikan apa pun jika Anda menguji banyak hal. Angka 56 adalah hasil benih 0 sampai 999; benih lain memberi angka sedikit berbeda di sekitar 5% (ini perkiraan statistik, tidak diuji di sini).

Bab 30 — Iterator dan Generator

Latihan 1. Tulis fungsi generator genap_sampai(n) yang memberikan bilangan genap dari 2 sampai n. Cetak list(genap_sampai(10)).

15_bab30.py
# Bab 74 - Kunci jawaban: latihan Bab 30 (generator)

def genap_sampai(n):
    angka = 2
    while angka <= n:
        yield angka                  # memberikan satu nilai, lalu berhenti sementara
        angka += 2

print(list(genap_sampai(10)))
print(list(genap_sampai(1)))         # batas terlalu kecil: kosong
[2, 4, 6, 8, 10]
[]

Penjelasan.

  • Latihan 1: Fungsi dengan yield adalah generator: ia memberikan satu nilai, berhenti, lalu lanjut saat diminta lagi. list(...) menghabiskannya: [2, 4, 6, 8, 10]. Untuk n = 1 tidak ada bilangan genap yang memenuhi, jadi hasilnya daftar kosong, bukan error.

Bab 31 — Decorator dan Closure

Latihan 1. Tulis closure buat_pengali(faktor) yang mengembalikan fungsi mengalikan angka dengan faktor. Buat kali_tiga dan kali_sepuluh, lalu coba keduanya.

Latihan 2. Tulis decorator hitung_panggilan yang menyimpan jumlah panggilan di atribut fungsi (pembungkus.jumlah). Pasang pada satu fungsi, panggil empat kali, dan cetak jumlah.

Latihan 4. Tantangan: tulis decorator lebih_dari(batas) yang melempar ValueError bila hasil fungsi di bawah batas. Pasang pada fungsi biomassa, dan uji dengan satu masukan yang lolos dan satu yang gagal.

16_bab31.py
# Bab 74 - Kunci jawaban: latihan Bab 31 (closure dan decorator)
import functools

print("--- Latihan 1 ---")
def buat_pengali(faktor):
    def kali(angka):
        return angka * faktor        # faktor "diingat" dari fungsi luar
    return kali

kali_tiga = buat_pengali(3)
kali_sepuluh = buat_pengali(10)
print(kali_tiga(7), kali_sepuluh(7))

print("--- Latihan 2 ---")
def hitung_panggilan(fungsi):
    @functools.wraps(fungsi)
    def pembungkus(*args, **kwargs):
        pembungkus.jumlah += 1
        return fungsi(*args, **kwargs)
    pembungkus.jumlah = 0
    return pembungkus

@hitung_panggilan
def sapa(nama):
    return f"Halo, {nama}"

for nama in ["Budi", "Sari", "Joko", "Tini"]:
    sapa(nama)
print("Jumlah panggilan:", sapa.jumlah, "| nama fungsi:", sapa.__name__)

print("--- Latihan 4 (tantangan) ---")
def lebih_dari(batas):
    def dekorator(fungsi):
        @functools.wraps(fungsi)
        def pembungkus(*args, **kwargs):
            hasil = fungsi(*args, **kwargs)
            if hasil < batas:
                raise ValueError(f"{fungsi.__name__} menghasilkan {hasil}, di bawah batas {batas}")
            return hasil
        return pembungkus
    return dekorator

@lebih_dari(100)
def biomassa_kg(jumlah, bobot_g):
    return jumlah * bobot_g / 1000

print(biomassa_kg(800, 540.5))
try:
    print(biomassa_kg(10, 300))
except ValueError as e:
    print("ValueError:", e)
--- Latihan 1 ---
21 70
--- Latihan 2 ---
Jumlah panggilan: 4 | nama fungsi: sapa
--- Latihan 4 (tantangan) ---
432.4
ValueError: biomassa_kg menghasilkan 3.0, di bawah batas 100

Penjelasan.

  • Latihan 1: buat_pengali(3) mengembalikan fungsi yang mengingat faktor = 3 walau fungsi luarnya sudah selesai. Itulah closure. kali_tiga(7) = 21 dan kali_sepuluh(7) = 70.
  • Latihan 2: Pembungkus menambah atribut jumlah pada dirinya sendiri setiap kali dipanggil: empat panggilan, jumlah = 4. functools.wraps menjaga __name__ tetap sapa, bukan pembungkus.
  • Latihan 4: Decorator berargumen punya tiga lapis: lebih_dari(batas) menerima argumen, mengembalikan dekorator(fungsi), yang mengembalikan pembungkus. Pembungkus memeriksa hasil: biomassa_kg(800, 540.5) = 432,4 lolos karena di atas 100, sedangkan biomassa_kg(10, 300) = 3,0 berada di bawah 100 sehingga ValueError dilempar dengan pesan yang menyebut nama fungsi (berkat wraps).

Bab 32 — Context Manager

Latihan 1. Tulis context manager kelas Pencatat yang mencetak “mulai” di __enter__ dan “selesai” di __exit__. Pakai dengan with dan tambahkan satu print di dalamnya.

Latihan 4. Tantangan: tulis context manager catat_langkah(nama) memakai @contextmanager yang mencetak “[nama] mulai” dan “[nama] selesai”; bila terjadi galat, cetak “[nama] GAGAL” lalu lempar ulang galatnya. Uji dengan satu blok sukses dan satu blok gagal.

17_bab32.py
# Bab 74 - Kunci jawaban: latihan Bab 32 (context manager)
from contextlib import contextmanager

print("--- Latihan 1 ---")
class Pencatat:
    def __enter__(self):
        print("mulai")
        return self

    def __exit__(self, jenis_error, nilai, jejak):
        print("selesai")
        return False                 # jangan menelan error

with Pencatat():
    print("  bekerja di dalam with")

print("--- Latihan 4 (tantangan) ---")
@contextmanager
def catat_langkah(nama):
    print(f"[{nama}] mulai")
    try:
        yield
    except Exception:
        print(f"[{nama}] GAGAL")
        raise                        # lempar ulang galatnya
    else:
        print(f"[{nama}] selesai")

with catat_langkah("sukses"):
    print("  isi blok sukses")
try:
    with catat_langkah("gagal"):
        raise ValueError("data rusak")
except ValueError as e:
    print("galat ditangkap di luar:", e)
--- Latihan 1 ---
mulai
  bekerja di dalam with
selesai
--- Latihan 4 (tantangan) ---
[sukses] mulai
  isi blok sukses
[sukses] selesai
[gagal] mulai
[gagal] GAGAL
galat ditangkap di luar: data rusak

Penjelasan.

  • Latihan 1: Urutan cetaknya: mulai, isi with, selesai. __exit__ selalu dijalankan; mengembalikan False berarti error (bila ada) tidak ditelan.
  • Latihan 4: Pada @contextmanager, kode sebelum yield adalah “masuk” dan kode sesudahnya adalah “keluar”. Blok sukses mencetak mulai lalu selesai (cabang else). Blok gagal mencetak mulai lalu GAGAL (cabang except), kemudian raise melempar ulang galatnya, yang akhirnya ditangkap di luar with. Tanpa raise, galat akan hilang diam-diam.

Bab 33 dan 34 — Type Hint dan Validasi Data dan Konfigurasi

Latihan 2 (Bab 33). Tulis fungsi cari_pohon(daftar: list[str], nama: str) -> int | None yang mengembalikan posisi nama dalam daftar, atau None bila tidak ada.

Latihan 1 (Bab 34). Buat model Ternak dengan kolom kandang: str, jenis: Literal["sapi", "kambing", "ayam"], umur_bulan: int (harus >= 0), dan bobot_kg: float (harus > 0). Coba dengan satu data benar dan satu data salah.

18_bab33_34.py
# Bab 74 - Kunci jawaban: latihan Bab 33 (type hint) dan Bab 34 (pydantic)
from typing import Literal

from pydantic import BaseModel, Field, ValidationError

print("--- Bab 33, latihan 2 ---")
def cari_pohon(daftar: list[str], nama: str) -> int | None:
    if nama in daftar:
        return daftar.index(nama)
    return None

pohon = ["Jati", "Meranti", "Mahoni"]
print(cari_pohon(pohon, "Mahoni"), cari_pohon(pohon, "Sengon"))

print("--- Bab 34, latihan 1 ---")
class Ternak(BaseModel):
    kandang: str
    jenis: Literal["sapi", "kambing", "ayam"]
    umur_bulan: int = Field(ge=0)
    bobot_kg: float = Field(gt=0)

print(Ternak(kandang="K1", jenis="sapi", umur_bulan=36, bobot_kg=350.0))
try:
    Ternak(kandang="K2", jenis="bebek", umur_bulan=-3, bobot_kg=0)
except ValidationError as e:
    print(f"{e.error_count()} kesalahan:")
    for galat in e.errors():
        print(" -", galat["loc"][0], "->", galat["msg"])
--- Bab 33, latihan 2 ---
2 None
--- Bab 34, latihan 1 ---
kandang='K1' jenis='sapi' umur_bulan=36 bobot_kg=350.0
3 kesalahan:
 - jenis -> Input should be 'sapi', 'kambing' or 'ayam'
 - umur_bulan -> Input should be greater than or equal to 0
 - bobot_kg -> Input should be greater than 0

Penjelasan.

  • Bab 33, latihan 2: cari_pohon mengembalikan posisi 2 untuk “Mahoni” dan None untuk “Sengon” yang tidak ada. Tipe int | None memberi tahu pemanggil bahwa ia harus memeriksa None. Skrip ini tidak menjalankan mypy, jadi pemeriksaan tipenya belum dibuktikan di sini.
  • Bab 34, latihan 1: Data yang benar lolos dan dicetak rapi. Data yang salah memicu tiga kesalahan sekaligus: jenis (“bebek” bukan salah satu pilihan), umur_bulan (-3 di bawah 0), dan bobot_kg (0 tidak lebih dari 0). Pydantic melaporkan semuanya dalam satu ValidationError, bukan berhenti di yang pertama. Pesan galat berbahasa Inggris karena itu bawaan Pydantic.