Slicing, aliasing, dan salinan

Sintaks slicing, perbedaan shallow copy dan deep copy, serta jebakan aliasing.

Daftar isi

Slicing adalah cara mengambil sebagian isi urutan nilai seperti list atau string berdasarkan rentang indeks, dengan hasil berupa objek baru. Topik ini membahas sintaks lengkapnya, salinan lewat slicing, perbedaan aliasing terhadap salinan, perbandingan is dengan ==, hingga perbedaan salinan dangkal (shallow copy) dan salinan dalam (deep copy). Pemahaman ini mencegah bug yang halus, yaitu perubahan yang diam-diam menjalar ke variabel lain.

Sintaks slicing#

Bentuk umumnya adalah s[awal:akhir:langkah], dengan awal ikut diambil, akhir tidak ikut diambil, dan langkah menentukan lompatannya. Ketiga nilai bersifat opsional, dengan nilai default 0, panjang urutan nilai, dan 1. Indeks negatif tetap berlaku, dan rentang yang melewati batas tidak menimbulkan error melainkan dipotong sampai batas yang tersedia.

python Contoh: variasi slicing
huruf = ["a", "b", "c", "d", "e"]
print(huruf[1:4])
print(huruf[:3])
print(huruf[2:])
print(huruf[-2:])
print(huruf[::2])
print(huruf[::-1])
text Keluaran
['b', 'c', 'd']
['a', 'b', 'c']
['c', 'd', 'e']
['d', 'e']
['a', 'c', 'e']
['e', 'd', 'c', 'b', 'a']

Rentang 1:4 mengambil indeks 1 sampai 3 saja, karena batas akhir tidak ikut diambil. Langkah 2 mengambil setiap elemen kedua, sedangkan langkah -1 menghasilkan urutan terbalik. Slicing pada string mengikuti aturan yang sama. Rentang yang melewati panjang list tidak error, melainkan mengambil bagian yang tersisa.

python Contoh: slicing pada string dan batas yang melewati jangkauan
teks = "python"
angka = [10, 20, 30]
print(teks[1:4])
print(angka[1:99])
text Keluaran
yth
[20, 30]

String python menghasilkan potongan yth, dan rentang 1:99 pada list tiga elemen menghasilkan [20, 30]. Perilaku pemaaf ini berbeda dengan pengaksesan satu indeks, yang ketat terhadap jangkauan.

Hasil slicing selalu berupa objek baru, sehingga perubahan pada potongan tidak menyentuh aslinya pada lapisan terluar. Batas yang menunjuk ke belakang pun diperlakukan wajar, misalnya huruf[4:2] menghasilkan list kosong karena rentangnya kosong. Perilaku yang toleran ini membuat slicing aman digunakan tanpa pemeriksaan panjang terlebih dahulu.

Salinan lewat slicing#

Slicing tanpa batas, yaitu a[:], menyalin seluruh isi list ke objek baru. Salinan semacam ini terpisah satu lapis, yaitu perubahan pada lapisan terluarnya tidak menjalar ke aslinya.

python Contoh: salinan penuh lewat a[:]
a = [1, 2, 3]
b = a[:]
b.append(4)
print(a)
print(b)
text Keluaran
[1, 2, 3]
[1, 2, 3, 4]

Penambahan 4 hanya terjadi pada b, dan a tetap berisi tiga elemen. Kata satu lapis penting di sini, karena elemen yang bersarang belum tentu ikut tersalin, sebagaimana dijelaskan pada bagian salinan dangkal di bawah.

Untuk list, beberapa cara menulis salinan lapisan terluar saling setara, yaitu a[:], list(a), a.copy(), dan copy.copy(a). Bentuk a.copy() paling jelas menyatakan maksudnya, sedangkan a[:] lebih pendek dan lazim ditemui pada kode lama. Untuk tuple, slicing tetap menghasilkan tuple baru, dan karena tuple immutable, salinannya jarang dibutuhkan.

Aliasing dan perbandingan is dengan ==#

Penugasan biasa tidak menyalin apa pun, melainkan hanya menempelkan nama baru pada objek yang sama. Kondisi ini disebut aliasing, dan perbedaannya dengan salinan terlihat dari dua operator. Operator == membandingkan isi, sedangkan operator is membandingkan identitas, yaitu apakah keduanya menunjuk objek yang sama atau tidak.

python Contoh: aliasing, ==, dan is
a = [1, 2, 3]
b = a
c = [1, 2, 3]
print(a == b, a == c)
print(a is b, a is c)
b.append(4)
print(a)
text Keluaran
True True
True False
[1, 2, 3, 4]

Ketiga list berisi angka yang sama, sehingga == bernilai True untuk semua pasangan. Hanya b yang menunjuk objek yang sama dengan a, sedangkan c adalah objek lain yang isinya kebetulan sama. Setelah append() pada b, list a ikut bertambah panjang karena keduanya satu objek yang sama.

