Python & AI Tutorials Logo
Pemrograman Python

31. Fitur Lanjutan Class

Di Bab 30, kita belajar cara membuat class dasar dengan atribut dan method instance. Sekarang kita akan mengeksplorasi fitur class yang lebih canggih yang memberi kamu kontrol lebih detail atas bagaimana objekmu berperilaku. Fitur-fitur ini memungkinkan kamu membuat class yang terasa seperti tipe bawaan Python, dengan sintaks yang natural untuk operasi seperti penjumlahan, perbandingan, dan indexing.

31.1) Variabel Class vs Variabel Instance

Saat kita membuat atribut dalam sebuah class, ada dua tempat yang secara fundamental berbeda untuk menyimpannya: pada class itu sendiri atau pada instance individual. Memahami perbedaan ini sangat penting untuk menulis kode berorientasi objek yang benar.

31.1.1) Memahami Variabel Instance

Variabel instance adalah atribut yang dimiliki oleh objek tertentu. Setiap instance memiliki salinan variabelnya sendiri yang terpisah. Kita sudah memakai variabel instance sepanjang Bab 30 — yaitu atribut yang kita buat di __init__ menggunakan self:

python
class BankAccount:
    def __init__(self, owner, balance):
        self.owner = owner      # Variabel instance
        self.balance = balance  # Variabel instance
 
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 500)
 
print(account1.balance)  # Output: 1000
print(account2.balance)  # Output: 500

Setiap instance BankAccount punya owner dan balance masing-masing. Mengubah account1.balance tidak memengaruhi account2.balance — keduanya benar-benar independen.

31.1.2) Memahami Variabel Class

Variabel class adalah atribut yang dimiliki oleh class itu sendiri, bukan oleh instance tertentu. Semua instance berbagi variabel class yang sama. Kita mendefinisikan variabel class langsung di body class, di luar method apa pun:

python
class BankAccount:
    interest_rate = 0.02  # Variabel class - dibagikan oleh semua instance
    
    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance
    
    def apply_interest(self):
        self.balance += self.balance * BankAccount.interest_rate
 
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 500)
 
print(account1.interest_rate)  # Output: 0.02
print(account2.interest_rate)  # Output: 0.02
print(BankAccount.interest_rate)  # Output: 0.02

Perhatikan bahwa kita bisa mengakses interest_rate lewat instance (account1.interest_rate) atau lewat class itu sendiri (BankAccount.interest_rate). Keduanya merujuk ke variabel yang sama.

Inilah yang membuat variabel class kuat — ketika kita mengubah variabel class, semua instance melihat perubahannya:

python
class BankAccount:
    interest_rate = 0.02
    
    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance
 
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 500)
 
print(account1.interest_rate)  # Output: 0.02
print(account2.interest_rate)  # Output: 0.02
 
# Ubah variabel class
BankAccount.interest_rate = 0.03
 
print(account1.interest_rate)  # Output: 0.03
print(account2.interest_rate)  # Output: 0.03

Kedua instance langsung melihat suku bunga baru karena mereka semua melihat variabel class yang sama.

31.1.3) Jebakan Shadowing: Saat Variabel Instance Menyembunyikan Variabel Class

Ada perilaku yang halus tapi penting: jika kamu melakukan assignment ke sebuah atribut lewat instance, Python membuat variabel instance yang men-shadow (menyembunyikan) variabel class:

python
class BankAccount:
    interest_rate = 0.02
    
    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance
 
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 500)
 
# Buat variabel instance yang men-shadow variabel class
account1.interest_rate = 0.05
 
print(account1.interest_rate)  # Output: 0.05 (variabel instance)
print(account2.interest_rate)  # Output: 0.02 (variabel class)
print(BankAccount.interest_rate)  # Output: 0.02 (variabel class)

Sekarang account1 punya variabel instance interest_rate miliknya sendiri yang menyembunyikan variabel class. Variabel class masih ada, tetapi account1.interest_rate merujuk ke variabel instance sebagai gantinya. Ini biasanya bukan yang kamu inginkan — jika kamu perlu mengubah variabel class, ubah lewat nama class, bukan lewat instance.

31.1.4) Kegunaan Praktis Variabel Class

Variabel class berguna untuk data yang harus dibagikan di semua instance:

python
class Student:
    school_name = "Python High School"  # Sama untuk semua siswa
    total_students = 0  # Melacak berapa banyak siswa yang ada
    
    def __init__(self, name, grade):
        self.name = name
        self.grade = grade
        Student.total_students += 1  # Tambah saat membuat siswa
    
    def __str__(self):
        return f"{self.name} (Grade {self.grade}) at {Student.school_name}"
 
