Python

Protokol Descriptor Python yang Terlupakan: Rahasia di Balik Desain ORM Modern

Kholil · 07 May 2026 · 2 min read · 1 views
Protokol Descriptor Python yang Terlupakan: Rahasia di Balik Desain ORM Modern

Pelajari bagaimana Python descriptor protocol adalah rahasia di balik desain ORM modern seperti SQLAlchemy dan Django ORM.

Sebagian besar developer Python mengenal @property dan decorator, namun hanya sedikit yang memahami mekanisme yang sebenarnya bekerja di balik layar. Protokol descriptor adalah fondasi yang powerful namun sering diabaikan—dan inilah yang membuat ORM modern seperti SQLAlchemy dan Django ORM berfungsi dengan mulus.

Memahami Descriptor Protocol: Lebih dari Sekadar Magic

Descriptor protocol adalah mekanisme Python yang memungkinkan objek untuk mengontrol bagaimana atribut diakses, dimodifikasi, atau dihapus. Ketika Anda menulis obj.attr, Python sebenarnya menjalankan serangkaian operasi kompleks yang melibatkan descriptor.

Secara teknis, descriptor adalah objek yang mengimplementasikan setidaknya satu dari tiga metode khusus: __get__, __set__, atau __delete__. Ketika metode-metode ini didefinisikan pada class level, Python akan memanggil mereka secara otomatis ketika atribut diakses.

class Descriptor:
    def __get__(self, obj, objtype=None):
        print(f"Getting attribute")
        return "nilai descriptor"
    
    def __set__(self, obj, value):
        print(f"Setting attribute to {value}")
    
    def __delete__(self, obj):
        print(f"Deleting attribute")

class MyClass:
    attr = Descriptor()

obj = MyClass()
obj.attr  # Memanggil __get__
obj.attr = "baru"  # Memanggil __set__
del obj.attr  # Memanggil __delete__

Property: Descriptor dalam Pakaian Berbeda

Tahukah Anda bahwa @property sebenarnya adalah descriptor? Ketika Anda menulis getter dan setter dengan decorator, Python membuat instance dari class property, yang merupakan descriptor built-in.

class Person:
    def __init__(self, name):
        self._name = name
    
    @property
    def name(self):
        return self._name
    
    @name.setter
    def name(self, value):
        if not value:
            raise ValueError("Name tidak boleh kosong")
        self._name = value

p = Person("Alice")
print(p.name)  # Memanggil getter
p.name = "Bob"  # Memanggil setter

Ini bekerja karena property mengimplementasikan descriptor protocol. Setiap kali atribut diakses, Python secara otomatis menjalankan fungsi getter yang terikat.

Data Descriptors vs Non-Data Descriptors

Ada perbedaan penting antara dua jenis descriptor. Data descriptor mengimplementasikan baik __get__ maupun __set__ (atau __delete__), sementara non-data descriptor hanya mengimplementasikan __get__.

Perbedaan ini memiliki konsekuensi penting untuk pencarian atribut (attribute lookup). Python mengikuti urutan pencarian ini untuk atribut instance:

  1. Data descriptor dari class
  2. Atribut instance
  3. Non-data descriptor dari class
  4. Atribut class lainnya
class DataDescriptor:
    def __get__(self, obj, objtype=None):
        return "dari data descriptor"
    
    def __set__(self, obj, value):
        pass

class NonDataDescriptor:
    def __get__(self, obj, objtype=None):
        return "dari non-data descriptor"

class Example:
    data_desc = DataDescriptor()
    non_data_desc = NonDataDescriptor()

obj = Example()
obj.data_desc = "nilai instance"  # Tetap memanggil __get__ descriptor
obj.non_data_desc = "nilai instance"  # Menggunakan atribut instance

print(obj.data_desc)  # "dari data descriptor"
print(obj.non_data_desc)  # "nilai instance"

Bagaimana ORM Memanfaatkan Descriptor

ORM modern menggunakan descriptor untuk mengubah cara field database diakses dari Python. Ketika Anda mendefinisikan model seperti ini di SQLAlchemy:

from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class User(Base):
    __tablename__ = 'users'
    
    id = Column(Integer, primary_key=True)
    name = Column(String(50))
    email = Column(String(100))

Setiap Column adalah descriptor. Ketika Anda mengakses user.name, descriptor tidak hanya mengembalikan nilai—ia juga melacak apakah nilai telah berubah (dirty tracking), memvalidasi tipe data, dan mengelola koneksi database.

# Behind the scenes
user = User()
user.name = "Alice"  # __set__ descriptor mencatat perubahan
print(user.name)  # __get__ descriptor mengembalikan nilai

# SQLAlchemy tahu field mana yang telah dimodifikasi
session.add(user)
session.commit()  # Hanya field yang berubah yang di-update

Lazy Loading dan Relationship Management

Salah satu fitur paling powerful yang diaktifkan oleh descriptor adalah lazy loading. ORM dapat menggunakan descriptor untuk memuat data relasi hanya ketika diakses, bukan sebelumnya.

class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    posts = relationship('Post')  # Ini adalah descriptor

user = session.query(User).first()
# Queries belum dijalankan untuk posts
user.posts  # Descriptor __get__ memicu query ke database
# Data posts sekarang dimuat

Descriptor memungkinkan ORM untuk mengintervensi akses atribut dan membuat keputusan cerdas tentang kapan dan bagaimana memuat data dari database.

Validasi dan Type Coercion

Descriptor juga memungkinkan validasi otomatis. Anda dapat membuat descriptor custom yang memvalidasi data sebelum disimpan:

class ValidatedField:
    def __init__(self, validator=None):
        self.validator = validator
        self.data = {}
    
    def __set_name__(self, owner, name):
        self.name = name
    
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return self.data.get(id(obj))
    
    def __set__(self, obj, value):
        if self.validator:
            self.validator(value)
        self.data[id(obj)] = value

def positive_number(value):
    if value < 0:
        raise ValueError("Nilai harus positif")

class Product:
    price = ValidatedField(validator=positive_number)

p = Product()
p.price = 100  # OK
p.price = -50  # ValueError!

Performance Implications

Menggunakan descriptor memiliki trade-off performance. Setiap akses atribut melalui descriptor melibatkan overhead—Python harus memeriksa method resolution order dan memanggil method khusus. Untuk hot paths dengan jutaan akses, ini bisa berpengaruh.

Namun, keuntungan yang diberikan—lazy loading, validasi, dirty tracking—biasanya jauh menimbangi overhead ini dalam aplikasi dunia nyata. ORM dapat mengoptimalkan dengan caching dan prefetching strategies yang sophisticated.

Kesimpulan: Descriptor adalah Fondasi

Protokol descriptor adalah salah satu fitur Python yang paling powerful namun sering diabaikan. Memahaminya membuka pandangan baru tentang bagaimana decorator, property, dan ORM bekerja. Ketika Anda berikutnya menggunakan Django ORM atau SQLAlchemy, ingatlah bahwa di balik setiap akses field, ada descriptor yang bekerja keras mengelola kompleksitas interaksi database.

Menguasai descriptor bukan hanya tentang pengetahuan akademis—ini tentang menulis code Python yang lebih baik, lebih ekspresif, dan lebih powerful.