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:
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: 500Setiap 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:
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.02Perhatikan 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:
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.03Kedua 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:
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:
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: 3Perhatikan bagaimana kita menggunakan Student.total_students (bukan self.total_students) di __init__ untuk memperjelas bahwa kita memodifikasi variabel class, bukan membuat variabel instance.
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:
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: 1000000Tanpa 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:
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:
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:
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.0Method setter menerima nilai baru dan bisa memvalidasi atau mentransformasikannya sebelum disimpan.
31.2.4) Menggunakan Property untuk Validasi
Property sangat bagus untuk menerapkan batasan:
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 negativePerhatikan 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:
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 setterIni berguna untuk nilai turunan yang seharusnya dihitung, bukan disimpan.
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:
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 SchoolParameter 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:
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-27Perhatikan 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:
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:543231.3.4) Class Method untuk Menghitung Jumlah Instance
Class method bisa bekerja bersama variabel class untuk melacak informasi tentang semua instance:
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: 031.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:
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: TrueStatic 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:
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.0Panduan:
- 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 Method | Parameter Pertama | Gunakan Saat |
|---|---|---|
| Method Instance | self | Butuh akses ke data instance |
| Class Method | cls | Butuh 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:
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 username31.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:
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__()- untukprint()danstr()__repr__()- untuk REPL danrepr()
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__()- untuklen()__contains__()- untukin__getitem__()- untuk indexingobj[key]__setitem__()- untuk assignmentobj[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:
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: 3Tanpa __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:
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: FalseSekarang 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:
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.66666666666667Perhatikan 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 +:
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 ==:
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: FalseTanpa __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:
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: True31.7.4) Menangani Ketidakcocokan Tipe pada Operator
Saat mengimplementasikan operator, kamu sebaiknya menangani kasus ketika operand lain bukan tipe yang diharapkan:
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.
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:
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:
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:
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 B31.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:
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 CPython 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:
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: 2Bab 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.