Enkapsulasi ala Python

Konvensi _protected dan __mangled serta filosofi enkapsulasi pada Python.

Daftar isi

Enkapsulasi adalah penyembunyian detail internal di balik antarmuka yang stabil, dan Python menanganinya dengan konvensi penamaan alih-alih kata kunci privat seperti pada banyak bahasa lain. Dua konvensi yang dipakai adalah satu garis bawah untuk penanda internal dan dua garis bawah untuk name mangling, yaitu pengubahan nama atribut secara otomatis. Topik ini membahas keduanya, perilaku tepatnya pada interpreter, serta filosofi yang menyertai enkapsulasi ala Python.

Konvensi satu garis bawah#

Nama atribut atau metode yang diawali satu garis bawah, seperti _saldo, menyatakan bahwa nama itu ditujukan untuk pemakaian internal. Penanda ini murni konvensi, karena interpreter tetap mengizinkan aksesnya dari luar kelas. PEP 8 merinci bahwa bentuk _single_leading_underscore menandakan penggunaan internal secara konvensi, dan alat-alat seperti linter mengikuti penanda yang sama. Pernyataan from modul import * juga melewatkan nama-nama berawalan garis bawah, sehingga nama internal tidak ikut terimpor ke ruang nama pemanggil.

python Contoh: atribut internal tetap dapat diakses
class Dompet:
    def __init__(self, saldo_awal):
        self._saldo = saldo_awal

d = Dompet(50000)
print(d._saldo)
text Keluaran
50000

Atribut _saldo terbaca dari luar kelas tanpa hambatan apa pun, yang membuktikan penanda ini bukan penghalang akses. Penegakan konvensinya bersifat sosial, yaitu pembaca kode memahami bahwa atribut tersebut dapat berubah tanpa pemberitahuan, sehingga akses langsung tidak memberi jaminan antarmuka yang stabil. Pemakaian _saldo dari luar kelas boleh terjadi, tetapi dianggap menyimpang dari desain kelasnya. Konvensi serupa berlaku untuk fungsi dan modul, sehingga satu aturan penamaan memuat seluruh batas internal dalam satu basis kode.

Dua garis bawah dan name mangling#

Nama atribut yang diawali dua garis bawah tanpa diakhiri dua garis bawah mengalami name mangling. Interpreter mengubah __sandi menjadi _NamaKelas__sandi, sehingga nama itu tidak mudah tertimpa oleh subclass yang kebetulan memakai nama sama. Mekanisme ini sering dikelirukan sebagai cara menyembunyikan data, padahal namanya hanya dipindahkan dan tetap dapat ditemukan.

python Contoh: mangling pada atribut __sandi
class Akun:
    def __init__(self):
        self.__sandi = "rahasia-123"

    def periksa(self, tebakan):
        return tebakan == self.__sandi

a = Akun()
print(a.periksa("salah"))
print(a.periksa("rahasia-123"))
print(a.__dict__)
print(a._Akun__sandi)
text Keluaran
False
True
{'_Akun__sandi': 'rahasia-123'}
rahasia-123

Metode periksa() membaca self.__sandi dari dalam kelas tanpa masalah, karena mangling diterapkan berdasarkan kelas tempat kode itu ditulis. Kamus __dict__ memperlihatkan atribut tersimpan dengan nama _Akun__sandi, dan baris terakhir membaca atribut itu secara langsung melalui nama hasil manglingnya. Dengan demikian, dua garis bawah mengubah nama, bukan mengunci akses.

Perlu dicatat bahwa mangling bekerja pada penulisan identifier di dalam badan kelas, bukan pada akses melalui string nama. Pemanggilan getattr(a, "__sandi") justru menimbulkan AttributeError, karena nama atribut aslinya sudah menjadi _Akun__sandi, sedangkan Python tidak mengubah string yang diberikan secara dinamis. Perbedaan ini menegaskan bahwa mangling adalah aturan penamaan pada saat kode ditulis, bukan mekanisme penguncian saat berjalan.

Batas antar kelas turunan#

Manfaat praktis mangling terlihat pada pewarisan. Apabila subclass dan kelas induk sama-sama memakai nama berawalan dua garis bawah, mangling memastikan keduanya menghasilkan atribut yang berbeda, sehingga tidak ada nilai yang tertimpa tanpa disengaja.

python Contoh: mangling mencegah tabrakan nama
class Lapisan:
    def __init__(self):
        self.__data = "lapisan"


class LapisanTurunan(Lapisan):
    def __init__(self):
        super().__init__()
        self.__data = "turunan"

