Bayangkan kita mengambil sepuluh baris pertama dari data penjualan untuk diperiksa, lalu mengubah satu nilai di hasil itu — dan beberapa saat kemudian menyadari data aslinya ikut berubah. Kejadian seperti ini adalah salah satu jebakan paling umum bagi pengguna NumPy baru. Penyebabnya ada pada perbedaan view dan copy.

Sebuah view adalah array baru yang berbagi buffer memori dengan array aslinya; perubahan pada salah satunya otomatis terlihat di satunya. Sebuah copy memiliki buffer sendiri yang terpisah. NumPy sengaja mengembalikan view bila memungkinkan demi efisiensi memori dan kecepatan, sehingga kita wajib tahu operasi mana yang berbagi memori dan mana yang tidak.

Aturan Dasar View dan Copy #

Aturannya ringkas dan bisa dihafal:

  1. Single indexing dan basic slicing (contoh a[0], a[0:2], a[:, 1]) menghasilkan view.
  2. Fancy indexing dan boolean indexing (contoh a[[0, 2]], a[a > 5]) menghasilkan copy.
  3. .copy() menghasilkan copy secara eksplisit kapan pun kita memanggilnya.
  4. reshape, ravel, dan .T menghasilkan view bila memungkinkan; flatten selalu menghasilkan copy.
OperasiJenis hasilBerbagi memori dengan aslinya?
a[i], a[i, j] (single indexing)viewYa
a[0:2], a[:, 1] (basic slicing)viewYa
a.reshape(...), a.ravel()view bila memungkinkanYa
a.TviewYa
a[[0, 2]] (fancy indexing)copyTidak
a[a > 5] (boolean indexing)copyTidak
a.flatten()copyTidak
a.copy()copyTidak

Dua bab sebelumnya telah menyinggung aturan ini secara sepintas; kini kita buktikan dengan eksekusi nyata.

Membuktikan View dengan Pola Ubah dan Amati #

Cara paling meyakinkan menguji sifat sebuah array adalah pola ubah-dan-amati: ubah satu nilai di hasil seleksi, lalu periksa array aslinya.

python
import numpy as np

penjualan = np.array([
    [120, 85, 200, 65],
    [90, 150, 110, 40],
    [175, 60, 95, 130],
])
b = penjualan[0:2]
b[0, 0] = 999
print(penjualan)
print(b.base is penjualan)

Output yang Dihasilkan:

text
[[999  85 200  65]
 [ 90 150 110  40]
 [175  60  95 130]]
True

Yang terjadi di belakang layar:

  1. Baris 8 (b = penjualan[0:2]): basic slicing tidak menyalin satu byte pun. NumPy hanya membuat objek array baru yang menunjuk ke buffer milik penjualan, dengan metadata bentuk (2, 4).
  2. Baris 9 (b[0, 0] = 999): penulisan dilakukan ke buffer yang sama, sehingga elemen pertama penjualan ikut berubah.
  3. Baris 10 (print(penjualan)): buktinya tampak jelas — 120 di posisi pertama telah menjadi 999, padahal yang kita ubah adalah b.
  4. Baris 11 (b.base is penjualan): atribut base milik sebuah view menunjuk ke array pemilik buffer. Hasilnya True, tanda bahwa b hanyalah jendela menuju data milik penjualan. Pada hasil copy, base bernilai None.

Fancy Indexing Menghasilkan Copy #

Kebalikannya berlaku untuk fancy indexing: hasilnya selalu copy, sehingga modifikasi pada hasil tidak pernah menyentuh data asli. Untuk mendapat efek yang sama dari sebuah slice, kita memanggil .copy() secara eksplisit.

python
import numpy as np

penjualan = np.array([
    [120, 85, 200, 65],
    [90, 150, 110, 40],
    [175, 60, 95, 130],
])
c = penjualan[[0]]
c[0, 0] = 777
print(c)
print(penjualan[0, 0])

d = penjualan[0].copy()
d[0] = 555
print(penjualan[0, 0])
print(c.base)

Output yang Dihasilkan:

text
[[777  85 200  65]]
120
120
None

Mari kita periksa:

  1. Baris 8 (c = penjualan[[0]]): fancy indexing menyalin baris pertama ke buffer baru. Variabel c berdiri sendiri, dan base-nya None.
  2. Baris 9-10 (c[0, 0] = 777 lalu print): perubahan hanya terjadi pada c; penjualan[0, 0] tetap 120. Berbeda dengan blok sebelumnya, data asli tidak tersentuh.
  3. Baris 12 (d = penjualan[0].copy()): penjualan[0] saja adalah view, tetapi .copy() memutus hubungannya. d kini memiliki buffer sendiri.
  4. Baris 13-14 (d[0] = 555 lalu print): mengubah d tidak memengaruhi penjualan — persis perilaku yang kita inginkan saat data asli harus dilindungi.

reshape, T, ravel, dan flatten #

Keluarga operasi pengubahan bentuk mengikuti kaidah yang sama: view bila memungkinkan, copy bila tidak. Kita uji semuanya sekaligus dengan base.

python
import numpy as np

a = np.arange(6)
b = a.reshape(2, 3)
print(b.base is a)
print(b.T.base is a)
print(b.ravel().base is a)
print(b.flatten().base)
print(b.T.ravel().base)
b.T[0, 0] = 99
print(a)

Output yang Dihasilkan:

text
True
True
True
None
None
[99  1  2  3  4  5]

Penjelasan tiap pemeriksaan:

  1. Baris 4 (b = a.reshape(2, 3)): reshape biasanya hanya membaca ulang metadata tanpa memindahkan data, sehingga b.base is a bernilai True.
  2. Baris 5 (b.T.base is a): transpose adalah view; tidak ada elemen yang dipindah, hanya urutan pembacaan sumbu yang ditukar. Karena itu menulis lewat transpose ikut mengubah data asli — terbukti dari dua baris terakhir, ketika b.T[0, 0] = 99 mengubah a[0] menjadi 99.
  3. Baris 6 (b.ravel().base is a): pada array yang kontigu, ravel mengembalikan view berbentuk satu dimensi.
  4. Baris 7 (b.flatten().base): hasilnya None — flatten selalu menyalin, apa pun kondisinya.
  5. Baris 8 (b.T.ravel().base): setelah transpose, susunan data tidak lagi kontigu untuk urutan yang diminta ravel, sehingga NumPy terpaksa menyalin; base-nya None. Inilah kasus langka ketika ravel menghasilkan copy.

Satu tabel dan tiga eksperimen di atas cukup untuk memeriksa hampir semua operasi seleksi yang kita tulis. Saat membaca kode orang lain, cari dua hal: pola slicing yang menjadi view, dan pola fancy/boolean yang menjadi copy — lalu tanyakan apakah data asli seharusnya berubah atau tidak.