Setiap nilai pada Python adalah objek yang membawa tiga hal, yaitu identitas, tipe, dan nilai, sedangkan nama variabel hanyalah referensi yang menunjuk objek itu. Topik ini membahas konsekuensi model tersebut, mulai dari mutabilitas, aliasing, hingga perbedaan operator is dengan ==. Pemahaman ini mencegah bug yang halus, sekaligus menjelaskan mengapa data dapat berubah tanpa sepengetahuan bagian kode lain yang ikut menunjuknya, sesuatu yang berpengaruh langsung terhadap kebenaran dan konsumsi memori program.
Nama adalah referensi, bukan kotak#
Penugasan pada Python tidak menyalin nilai, melainkan menempelkan nama baru pada objek yang sudah ada. Objeknya sendiri tersimpan di heap, yaitu area memori yang dikelola CPython, dan setiap objek membawa penghitung referensi (reference count) yang mencatat berapa nama dan struktur data yang menunjuknya. Dua nama yang menunjuk objek yang sama disebut aliasing, dan kondisi ini menjadi akar dari sebagian besar jebakan mutabilitas.
import sys
data = [10, 20, 30]
print(sys.getrefcount(data))
salinan_nama = data
print(sys.getrefcount(data))
del salinan_nama
print(sys.getrefcount(data))2
3
2Fungsi sys.getrefcount() mengembalikan jumlah referensi ditambah satu, karena pemanggilan fungsi itu sendiri membuat satu referensi sementara. Keluaran di atas menunjukkan angka bertambah ketika salinan_nama ditempelkan pada objek yang sama dan turun kembali setelah nama itu dihapus. Sifat ini mengonfirmasi bahwa del hanya melepas nama, bukan memusnahkan objeknya.
a = [1, 2, 3]
b = a
c = [1, 2, 3]
print(a is b)
print(a is c)
b.append(4)
print(a)True
False
[1, 2, 3, 4]Variabel b adalah alias dari a, sehingga a is b bernilai True dan penambahan lewat b terlihat oleh a. Sebaliknya c adalah objek lain yang isinya kebetulan sama, sehingga a is c bernilai False dan c sepenuhnya terpisah dari a.
Mutable dan immutable#
Objek Python terbagi dua kelompok besar. Objek mutable seperti list, dictionary, dan set dapat diubah isinya tanpa mengganti identitasnya, sedangkan objek immutable seperti int, float, str, tuple, dan frozenset tidak pernah berubah. Apabila isi objek immutable perlu berbeda, Python membangun objek baru, sebagaimana perintah teks = teks + "!" yang menghasilkan string baru alih-alih memodifikasi string lama.
Kelompok immutable menawarkan satu keuntungan yang dipakai CPython secara langsung, yaitu aman dibagikan. Apabila dua nama menunjuk objek yang tidak mungkin berubah, keduanya boleh menunjuk satu objek yang sama tanpa risiko saling mengganggu, dan penghematan memori diperoleh tanpa konsekuensi kebenaran apa pun.
a = "satu"
b = "satu"
c = "".join(["sa", "tu"])
print(a is b)
print(a is c)
print(a == c)True
False
TrueKonstanta string yang sama di dalam satu kode sumber menjadi satu objek yang dibagikan, sehingga a is b bernilai True. String c dirakit saat program berjalan dan selalu berupa objek baru, sehingga a is c bernilai False meskipun a == c tetap True. Diagram berikut merangkum keadaan semacam itu, yaitu dua nama yang berbagi satu objek.
Identitas versus kesetaraan#
Operator is membandingkan identitas, yaitu apakah keduanya menunjuk objek yang sama atau tidak, sedangkan operator == membandingkan nilai melalui metode __eq__. Kedua pertanyaan itu berbeda, dan kebingungan di antara keduanya adalah sumber bug yang halus. Kelas yang tidak mendefinisikan __eq__ mewarisi perilaku bawaan, yaitu kesetaraan dihitung dari identitas, sehingga dua instans yang isinya sama pun tidak pernah ==.
class Tugas:
def __init__(self, judul):
self.judul = judul
t1 = Tugas("kirim laporan")
t2 = Tugas("kirim laporan")
print(t1 == t2, t1 is t2)False FalseKedua instans berjudul sama, tetapi tanpa definisi __eq__ kesetaraannya jatuh ke identitas dan hasilnya False. Panduan praktisnya, gunakan == untuk membandingkan nilai dan simpan is untuk uji identitas, terutama pola x is None yang tidak dapat disalahartikan. Bedanya juga terasa pada struktur data, karena dictionary dan set mencari entrinya dengan kesetaraan, sehingga dua kunci yang == dianggap satu entri meskipun objeknya berbeda, sedangkan is tidak terlibat sama sekali dalam pencarian itu.
Mutabilitas pada argumen fungsi#
Fungsi menerima referensi, bukan salinan, sehingga mutasi pada parameter langsung terlihat oleh pemanggil. Perilaku ini berguna untuk operasi yang memang dimaksudkan mengubah data, tetapi mengejutkan apabila pemanggil tidak menyadari bahwa datanya dibagikan.
def tambah_tugas(daftar, tugas):
daftar.append(tugas)
tugas_hari_ini = ["rapat"]
tugas_terpantau = tugas_hari_ini
tambah_tugas(tugas_hari_ini, "ulas kode")
print(tugas_hari_ini)
print(tugas_terpantau)['rapat', 'ulas kode']
['rapat', 'ulas kode']Variabel tugas_terpantau tidak pernah disentuh fungsi, tetapi isinya ikut bertambah karena menunjuk objek yang sama. Fungsi yang memutasi argumennya sebaiknya menyatakan hal itu pada namanya atau pada docstring. Apabila data asli harus terjaga, pemanggil dapat mengirim salinan lewat tambah_tugas(list(tugas), ...).
Pola aman yang lazim adalah membagi fungsi menjadi dua kelompok yang jelas, yaitu fungsi yang memutasi argumennya dan fungsi yang mengembalikan hasil baru. Kelompok pertama lazim menamai dirinya dengan kata kerja yang tegas seperti tambah atau urutkan, sedangkan kelompok kedua mengembalikan list baru dan membiarkan argumennya utuh. Konvensi sederhana ini membuat pembaca kode tidak perlu membaca isinya untuk mengetahui apakah datanya berubah.
Demo aliasing#
Demo berikut memperagakan hubungan nama dan objek secara langsung. Tombol b = a menjadikan b alias dari a, tombol b.append(4) memutasi objek yang ditunjuk b, dan tombol b = [1, 2, 3] memberi objek baru kepada b. Perhatikan isyarat a is b pada setiap langkah, kemudian cocokkan hasilnya dengan perilaku list pada contoh-contoh sebelumnya.
Kode sumber demo
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8">
<title>Demo aliasing list</title>
<style>
body { font-family: ui-sans-serif, system-ui, sans-serif; margin: 0; padding: 12px 14px; background: #f8fafc; color: #0f172a; }
h1 { font-size: 15px; margin: 0 0 6px; }
p#status { font-size: 13px; line-height: 1.5; min-height: 40px; margin: 0 0 8px; }
.tombol { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 10px; }
button { font: inherit; font-size: 13px; padding: 6px 12px; border: 1px solid #6366f1; border-radius: 6px; background: #eef2ff; color: #3730a3; cursor: pointer; }
button:hover { background: #e0e7ff; }
svg { display: block; background: #ffffff; border: 1px solid #e2e8f0; border-radius: 8px; max-width: 100%; }
.badge { display: inline-block; font-size: 12px; font-weight: 600; padding: 2px 10px; border-radius: 999px; background: #fef3c7; }
</style>
</head>
<body>
<h1>Demo aliasing: nama versus objek</h1>
<p id="status">Keadaan awal: a dan b menunjuk dua objek list yang berbeda, sehingga identitasnya terpisah.</p>
<div class="tombol">
<button id="b-alias" type="button">b = a</button>
<button id="b-append" type="button">b.append(4)</button>
<button id="b-baru" type="button">b = [1, 2, 3]</button>
<button id="b-reset" type="button">Ulangi</button>
</div>
<svg id="kanvas" viewBox="0 0 520 190" width="520" height="190"></svg>
<p id="isi">a is b: <span class="badge" id="hasil">False</span></p>
<script>
(function () {
var NS = "http://www.w3.org/2000/svg";
var kanvas = document.getElementById("kanvas");
var status = document.getElementById("status");
var hasil = document.getElementById("hasil");
var hitung = 0;
var objekA, objekB;
function objekBaru() {
hitung += 1;
return { id: hitung, isi: [1, 2, 3] };
}
function teksIsi(o) {
return "list " + JSON.stringify(o.isi);
}
function teks(x, y, t, ukuran, warna) {
var el = document.createElementNS(NS, "text");
el.setAttribute("x", x);
el.setAttribute("y", y);
el.setAttribute("text-anchor", "middle");
el.setAttribute("font-size", ukuran || 14);
el.setAttribute("fill", warna || "#0f172a");
el.textContent = t;
kanvas.appendChild(el);
}
function kotak(x, y, w, h, isi) {
var r = document.createElementNS(NS, "rect");
r.setAttribute("x", x); r.setAttribute("y", y);
r.setAttribute("width", w); r.setAttribute("height", h);
r.setAttribute("rx", 8);
r.setAttribute("fill", "#eef2ff");
r.setAttribute("stroke", "#6366f1");
r.setAttribute("stroke-width", 1.5);
kanvas.appendChild(r);
teks(x + w / 2, y + h / 2 - 3, isi, 14);
}
function nama(x, y, label) {
var r = document.createElementNS(NS, "rect");
r.setAttribute("x", x); r.setAttribute("y", y);
r.setAttribute("width", 64); r.setAttribute("height", 32);
r.setAttribute("rx", 8);
r.setAttribute("fill", "#f1f5f9");
r.setAttribute("stroke", "#64748b");
r.setAttribute("stroke-width", 1.5);
kanvas.appendChild(r);
teks(x + 32, y + 21, label, 14);
}
function panah(x1, y1, x2, y2) {
var l = document.createElementNS(NS, "line");
l.setAttribute("x1", x1); l.setAttribute("y1", y1);
l.setAttribute("x2", x2); l.setAttribute("y2", y2);
l.setAttribute("stroke", "#475569");
l.setAttribute("stroke-width", 1.5);
l.setAttribute("marker-end", "url(#panah)");
kanvas.appendChild(l);
}
function gambar() {
while (kanvas.firstChild) {
kanvas.removeChild(kanvas.firstChild);
}
var defs = document.createElementNS(NS, "defs");
var m = document.createElementNS(NS, "marker");
m.setAttribute("id", "panah");
m.setAttribute("viewBox", "0 0 10 10");
m.setAttribute("refX", "9"); m.setAttribute("refY", "5");
m.setAttribute("markerWidth", "7"); m.setAttribute("markerHeight", "7");
m.setAttribute("orient", "auto-start-reverse");
var p = document.createElementNS(NS, "path");
p.setAttribute("d", "M0 0L10 5L0 10z");
p.setAttribute("fill", "#475569");
m.appendChild(p); defs.appendChild(m); kanvas.appendChild(defs);
teks(52, 16, "nama", 12, "#64748b");
teks(345, 16, "objek", 12, "#64748b");
if (objekA === objekB) {
kotak(280, 78, 130, 44, teksIsi(objekA));
nama(20, 40, "a");
nama(20, 128, "b");
panah(86, 56, 276, 94);
panah(86, 144, 276, 106);
teks(345, 142, "refcount = 2", 12, "#64748b");
} else {
kotak(280, 26, 130, 44, teksIsi(objekA));
kotak(280, 126, 130, 44, teksIsi(objekB));
nama(20, 32, "a");
nama(20, 132, "b");
panah(86, 48, 276, 48);
panah(86, 148, 276, 148);
teks(345, 90, "refcount = 1", 12, "#64748b");
}
hasil.textContent = objekA === objekB ? "True" : "False";
}
function reset() {
objekA = objekBaru();
objekB = objekBaru();
status.textContent = "Keadaan awal: a dan b menunjuk dua objek list yang berbeda, sehingga identitasnya terpisah.";
gambar();
}
document.getElementById("b-alias").addEventListener("click", function () {
objekB = objekA;
status.textContent = "b kini menunjuk objek yang sama dengan a. Mutasi lewat salah satu nama terlihat oleh keduanya, dan a is b menjadi True.";
gambar();
});
document.getElementById("b-append").addEventListener("click", function () {
objekB.isi.push(4);
status.textContent = objekA === objekB
? "Objek bersama berubah isinya, sehingga a juga melihat 4. Inilah jebakan aliasing."
: "Hanya objek milik b yang berubah, sedangkan a tidak terdampak karena berbeda objek.";
gambar();
});
document.getElementById("b-baru").addEventListener("click", function () {
objekB = objekBaru();
status.textContent = "b diberi objek baru. a tetap menunjuk objek lamanya, sehingga keduanya terpisah kembali.";
gambar();
});
document.getElementById("b-reset").addEventListener("click", reset);
reset();
})();
</script>
</body>
</html>Pada keadaan awal, a dan b menunjuk dua objek yang berbeda, sehingga a is b bernilai False. Setelah tombol b = a, kedua nama menunjuk satu kotak dan mutasi lewat salah satunya terlihat oleh yang lain, sama seperti keluaran ['rapat', 'ulas kode'] di atas. Tombol b = [1, 2, 3] memutus keterkaitan itu, dan identitasnya kembali terpisah tanpa mengubah isi objek milik a. Setiap pergantian keadaan pada demo adalah padanan langsung dari penugasan Python, sehingga kejadian yang sama dapat diuji ulang di REPL dengan tiga baris yang sama.
Kesalahan umum#
Kesalahan pertama adalah memakai is untuk membandingkan isi. Pada beberapa kasus hasilnya tampak benar karena CPython menyimpan ulang (interning) bilangan kecil dan konstanta string yang sama, sehingga dua nilai identik kebetulan objeknya sama. Keputusan seperti ini milik implementasi, dapat berubah antar versi, dan tidak menjamin apa pun untuk nilai yang dibangun saat program berjalan.
Kesalahan kedua adalah menyiapkan struktur data bersama tanpa memikirkan siapa yang berhak memutasi. Perbaikannya sederhana, yaitu membuat salinan pada batas antarmodul, misalnya list(data) untuk lapisan terluar atau copy.deepcopy(data) untuk struktur bersarang, kemudian menjadikan ketidakberubahan sebagai janji antarmuka yang tertulis.
Ringkasan#
- Nama hanyalah referensi, dan penugasan menempelkan nama pada objek yang sama, sebagaimana dibuktikan
sys.getrefcount()dan operatoris. - Objek mutable berubah tanpa mengganti identitasnya, sedangkan objek immutable aman dibagikan karena isinya tidak mungkin berubah.
ismembandingkan identitas,==membandingkan nilai melalui__eq__, dan kelas tanpa__eq__menjatuhkan kesetaraan ke identitas.- Fungsi menerima referensi, sehingga mutasi parameter terlihat oleh pemanggil dan salinan eksplisit diperlukan bila data aslinya harus terjaga.
Referensi#
- Data model — Python Software Foundation — Definisi identitas, tipe, dan nilai pada objek Python.
- sys.getrefcount — Python Software Foundation — Dokumentasi penghitungan referensi beserta catatan referensi sementara pada argumen.
- copy — Python Software Foundation — Salinan dangkal dan dalam untuk memutus aliasing pada struktur bersarang.