t = LapisanTurunan()
print(t.__dict__)
text Keluaran
{'_Lapisan__data': 'lapisan', '_LapisanTurunan__data': 'turunan'}

Masing-masing kelas menyimpan atribut dengan nama hasil manglingnya sendiri, sehingga kedua nilai tersimpan berdampingan pada objek t. Apabila kedua kelas memakai nama satu garis bawah atau tanpa garis bawah, penugasan pada subclass akan menimpa atribut milik kelas induk. Justru karena sifat ini, PEP 8 menyebut dua garis bawah sebagai pilihan untuk atribut yang memang tidak boleh bertabrakan dengan nama subclass, misalnya pada kelas yang dirancang untuk diwariskan. Sebaliknya, apabila penimpaan antar kelas memang dimaksudkan sebagai perilaku, satu garis bawah atau nama tanpa garis bawah membuatnya eksplisit dan mudah ditebak.

Filosofi: kesepakatan, bukan penghalang#

Python tidak menyediakan anggota privat sejati, dan pilihannya ini disengaja. Dokumentasi resmi menyatakan bahwa variabel internal dapat diakses bila memang diperlukan, asalkan pemanggilnya memahami risikonya. Praktik yang diharapkan adalah merancang antarmuka publik yang cukup, yaitu metode atau property untuk operasi yang sah, kemudian membiarkan detail internal berada di balik penanda garis bawah. Pendekatan ini menaruh kepercayaan pada penulis kode yang memakai kelas tersebut, sekaligus menjaga fleksibilitas untuk membuka bagian internal apabila kebutuhan yang sah muncul, misalnya saat pengujian. Berkas berikut menunjukkan kelas dengan antarmuka publik yang menjaga atribut internalnya tetap terkendali.

python dompet.py
class Dompet:
    def __init__(self, saldo_awal):
        self._saldo = saldo_awal      # konvensi internal

    def setor(self, jumlah):
        if jumlah <= 0:
            return False
        self._saldo += jumlah
        return True

    def lihat_saldo(self):
        return self._saldo

d = Dompet(50000)
d.setor(25000)
print(d.lihat_saldo())
text Keluaran
75000

Simpan berkas di atas di direktori kerja, lalu jalankan dengan perintah berikut.

bash
# menjalankan program kelas Dompet
python3 dompet.py
text Keluaran
75000

Satu-satunya jalan resmi mengubah saldo adalah metode setor(), yang menolak nilai nol dan negatif. Atribut _saldo tetap dapat diakses dari luar sebagaimana contoh sebelumnya, tetapi pembaca kode memahami bahwa perubahan langsung berada di luar kesepakatan. Apabila kelak aturan bisnisnya berubah, misalnya ada batas saldo maksimum, cukup metode internalnya yang disunting, dan kode yang mengikuti antarmuka tetap berjalan.

Kesalahan umum#

Kesalahan pertama adalah menganggap mangling sebagai keamanan. Nama _NamaKelas__sandi dapat dibaca siapa pun melalui __dict__, seperti yang ditunjukkan contoh sebelumnya, sehingga data sensitif tetap harus diamankan di luar objek, misalnya tidak pernah disimpan dalam bentuk teks mentah. Mangling dirancang untuk menghindari tabrakan nama pada pewarisan, bukan untuk menyembunyikan rahasia.

Kesalahan kedua adalah menciptakan nama berawalan dan berakhiran dua garis bawah, seperti __status__, dengan harapan ikut dimangling. Bentuk __nama__ tidak mengalami mangling sama sekali, karena bentuk tersebut diperuntukkan bagi nama khusus Python seperti __init__ dan __repr__. Menggunakan bentuk ini untuk nama baru berisiko bentrok dengan nama khusus versi Python berikutnya.

Kesalahan ketiga adalah membiasakan akses atribut berawalan garis bawah dari luar kelas. Kebiasaan ini menjadikan detail internal bagian dari antarmuka pada praktiknya, sehingga penyusunan ulang kode yang mengubah atribut tersebut langsung merusak pemanggilnya. Apabila sebuah nilai memang dibutuhkan di luar kelas, sediakan metode atau property publik untuk itu.

Ringkasan#

  • Satu garis bawah adalah konvensi internal yang tidak membatasi akses apa pun di interpreter.
  • Dua garis bawah memicu name mangling menjadi _NamaKelas__atribut, yang tujuannya mencegah tabrakan nama pada subclass.
  • Mangling mengubah nama, bukan mengunci akses, sehingga bukan mekanisme keamanan.
  • Bentuk __nama__ tidak dimangling dan diperuntukkan bagi nama khusus Python, sehingga jangan dipakai untuk nama baru.

Referensi#