student1 = Student("Alice", 10)
student2 = Student("Bob", 11)
student3 = Student("Carol", 10)
 
print(student1)  # Output: Alice (Grade 10) at Python High School
print(f"Total students: {Student.total_students}")  # Output: Total students: 3

Perhatikan bagaimana kita menggunakan Student.total_students (bukan self.total_students) di __init__ untuk memperjelas bahwa kita memodifikasi variabel class, bukan membuat variabel instance.

Variabel Class

Didefinisikan di body class

Dibagikan oleh semua instance

Diakses via ClassName.variable

Variabel Instance

Didefinisikan di init dengan self

Unik untuk setiap instance

Diakses via instance.variable

31.2) Mengelola Atribut dengan @property

Terkadang kamu ingin mengontrol apa yang terjadi saat seseorang mengakses atau memodifikasi sebuah atribut. Misalnya, kamu mungkin ingin memvalidasi bahwa nilainya positif, atau menghitung nilai secara on-the-fly alih-alih menyimpannya. Decorator @property di Python memungkinkan kamu menulis method yang terlihat seperti akses atribut sederhana.

31.2.1) Masalahnya: Akses Atribut Langsung Tidak Bisa Memvalidasi

Saat atribut diakses secara langsung, kamu tidak bisa memvalidasi atau mentransformasikan nilai:

python
class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius
 
temp = Temperature(25)
print(temp.celsius)  # Output: 25
 
# Tidak ada yang mencegah kita mengatur temperatur yang secara fisik mustahil
temp.celsius = -500  # Di bawah nol mutlak (-273.15°C)!
print(temp.celsius)  # Output: -500
 
# Atau nilai yang sangat tinggi dan tidak masuk akal
temp.celsius = 1000000
print(temp.celsius)  # Output: 1000000

Tanpa validasi, kita bisa tidak sengaja menetapkan data yang tidak valid, yang menyebabkan bug di kemudian hari dalam program. Kita bisa memakai method seperti get_celsius() dan set_celsius(), tetapi itu bukan gaya Python yang idiomatis. Developer Python mengharapkan akses atribut secara langsung, bukan melalui method getter/setter seperti di Java atau C++.

31.2.2) Menggunakan @property untuk Atribut Terhitung

Decorator @property mengubah sebuah method menjadi "getter" yang diakses seperti atribut:

python
class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius
    
    @property
    def fahrenheit(self):
        """Konversi celsius ke fahrenheit secara on-the-fly"""
        return self.celsius * 9/5 + 32
 
temp = Temperature(25)
print(temp.celsius)  # Output: 25
print(temp.fahrenheit)  # Output: 77.0 (dihitung, bukan disimpan)

Perhatikan kita memanggil temp.fahrenheit tanpa tanda kurung — kelihatannya seperti mengakses atribut, tapi sebenarnya itu memanggil method. Nilai fahrenheit dihitung setiap kali kita mengaksesnya, jadi selalu sinkron dengan celsius:

python
temp = Temperature(0)
print(temp.fahrenheit)  # Output: 32.0
 
temp.celsius = 100
print(temp.fahrenheit)  # Output: 212.0 (terupdate otomatis)

31.2.3) Menambahkan Setter dengan @property_name.setter

Untuk mengizinkan penyetelan sebuah property, kita menambahkan method setter menggunakan decorator @property_name.setter:

python
class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius
    
    @property
    def fahrenheit(self):
        return self.celsius * 9/5 + 32
    
    @fahrenheit.setter
    def fahrenheit(self, value):
        """Konversi fahrenheit ke celsius saat melakukan set"""
        self.celsius = (value - 32) * 5/9
 
temp = Temperature(0)
print(temp.celsius)  # Output: 0
print(temp.fahrenheit)  # Output: 32.0
 
# Set temperatur menggunakan fahrenheit
temp.fahrenheit = 212
print(temp.celsius)  # Output: 100.0
print(temp.fahrenheit)  # Output: 212.0

Method setter menerima nilai baru dan bisa memvalidasi atau mentransformasikannya sebelum disimpan.

31.2.4) Menggunakan Property untuk Validasi

Property sangat bagus untuk menerapkan batasan:

