7 November 2026•baca 5 menit

Python Class: Riwayat yang Dipakai Bersama Semua Akun

Dua akun, dan riwayat masing-masing memuat setoran akun lain. List-nya duduk di body class, append tidak pernah menulis ke objek, jadi kedua akun menambah ke list yang sama.

Baca dalam bahasa Inggris

Laporan rekening bank yang kamu jalankan mencetak setoran Ani di dalam riwayat Budi. Saldonya benar dan tidak ada error yang muncul, jadi tidak ada yang menunjuk ke body class. Di situlah bug-nya: sebuah class attribute yang bisa diubah dan dipakai bersama semua akun.

Ini output dari skrip yang membuat Account("Ani", 100) dan Account("Budi", 0), lalu memanggil ani.deposit(50) dan budi.deposit(20):

TEXT
ani.balance 150
budi.balance 20
ani.history ['Ani deposit 50', 'Budi deposit 20']
budi.history ['Ani deposit 50', 'Budi deposit 20']
same list: True
Account.history ['Ani deposit 50', 'Budi deposit 20']
ani.__dict__ {'owner': 'Ani', 'balance': 150}

Baca baris terakhir. __dict__ milik Ani hanya berisi owner dan balance, tanpa history. Atribut itu tidak pernah dipasang di objeknya, jadi pencariannya menemukan list milik class.

Membaca self.history mencari di objek dulu, baru di class. self.history.append(x) adalah pemanggilan method pada list hasil pencarian itu. Itu bukan assignment, jadi tidak ada yang ditulis ke objek. Tutorial class di Python membahas urutan pencarian ini.

Poin Penting

  • List yang dipasang di body class adalah milik class, jadi setiap objek membaca list yang sama sampai objek itu punya list sendiri.
  • self.history.append(x) membaca atribut class lalu mengubahnya; tidak ada atribut yang ditulis ke objek, itu sebabnya history tidak muncul di ani.__dict__.
  • self.balance = ... kelihatan benar karena assignment selalu menulis di objek yang memegang namanya.
  • Buat state yang bisa berubah di __init__, dan sisakan class attribute untuk nilai yang tidak boleh diubah objek mana pun.

Dua berkas, satu perintah

Mulai dari versi yang rusak. List-nya duduk di body class, di atas __init__:

PYTHON
# account.py
class Account:
    history = []

    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance

    def deposit(self, amount):
        self.balance = self.balance + amount
        self.history.append(f"{self.owner} deposit {amount}")

Berikutnya, sebuah checker. Skrip ini membuat dua akun, melakukan satu setoran di masing-masing, lalu mengajukan empat pertanyaan:

PYTHON
# check_account.py
import account

passed = 0
total = 0


def check(name, ok, detail=""):
    global passed, total
    total += 1
    if ok:
        passed += 1
        print(f"PASS  {name}")
    else:
        print(f"FAIL  {name}  <- {detail}")


ani = account.Account("Ani", 100)
budi = account.Account("Budi", 0)

ani.deposit(50)
budi.deposit(20)

check("a deposit lands in the account that made it", ani.balance == 150, f"balance is {ani.balance}")
check("the other account's balance does not move", budi.balance == 20, f"balance is {budi.balance}")
check(
    "each account has one line of history",
    len(ani.history) == 1 and len(budi.history) == 1,
    f"ani has {len(ani.history)}, budi has {len(budi.history)}",
)
check(
    "the two history lists are different lists",
    ani.history is not budi.history,
    "both accounts point at the same list",
)

print()
print(f"{passed} of {total} checks passed")
raise SystemExit(0 if passed == total else 1)

Jalankan python3 check_account.py dengan versi rusak di atas. Exit code-nya 1:

TEXT
PASS  a deposit lands in the account that made it
PASS  the other account's balance does not move
FAIL  each account has one line of history  <- ani has 2, budi has 2
FAIL  the two history lists are different lists  <- both accounts point at the same list

2 of 4 checks passed

Dua pemeriksaan sudah lolos, dan itu jebakannya. self.balance = ... adalah assignment, dan assignment selalu menulis di objeknya, jadi setiap akun punya balance sendiri. self.history.append(...) tidak pernah melakukan assignment. Saldo adalah hal pertama yang biasanya kamu uji, dan saldonya kelihatan benar.

Sekarang pindahkan list-nya ke __init__, supaya setiap objek membuat list sendiri:

PYTHON
# account.py
class Account:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance
        self.history = []

    def deposit(self, amount):
        self.balance = self.balance + amount
        self.history.append(f"{self.owner} deposit {amount}")

Jalankan perintah yang sama lagi. Kali ini exit code-nya 0:

TEXT
PASS  a deposit lands in the account that made it
PASS  the other account's balance does not move
PASS  each account has one line of history
PASS  the two history lists are different lists

4 of 4 checks passed

Semua ini dijalankan di Python 3.14.9 di Linux. Versi Python lain belum dicoba, jadi ini bukti dari satu mesin.

Yang tidak dilakukan checker ini

Class attribute tetap tempat yang benar untuk nilai yang tidak pernah diubah siapa pun. Checker tidak bisa membedakan konstanta bersama dari list bersama, dan memang tidak perlu. Berbagi bukan masalahnya; mengubah sesuatu yang dipakai bersama itulah masalahnya.

history yang kosong juga bukan bukti bahwa kamu sudah memperbaikinya. Kedua versi memulai setiap akun dengan list kosong. Bug-nya baru muncul begitu objek kedua ada, dan itulah yang dibangun oleh checker.

Rusak dengan sengaja

Taruh history = [] kembali di body class. Lalu tambahkan satu baris di __init__:

PYTHON
self.history = Account.history

Kelihatannya setiap akun sekarang dapat list sendiri, tapi jalankan checker-nya dan dua pemeriksaan gagal lagi:

TEXT
PASS  a deposit lands in the account that made it
PASS  the other account's balance does not move
FAIL  each account has one line of history  <- ani has 2, budi has 2
FAIL  the two history lists are different lists  <- both accounts point at the same list

2 of 4 checks passed

Baris baru itu menyalin objek list yang sama ke kedua objek, jadi keduanya masih berbagi list itu.

Sebelum menaruh list, dict, atau set di body class, ajukan satu pertanyaan: apakah objek kedua seharusnya melihat perubahan ini? Kalau iya, bagikan dengan sengaja. Kalau tidak, buat di __init__.

Biar makin jago ngoding.

Yuk gabung bareng temen-temen developer lainnya buat dapet update teknologi, tips, dan tutorial santai tiap minggu.

Biar Nggak Kudet

Update teknologi, tips ngoding, dan insight santai langsung ke email kamu tiap minggu.

Santai, anti spam. Bisa berhenti langganan kapan aja.

Baca Selanjutnya

Lihat semua

Untuk developer yang belajar dari kesulitan

© 2026 Inva.dev.