Kursus Python / Memahami scope, mutasi objek, dan salinan data

Scope menentukan tempat Python mencari nama, sedangkan mutabilitas menentukan perubahan yang terlihat melalui beberapa nama yang menunjuk objek sama. Membedakan pengikatan ulang nama dari perubahan objek membantu menjelaskan mengapa list dapat berubah setelah diberikan kepada fungsi.

Pisahkan nama lokal dari keadaan di luar fungsi

Nama yang diberi nilai di dalam fungsi biasanya menjadi nama lokal, kecuali dinyatakan global atau nonlocal. Membaca nilai dari luar fungsi berbeda dari mengikat nama luar ke objek baru. Deklarasi tersebut mengatur pengikatan nama, bukan membuat seluruh objek menjadi kebal terhadap perubahan.

Pencarian nama dalam fungsi biasa sering dijelaskan sebagai LEGB: Local atau lokal fungsi, Enclosing atau fungsi pembungkus, Global atau tingkat modul, lalu Builtins atau nama bawaan Python. Ini panduan untuk kasus fungsi biasa; badan kelas dan scope anotasi mempunyai aturan tambahan, sehingga jangan menganggap semua blok program identik. Lihat aturan resolusi nama Python.

global menunjuk pengikatan pada tingkat modul tempat fungsi didefinisikan. nonlocal menunjuk nama yang sudah terikat pada fungsi pembungkus terdekat yang sesuai; jika tidak ada, definisi ditolak dengan SyntaxError. Keduanya berguna pada keadaan tertentu, tetapi menerima data sebagai argumen dan mengembalikan hasil biasanya membuat aliran data lebih mudah diuji.

Amati list yang dibagi dan list yang disalin

Simpan program lengkap berikut sebagai scope.py dengan encoding UTF-8 pada folder latihan. Gunakan Python 3.12 atau lebih baru dan jalankan python scope.py di terminal lingkungan proyek. Program hanya menggunakan fitur bawaan.

def add_fee(items, fee):
    items.append(fee)

original = [3500, 8000]
alias = original
copy = original.copy()
add_fee(alias, 500)
print(original)
print(copy)

nested = [[1], [2]]
shallow = nested.copy()
shallow[0].append(9)
print(nested)

Keluaran yang diharapkan:

[3500, 8000, 500]
[3500, 8000]
[[1, 9], [2]]

alias = original tidak menyalin list. Kedua nama menunjuk objek yang sama, sehingga add_fee(alias, 500) menambahkan 500 ke list yang juga terlihat melalui original. Fungsi tidak mengembalikan list secara eksplisit; perubahan tetap terjadi karena append memodifikasi objek yang diterima.

original.copy() membuat list luar baru sebelum biaya ditambahkan. Karena itu, list copy masih memuat dua bilangan awal. Namun salinan tersebut merupakan salinan dangkal: ia menyimpan referensi ke unsur yang sama. Pada contoh nested, unsur pertama merupakan list lain; shallow[0].append(9) mengubah list dalam yang dibagi oleh kedua wadah luar.

Jika perlu menyalin struktur data bersarang secara rekursif, modul standar copy menyediakan copy.deepcopy. Pada list bersarang sederhana, perubahan list dalam hasil deep copy tidak mengubah list dalam sumber. Namun deep copy bukan cara umum menggandakan file terbuka, koneksi, atau semua keadaan yang sengaja dibagi. Salin hanya bagian yang membutuhkan kemandirian; dokumentasi shallow dan deep copy menjelaskan batasnya.

Telusuri pengikatan lokal dan counter nonlocal

Simpan contoh kedua berikut sebagai scope_names.py agar contoh list sebelumnya tetap tersedia. Jalankan python scope_names.py dari folder yang sama. Kedua program berdiri sendiri dan tidak memerlukan impor satu sama lain.

value = 10
def local_read():
    value = 20
    return value
def broken():
    value += 1
    return value
print(local_read(), value)
try:
    broken()
except UnboundLocalError:
    print("local name not initialized")
def make_counter():
    count = 0
    def increment():
        nonlocal count
        count += 1
        return count
    return increment
counter = make_counter()
print(counter(), counter())

Keluaran yang diharapkan:

20 10
local name not initialized
1 2

local_read mengikat nama lokal value ke 20; nama modul tetap bernilai 10. Di dalam broken, operasi value += 1 juga merupakan pemberian nilai. Karena itu, Python menganggap value sebagai lokal pada fungsi tersebut, tetapi nilai lokal belum tersedia saat penambahan membacanya. Hasilnya adalah UnboundLocalError, yang ditangkap agar program dapat meneruskan contoh counter.

Setiap panggilan make_counter() membuat keadaan count tersendiri. Fungsi increment memakai nonlocal count untuk mengubah pengikatan dari fungsi pembungkusnya. Objek fungsi yang dikembalikan mempertahankan akses ke keadaan itu sesudah make_counter selesai. Dua panggilan melalui counter menghasilkan 1 lalu 2.

Hapus deklarasi nonlocal count untuk percobaan terpisah. count += 1 kini mencoba membaca nama lokal yang belum terikat dan gagal. Pulihkan deklarasi setelah memahami penyebabnya. Membuat other = make_counter() menghasilkan counter independen yang kembali dimulai dari 1; ia tidak mengatur ulang counter pertama.

Nyatakan perubahan data dalam kontrak fungsi

  1. Di program list, panggil add_fee(copy, 100) sebelum mencetak hasil. List copy bertambah 100, tetapi original tidak memperoleh 100. Pulihkan percobaan ini sebelum perubahan berikutnya.
  2. Ganti items.append(fee) dengan items = items + [fee]. List baru hanya terikat ke nama lokal items; list milik pemanggil tetap sama. Tanpa return, list baru tersebut tidak diserahkan sebagai hasil fungsi.
  3. Pulihkan program asli dan cocokkan keluaran keduanya. Periksa identitas list luar dan list dalam secara terpisah agar kesamaan isi tidak disamakan dengan kesamaan objek.

Jika fungsi seharusnya mempertahankan input, buat hasil baru dan kembalikan secara jelas. Jika fungsi memang mengubah input, jelaskan perubahan itu melalui nama, dokumentasi, dan pengujian. Uji nilai balik sekaligus data asal; nilai balik yang benar saja belum membuktikan bahwa data pemanggil terjaga.

Objek mutable yang dibagi juga menjelaskan risiko list sebagai nilai argumen bawaan. Scope dan penyalinan menyelesaikan persoalan berbeda: scope menentukan nama yang dirujuk, sedangkan salinan menentukan bagian struktur objek yang masih dibagi.