python
class BankAccount:
    def __init__(self, owner, balance):
        self.owner = owner
        self._balance = balance  # Garis bawah menyarankan "untuk penggunaan internal"
    
    @property
    def balance(self):
        """Ambil saldo saat ini"""
        return self._balance
    
    @balance.setter
    def balance(self, value):
        """Set saldo, tapi hanya jika tidak negatif"""
        if value < 0:
            raise ValueError("Balance cannot be negative")
        self._balance = value
 
account = BankAccount("Alice", 1000)
print(account.balance)  # Output: 1000
 
account.balance = 1500  # Berjalan baik
print(account.balance)  # Output: 1500
 
# Ini memunculkan error
account.balance = -100
# Output: ValueError: Balance cannot be negative

Perhatikan konvensi penamaan: kita menyimpan nilai sebenarnya di _balance (dengan garis bawah di depan) dan mengeksposnya melalui property balance. Garis bawah adalah konvensi Python yang menyarankan "ini detail implementasi internal," walaupun atribut tersebut secara teknis masih bisa diakses. Pola ini memungkinkan kita mengontrol akses lewat property sambil menjaga penyimpanan sebenarnya tetap terpisah.

31.2.5) Property Read-Only

Jika kamu mendefinisikan property tanpa setter, property itu menjadi read-only:

python
class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    @property
    def area(self):
        """Property read-only yang dihitung"""
        return self.width * self.height
 
rect = Rectangle(5, 3)
print(rect.area)  # Output: 15
 
rect.width = 10
print(rect.area)  # Output: 30 (terupdate otomatis)
 
# Mencoba melakukan set area akan memunculkan error
rect.area = 50
# Output: AttributeError: property 'area' of 'Rectangle' object has no setter

Ini berguna untuk nilai turunan yang seharusnya dihitung, bukan disimpan.

@property Decorator

Mengubah method menjadi getter

Diakses seperti atribut

Bisa menghitung nilai secara on-the-fly

@property_name.setter

Menambahkan setter untuk property

Bisa memvalidasi sebelum menyimpan

Bisa mentransformasi nilai

31.3) Class Method dengan @classmethod

Terkadang kamu butuh method yang bekerja dengan class itu sendiri alih-alih dengan instance. Class method menerima class sebagai argumen pertamanya (secara konvensi diberi nama cls) alih-alih instance (self).

31.3.1) Mendefinisikan Class Method

Kita membuat class method menggunakan decorator @classmethod:

python
class Student:
    school_name = "Python High School"
    
    def __init__(self, name, grade):
        self.name = name
        self.grade = grade
    
    @classmethod
    def get_school_name(cls):
        """Class method - menerima class, bukan instance"""
        return cls.school_name
 
# Panggil pada class itu sendiri
print(Student.get_school_name())  # Output: Python High School
 
# Bisa juga dipanggil pada instance (tapi cls tetap class)
student = Student("Alice", 10)
print(student.get_school_name())  # Output: Python High School

Parameter cls otomatis menerima class, sama seperti self otomatis menerima instance pada method biasa.

31.3.2) Konstruktor Alternatif dengan Class Method

Salah satu penggunaan paling umum untuk class method adalah membuat konstruktor alternatif — cara lain untuk membuat instance:

python
class Date:
    def __init__(self, year, month, day):
        self.year = year
        self.month = month
        self.day = day
    
    @classmethod
    def from_string(cls, date_string):
        """Buat Date dari string seperti '2024-12-27'"""
        year, month, day = date_string.split('-')
        return cls(int(year), int(month), int(day))
    
    @classmethod
    def today(cls):
        """Buat Date untuk hari ini (contoh yang disederhanakan)"""
        # Dalam kode nyata, kamu akan memakai modul datetime
        return cls(2024, 12, 27)
    
    def __str__(self):
        return f"{self.year}-{self.month:02d}-{self.day:02d}"
 
# Konstruktor normal
date1 = Date(2024, 12, 27)
print(date1)  # Output: 2024-12-27
 
# Konstruktor alternatif dari string
date2 = Date.from_string("2024-12-27")
print(date2)  # Output: 2024-12-27
 
# Konstruktor alternatif untuk hari ini
date3 = Date.today()
print(date3)  # Output: 2024-12-27

Perhatikan bagaimana from_string dan today sama-sama mengembalikan cls(...) — ini membuat instance baru dari class tersebut. Menggunakan cls alih-alih hardcode Date membuat kode bekerja dengan benar pada subclass (kita akan belajar tentang inheritance di Bab 32).

31.3.3) Class Method untuk Pola Factory

Class method berguna untuk membuat instance dengan konfigurasi yang berbeda:

python
class DatabaseConnection:
    def __init__(self, host, port, database, username):
        self.host = host
        self.port = port
        self.database = database
        self.username = username
    
    @classmethod
    def for_development(cls):
        """Buat koneksi yang dikonfigurasi untuk development"""
        return cls("localhost", 5432, "dev_db", "dev_user")
    
    @classmethod
    def for_production(cls):
        """Buat koneksi yang dikonfigurasi untuk production"""
        return cls("prod.example.com", 5432, "prod_db", "prod_user")
    
    def __str__(self):
        return f"Connection to {self.database} at {self.host}:{self.port}"
 
# Mudah untuk membuat koneksi yang sudah terkonfigurasi
dev_conn = DatabaseConnection.for_development()
prod_conn = DatabaseConnection.for_production()
 
print(dev_conn)  # Output: Connection to dev_db at localhost:5432
print(prod_conn)  # Output: Connection to prod_db at prod.example.com:5432

31.3.4) Class Method untuk Menghitung Jumlah Instance

Class method bisa bekerja bersama variabel class untuk melacak informasi tentang semua instance:

python
class Product:
    total_products = 0
    
    def __init__(self, name, price):
        self.name = name
        self.price = price
        Product.total_products += 1
    
    @classmethod
    def get_total_products(cls):
        """Kembalikan jumlah total produk yang dibuat"""
        return cls.total_products
    
    @classmethod
    def reset_count(cls):
        """Reset penghitung produk"""
        cls.total_products = 0
 
product1 = Product("Laptop", 999)
product2 = Product("Mouse", 25)
product3 = Product("Keyboard", 75)
 
print(Product.get_total_products())  # Output: 3
 
Product.reset_count()
print(Product.get_total_products())  # Output: 0

31.4) Static Method dengan @staticmethod

Static method adalah method yang tidak menerima instance (self) atau class (cls) sebagai argumen pertamanya. Mereka hanyalah fungsi biasa yang kebetulan didefinisikan di dalam class karena secara logis berhubungan dengan class tersebut.

31.4.1) Mendefinisikan Static Method

Kita membuat static method menggunakan decorator @staticmethod:

python
class MathUtils:
    @staticmethod
    def is_even(number):
        """Periksa apakah sebuah angka genap"""
        return number % 2 == 0
    
    @staticmethod
    def is_prime(number):
        """Periksa apakah sebuah angka prima (disederhanakan)"""
        if number < 2:
            return False
        for i in range(2, int(number ** 0.5) + 1):
            if number % i == 0:
                return False
        return True
 
# Panggil static method pada class
print(MathUtils.is_even(4))  # Output: True
print(MathUtils.is_even(7))  # Output: False
print(MathUtils.is_prime(17))  # Output: True
print(MathUtils.is_prime(18))  # Output: False
 
# Bisa juga dipanggil pada instance (tapi fungsinya sama)
utils = MathUtils()
print(utils.is_even(10))  # Output: True

Static method tidak perlu akses ke data instance atau class — mereka adalah fungsi utilitas yang berdiri sendiri.

31.4.2) Kapan Memakai Static Method vs Class Method vs Instance Method

Begini cara memilihnya:

python
class Temperature:
    # Variabel class
    absolute_zero_celsius = -273.15
    
    def __init__(self, celsius):
        self.celsius = celsius
    
    # Method instance - butuh akses ke data instance (self)
    def to_fahrenheit(self):
        return self.celsius * 9/5 + 32
    
    # Class method - butuh akses ke data class (cls)
    @classmethod
    def get_absolute_zero(cls):
        return cls.absolute_zero_celsius
    
    # Static method - tidak butuh data instance atau class
    @staticmethod
    def celsius_to_kelvin(celsius):
        return celsius + 273.15
    
    @staticmethod
    def fahrenheit_to_celsius(fahrenheit):
        return (fahrenheit - 32) * 5/9
 
temp = Temperature(25)
 
# Method instance - memakai data instance
print(temp.to_fahrenheit())  # Output: 77.0
 
# Class method - memakai data class
print(Temperature.get_absolute_zero())  # Output: -273.15
 
# Static method - hanya fungsi utilitas
print(Temperature.celsius_to_kelvin(25))  # Output: 298.15
print(Temperature.fahrenheit_to_celsius(77))  # Output: 25.0

Panduan:

  • Gunakan method instance saat kamu butuh akses ke atribut instance (self)
  • Gunakan class method saat kamu butuh akses ke atribut class atau ingin konstruktor alternatif (cls)
  • Gunakan static method saat kamu tidak butuh akses ke data instance atau class, tapi fungsi tersebut secara logis terkait dengan class