Cara kerjanya berangkat dari model variabel Python, yaitu variabel hanyalah nama yang menunjuk objek dan bukan kotak yang menyimpan nilai. Penugasan b = a menyalin penunjuknya saja, sehingga kedua nama menunjuk objek yang sama. Untuk nilai immutable seperti bilangan dan string, keterkaitan ini tidak berbahaya karena isi objeknya tidak mungkin berubah.

Salinan dangkal#

Fungsi copy.copy() dari modul copy, bersama a[:] dan list(a), menghasilkan salinan dangkal. Lapisan terluarnya benar-benar objek baru, tetapi setiap elemennya masih menunjuk objek yang lama. Pada list datar perbedaan ini tidak terlihat, sedangkan pada list bersarang perbedaannya berbahaya.

python Contoh: jebakan salinan dangkal
import copy
jadwal = [["senin", "Ali"], ["selasa", "Budi"]]
dangkal = copy.copy(jadwal)
dangkal[0][1] = "Citra"
print(jadwal)
text Keluaran
[['senin', 'Citra'], ['selasa', 'Budi']]

Yang diubah hanyalah dangkal[0][1], tetapi jadwal ikut berubah. Penyebabnya, salinan dangkal menyalin daftar penunjuk pada lapisan terluar saja, sehingga list dalam tetap menjadi objek yang sama bagi jadwal dan dangkal. Pada list datar, salinan dangkal dan salinan dalam tidak terbedakan hasilnya, karena elemennya tidak memiliki lapisan dalam yang dapat dibagikan.

Salinan dalam#

Fungsi copy.deepcopy() menyalin seluruh struktur secara rekursif, hingga lapisan terdalam sekalipun menjadi objek baru. Salinan semacam ini sepenuhnya terpisah, dengan biaya kerja dan memori yang lebih besar, sehingga digunakan hanya bila kedalaman itu memang diperlukan.

python Contoh: deepcopy memutus keterkaitan
import copy
jadwal = [["senin", "Ali"], ["selasa", "Budi"]]
dalam = copy.deepcopy(jadwal)
dalam[0][1] = "Citra"
print(jadwal)
print(dalam)
text Keluaran
[['senin', 'Ali'], ['selasa', 'Budi']]
[['senin', 'Citra'], ['selasa', 'Budi']]

Kali ini jadwal tetap pada keadaan aslinya, karena list dalamnya pun sudah berupa objek yang berbeda. Perbedaan copy.copy() dan copy.deepcopy() inilah pertimbangan utama saat menyalin struktur data bersarang. Fungsi deepcopy() juga mencatat objek yang telah disalinnya, sehingga struktur yang menunjuk dirinya sendiri tidak memicu perulangan tanpa henti.

Kesalahan umum#

Jebakan yang paling sering muncul berada pada perkalian list bersarang. Ekspresi [[0] * 3] * 3 tampak seperti tabel tiga kali tiga, padahal ketiga barisnya adalah satu objek yang sama yang diulang.

python Contoh: baris matriks yang saling terikat
papan = [[0] * 3] * 3
papan[0][0] = 1
print(papan)
papan2 = [[0] * 3 for _ in range(3)]
papan2[0][0] = 1
print(papan2)
text Keluaran
[[1, 0, 0], [1, 0, 0], [1, 0, 0]]
[[1, 0, 0], [0, 0, 0], [0, 0, 0]]

Satu penugasan pada papan mengubah tiga baris sekaligus, sedangkan versi comprehension membangun baris baru pada setiap putaran sehingga penugasannya tepat sasaran. Bukti sederhananya, ekspresi papan[0] is papan[1] bernilai True pada versi pertama karena keduanya satu objek. Pola [nilai] * n aman untuk list datar yang elemennya berupa bilangan, tetapi berbahaya apabila elemennya berupa list atau objek lain yang mutable.

Kesalahan kedua adalah memakai is untuk membandingkan isi. Operator itu menjawab pertanyaan identitas objek, dan jawabannya bergantung pada keputusan internal Python menyangkut penyimpanan ulang nilai kecil, sehingga perbandingan isi selalu diserahkan kepada ==. Uji cepatnya, gunakan == bila yang dibandingkan isi, dan is bila yang dibandingkan identitas.

Ringkasan#

  • Slicing s[awal:akhir:langkah] menghasilkan objek baru dengan nilai default 0, panjang urutan nilai, dan 1, serta batas yang dipotong alih-alih error.
  • Penugasan b = a hanya menempelkan nama baru pada objek yang sama, sehingga == membandingkan isi sedangkan is membandingkan identitas.
  • Salinan dangkal menyalin lapisan terluar saja, sedangkan copy.deepcopy() menyalin seluruh struktur bersarang hingga terpisah penuh.
  • Perkalian list bersarang seperti [[0] * 3] * 3 mengulang objek yang sama, dan comprehension menggantikannya dengan baris yang benar-benar baru.

Referensi#