Catatan: Static method bisa saja menjadi fungsi standalone, tetapi menaruhnya di dalam class mengelompokkan fungsionalitas terkait dan menghindari mengotori namespace global.

Tipe MethodParameter PertamaGunakan Saat
Method InstanceselfButuh akses ke data instance
Class MethodclsButuh akses ke data class atau konstruktor alternatif
Static Method(none)Fungsi utilitas yang terkait dengan class

31.4.3) Contoh Praktis: Utilitas Validasi

Static method sangat bagus untuk validasi dan fungsi utilitas:

python
class User:
    def __init__(self, username, password):
        if not User.is_valid_username(username):
            raise ValueError("Invalid username")
        if not User.is_valid_password(password):
            raise ValueError("Invalid password")
        
        self.username = username
        self._password = password
    
    @staticmethod
    def is_valid_username(username):
        """Periksa apakah username memenuhi persyaratan"""
        return len(username) >= 3 and username.isalnum()
        
    @staticmethod
    def is_valid_password(password):
        """Periksa apakah password memenuhi persyaratan keamanan"""
        return len(password) >= 8 and any(c.isdigit() for c in password)
 
# Method validasi ini bisa digunakan secara independen
print(User.is_valid_username("alice123"))  # Output: True
print(User.is_valid_username("ab"))  # Output: False
print(User.is_valid_password("pass1234"))  # Output: True
 
# Dan mereka bisa digunakan di method mana pun pada class
try:
    user = User("ab", "short")
except ValueError as e:
    print(f"Error: {e}")  # Output: Error: Invalid username

31.5) Memahami Special Method (Magic Method)

Special method (juga disebut magic method atau dunder method karena memiliki double underscore) di Python memungkinkan kamu menyesuaikan bagaimana objekmu berperilaku dengan operasi bawaan Python. Kita sudah memakai __init__, __str__, dan __repr__ di Bab 30. Sekarang kita akan mengeksplorasi lebih banyak lagi.

31.5.1) Apa yang Dilakukan Special Method

Special method dipanggil secara otomatis oleh Python saat kamu memakai sintaks tertentu atau fungsi bawaan:

python
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __str__(self):
        return f"Point({self.x}, {self.y})"
 
point = Point(3, 4)
 
# Saat kamu memanggil print(), Python memanggil __str__()
print(point)  # Output: Point(3, 4)
# Ini setara dengan: print(point.__str__())

Special method memungkinkan kamu membuat class berperilaku seperti tipe bawaan. Misalnya, kamu bisa membuat objekmu:

  • Mendukung operasi aritmetika (+, -, *, /)
  • Bisa dibandingkan (<, >, ==)
  • Bekerja dengan len(), in, dan indexing
  • Bertindak seperti container atau sequence

Kita akan mengeksplorasi ini secara detail di bagian-bagian berikutnya.

31.5.2) Kategori Special Method yang Umum

Berikut kategori utama special method:

Representasi String (bagaimana objek ditampilkan):

  • __str__() - untuk print() dan str()
  • __repr__() - untuk REPL dan repr()

Perbandingan (membandingkan objek):

  • __eq__() - untuk ==
  • __ne__() - untuk !=
  • __lt__() - untuk <
  • __le__() - untuk <=
  • __gt__() - untuk >
  • __ge__() - untuk >=

Aritmetika (operasi matematika):

  • __add__() - untuk +
  • __sub__() - untuk -
  • __mul__() - untuk *
  • __truediv__() - untuk /

Container/Sequence (perilaku mirip koleksi):

  • __len__() - untuk len()
  • __contains__() - untuk in
  • __getitem__() - untuk indexing obj[key]
  • __setitem__() - untuk assignment obj[key] = value

Kita akan membahas ini secara mendalam di bagian-bagian berikut.

31.6) Contoh 1: Antarmuka Koleksi (len, contains)

Mari buat sebuah class yang mengelola koleksi item dan membuatnya bekerja dengan fungsi bawaan len() dan operator in di Python.

31.6.1) Mengimplementasikan len untuk len()

Special method __len__() dipanggil saat kamu memakai len() pada objekmu:

python
class ShoppingCart:
    def __init__(self):
        self.items = []
    
    def add_item(self, item):
        self.items.append(item)
    
    def __len__(self):
        """Kembalikan jumlah item di keranjang"""
        return len(self.items)
 
cart = ShoppingCart()
cart.add_item("Apple")
cart.add_item("Banana")
cart.add_item("Orange")
 
# len() memanggil __len__()
print(len(cart))  # Output: 3

Tanpa __len__(), memanggil len(cart) akan memunculkan TypeError. Dengan mengimplementasikannya, ShoppingCart kita bekerja seperti koleksi bawaan.

31.6.2) Mengimplementasikan contains untuk Operator in

Special method __contains__() dipanggil saat kamu memakai operator in:

python
class ShoppingCart:
    def __init__(self):
        self.items = []
    
    def add_item(self, item):
        self.items.append(item)
    
    def __len__(self):
        return len(self.items)
    
    def __contains__(self, item):
        """Periksa apakah sebuah item ada di keranjang"""
        return item in self.items
 
cart = ShoppingCart()
cart.add_item("Apple")
cart.add_item("Banana")
 
# operator in memanggil __contains__()
print("Apple" in cart)  # Output: True
print("Orange" in cart)  # Output: False

Sekarang keranjang kita mendukung sintaks Python yang natural untuk pengecekan keanggotaan.

31.6.3) Membangun Class Koleksi yang Lebih Lengkap

Mari buat class koleksi yang lebih realistis yang melacak nilai siswa:

python
class GradeBook:
    def __init__(self):
        self.grades = {}  # student_name: list of grades
    
    def add_grade(self, student, grade):
        """Tambahkan nilai untuk seorang siswa"""
        if student not in self.grades:
            self.grades[student] = []
        self.grades[student].append(grade)
    
    def __len__(self):
        """Kembalikan jumlah siswa"""
        return len(self.grades)
    
    def __contains__(self, student):
        """Periksa apakah seorang siswa punya nilai apa pun"""
        return student in self.grades
    
    def get_average(self, student):
        """Ambil rata-rata nilai seorang siswa"""
        if student not in self:
            return None
        grades = self.grades[student]
        return sum(grades) / len(grades)
    
    def __str__(self):
        return f"GradeBook with {len(self)} students"
 
gradebook = GradeBook()
gradebook.add_grade("Alice", 85)
gradebook.add_grade("Alice", 90)
gradebook.add_grade("Bob", 78)
gradebook.add_grade("Bob", 82)
gradebook.add_grade("Bob", 88)
 
print(gradebook)  # Output: GradeBook with 2 students
print(len(gradebook))  # Output: 2
 
print("Alice" in gradebook)  # Output: True
print("Carol" in gradebook)  # Output: False
 
print(f"Alice's average: {gradebook.get_average('Alice')}")  # Output: Alice's average: 87.5
print(f"Bob's average: {gradebook.get_average('Bob')}")  # Output: Bob's average: 82.66666666666667

Perhatikan bagaimana get_average() memakai if student not in self — ini memanggil method __contains__() kita, membuat kode terbaca natural.

31.7) Contoh 2: Operator Overloading (add, eq, lt)

Operator overloading berarti mendefinisikan apa yang dilakukan operator seperti +, ==, dan < untuk class kustommu. Ini membuat objekmu bekerja secara natural dengan sintaks Python.

31.7.1) Mengimplementasikan add untuk Penjumlahan

Special method __add__() dipanggil saat kamu memakai operator +:

python
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __add__(self, other):
        """Jumlahkan dua vector"""
        return Vector(self.x + other.x, self.y + other.y)
    
    def __str__(self):
        return f"Vector({self.x}, {self.y})"
 
v1 = Vector(1, 2)
v2 = Vector(3, 4)
 
# operator + memanggil __add__()
v3 = v1 + v2
print(v3)  # Output: Vector(4, 6)

Saat Python melihat v1 + v2, ia memanggil v1.__add__(v2). Method __add__() milik operand kiri menerima operand kanan sebagai argumen.

31.7.2) Mengimplementasikan eq untuk Kesetaraan

Special method __eq__() dipanggil saat kamu memakai operator ==:

python
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)
    
    def __eq__(self, other):
        """Periksa apakah dua vector sama"""
        return self.x == other.x and self.y == other.y
    
    def __str__(self):
        return f"Vector({self.x}, {self.y})"
 
v1 = Vector(1, 2)
v2 = Vector(1, 2)
v3 = Vector(3, 4)
 
# operator == memanggil __eq__()
print(v1 == v2)  # Output: True
print(v1 == v3)  # Output: False

Tanpa __eq__(), Python membandingkan identitas objek (apakah mereka objek yang sama di memori), bukan nilainya. Dengan __eq__(), kita mendefinisikan apa arti “setara” untuk class kita.

31.7.3) Mengimplementasikan Operator Perbandingan

Mari implementasikan operator perbandingan untuk class Money:

python
class Money:
    def __init__(self, amount):
        self.amount = amount
    
    def __eq__(self, other):
        """Periksa apakah jumlahnya sama"""
        return self.amount == other.amount
    
    def __lt__(self, other):
        """Periksa apakah jumlah ini lebih kecil dari other"""
        return self.amount < other.amount
    
    def __le__(self, other):
        """Periksa apakah jumlah ini lebih kecil atau sama dengan other"""
        return self.amount <= other.amount
    
    def __gt__(self, other):
        """Periksa apakah jumlah ini lebih besar dari other"""
        return self.amount > other.amount
    
    def __ge__(self, other):
        """Periksa apakah jumlah ini lebih besar atau sama dengan other"""
        return self.amount >= other.amount
    
    def __str__(self):
        return f"${self.amount:.2f}"
 
price1 = Money(10.50)
price2 = Money(15.75)
price3 = Money(10.50)
 
print(price1 == price3)  # Output: True
print(price1 < price2)  # Output: True
print(price1 <= price3)  # Output: True
print(price2 > price1)  # Output: True
print(price2 >= price1)  # Output: True

31.7.4) Menangani Ketidakcocokan Tipe pada Operator

Saat mengimplementasikan operator, kamu sebaiknya menangani kasus ketika operand lain bukan tipe yang diharapkan:

python
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __add__(self, other):
        """Tambahkan dua vector atau tambahkan skalar ke kedua komponen"""
        if isinstance(other, Vector):
            return Vector(self.x + other.x, self.y + other.y)
        elif isinstance(other, (int, float)):
            return Vector(self.x + other, self.y + other)
        else:
            return NotImplemented  # Biarkan Python mencoba other.__radd__(self)
    
    def __eq__(self, other):
        if not isinstance(other, Vector):
            return False
        return self.x == other.x and self.y == other.y
    
    def __str__(self):
        return f"Vector({self.x}, {self.y})"
 
v1 = Vector(1, 2)
v2 = Vector(3, 4)
 
print(v1 + v2)  # Output: Vector(4, 6) (vector addition)
print(v1 + 5)  # Output: Vector(6, 7) (scalar addition)
 
print(v1 == v2)  # Output: False
print(v1 == "not a vector")  # Output: False (no error)

Mengembalikan NotImplemented (konstanta bawaan khusus) memberi tahu Python untuk mencoba operasi refleksi pada operand lain. Ini penting agar operator bekerja dengan benar untuk tipe yang berbeda.

Operator Overloading

Operator Aritmetika

Operator Perbandingan

add untuk +

sub untuk -

mul untuk *

truediv untuk /

eq untuk ==

lt untuk <

le untuk <=

gt untuk >

ge untuk >=

31.8) Contoh 3: Akses Sequence (getitem, setitem)

Special method __getitem__() dan __setitem__() memungkinkan kamu memakai sintaks indexing (obj[key]) dengan class kustommu. Ini membuat objekmu berperilaku seperti list, dictionary, atau sequence lainnya.

31.8.1) Mengimplementasikan getitem untuk Indexing

Method __getitem__() dipanggil saat kamu memakai tanda kurung siku untuk mengakses item:

python
class Playlist:
    def __init__(self, name):
        self.name = name
        self.songs = []
    
    def add_song(self, song):
        self.songs.append(song)
    
    def __getitem__(self, index):
        """Ambil lagu berdasarkan index"""
        return self.songs[index]
    
    def __len__(self):
        return len(self.songs)
 
playlist = Playlist("My Favorites")
playlist.add_song("Song A")
playlist.add_song("Song B")
playlist.add_song("Song C")
 
# Indexing memanggil __getitem__()
print(playlist[0])  # Output: Song A
print(playlist[1])  # Output: Song B
print(playlist[-1])  # Output: Song C (negative indexing works!)

Karena kita mendelegasikan ke self.songs[index], semua fitur indexing list bekerja otomatis: index positif, index negatif, bahkan memunculkan IndexError untuk index yang tidak valid.

31.8.2) Mendukung Slicing dengan getitem

Method __getitem__() yang sama juga menangani slicing:

python
class Playlist:
    def __init__(self, name):
        self.name = name
        self.songs = []
    
    def add_song(self, song):
        self.songs.append(song)
    
    def __getitem__(self, index):
        """Ambil lagu berdasarkan index atau slice"""
        return self.songs[index]
    
    def __len__(self):
        return len(self.songs)
 
playlist = Playlist("My Favorites")
playlist.add_song("Song A")
playlist.add_song("Song B")
playlist.add_song("Song C")
playlist.add_song("Song D")
 
# Slicing juga memanggil __getitem__()
print(playlist[1:3])  # Output: ['Song B', 'Song C']
print(playlist[:2])  # Output: ['Song A', 'Song B']
print(playlist[::2])  # Output: ['Song A', 'Song C']

Saat kamu memakai slicing, Python mengirimkan objek slice ke __getitem__(). Dengan mendelegasikan ke self.songs[index], kita otomatis mendukung semua sintaks slice.

31.8.3) Mengimplementasikan setitem untuk Assignment

Method __setitem__() dipanggil saat kamu melakukan assignment ke sebuah index:

python
class Playlist:
    def __init__(self, name):
        self.name = name
        self.songs = []
    
    def add_song(self, song):
        self.songs.append(song)
    
    def __getitem__(self, index):
        return self.songs[index]
    
    def __setitem__(self, index, value):
        """Ganti lagu pada index tertentu"""
        self.songs[index] = value
    
    def __len__(self):
        return len(self.songs)
 
playlist = Playlist("My Favorites")
playlist.add_song("Song A")
playlist.add_song("Song B")
playlist.add_song("Song C")
 
print(playlist[1])  # Output: Song B
 
# Assignment memanggil __setitem__()
playlist[1] = "New Song B"
print(playlist[1])  # Output: New Song B

31.8.4) Membuat Objek Bisa Di-iterasi dengan getitem

Efek samping yang menarik: jika kamu mengimplementasikan __getitem__() dengan index integer mulai dari 0, objekmu otomatis menjadi iterable:

python
class Playlist:
    def __init__(self, name):
        self.name = name
        self.songs = []
    
    def add_song(self, song):
        self.songs.append(song)
    
    def __getitem__(self, index):
        return self.songs[index]
    
    def __len__(self):
        return len(self.songs)
 
playlist = Playlist("My Favorites")
playlist.add_song("Song A")
playlist.add_song("Song B")
playlist.add_song("Song C")
 
# for loop bekerja otomatis!
for song in playlist:
    print(song)
# Output:
# Song A
# Song B
# Song C

Python mencoba mengiterasi dengan memanggil __getitem__(0), lalu __getitem__(1), dan seterusnya sampai mendapatkan IndexError. Ini adalah protokol iterasi yang lebih lama — kita akan belajar protokol iterator modern di Bab 35.

31.8.5) Akses Mirip Dictionary dengan Key String

__getitem__() dan __setitem__() bekerja dengan tipe key apa pun, bukan hanya integer:

python
class ScoreBoard:
    def __init__(self):
        self.scores = {}
    
    def __getitem__(self, player_name):
        """Ambil skor untuk seorang pemain"""
        return self.scores.get(player_name, 0)
    
    def __setitem__(self, player_name, score):
        """Set skor untuk seorang pemain"""
        self.scores[player_name] = score
    
    def __contains__(self, player_name):
        return player_name in self.scores
    
    def __len__(self):
        return len(self.scores)
 
scoreboard = ScoreBoard()
 
# Set skor menggunakan key string
scoreboard["Alice"] = 100
scoreboard["Bob"] = 85
 
# Update skor
scoreboard["Alice"] = 120
 
# Ambil skor
print(scoreboard["Alice"])  # Output: 120
print(scoreboard["Bob"])    # Output: 85
print(scoreboard["Carol"])  # Output: 0
 
print("Alice" in scoreboard)  # Output: True
print(len(scoreboard))  # Output: 2

Akses Sequence

getitem

setitem

Dipanggil untuk obj[key]

Menangani indexing

Menangani slicing

Membuat objek iterable

Dipanggil untuk obj[key] = value

Mengaktifkan assignment

Bisa memvalidasi nilai


Bab ini sudah menunjukkan cara membuat class yang canggih dan terintegrasi mulus dengan sintaks Python. Dengan mengimplementasikan variabel class, property, class method, static method, dan special method, kamu bisa membuat class kustom berperilaku seperti tipe bawaan. Di Bab 32, kita akan mengeksplorasi inheritance dan polymorphism, yang memungkinkan kamu membangun hierarki class yang saling berhubungan, berbagi perilaku, dan memperluasnya.

© 2025. Primesoft Co., Ltd.
support@primesoft.ai