Menggunakan Method __setitem__ dan __getitem__ untuk Mengimplementasikan Dynamic List di Python

Misalkan kita memiliki list seperti berikut:

numbers = [1,2,3,4,5]

kita hanya akan bisa mengakses anggota numbers dari numbers[0] sampai numbers[4]. Ketika mencoba untuk mengakses anggota list di luar jangkauan, kita akan mendapat error IndexError:

numbers[9] = 10
---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
<ipython-input-5-2e808a652e84> in <module>()
----> 1 numbers[9] = 10

IndexError: list assignment index out of range

Kita harus secara eksplisit memberi nilai elemen-elemen posisi sebelumnya sampai posisi ke-9, agar kemudian posisi ke-9 tersebut bisa ditempati, misalnya dengan menggunakan fungsi append():

>>> while(len(numbers) < 10): numbers.append(None)
>>> numbers
[1, 2, 3, 4, 5, None, None, None, None, None]
>>> numbers[9] = 10
>>> numbers
[1, 2, 3, 4, 5, None, None, None, None, 10]

Di python, kita bisa mendefinisikan metode object yang akan membuat object tersebut berlaku seperti list atau iterator, diantaranya __getitem__, __setitem__.

Kita mulai dengan membuat class DynamicList, testnya seperti ini:

import unittest
from dynamic_list import DynamicList

class TestDynamicList(unittest.TestCase):

    def test_it_has_attribute_to_store_actual_data(self):
        dynamic_list = DynamicList()

if __name__ == '__main__':
    unittest.main()

Ketika dijalankan:

$ python dynamic_list_test.py 
Traceback (most recent call last):
  File "dynamic_list_test.py", line 2, in <module>
    from dynamic_list import DynamicList
ImportError: No module named 'dynamic_list'

Terjadi error karena modul belum dibuat, jadi kita buat file dynamic_list.py seperti berikut:

class DynamicList:
    def __init__(self):
        pass

kemudian test dijalankan lagi:

$ python dynamic_list_test.py -v
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

Test lulus, kita tambahkan lagi test untuk menguji bahwa object kita memiliki attribut untuk menyimpan data sesungguhnya, sebut saja container

def test_it_has_attribute_to_store_actual_data(self):
    dynamic_list = DynamicList()
    self.assertEqual(dynamic_list.container, [])

hasilnya:

$ python dynamic_list_test.py -v
test_it_has_attribute_to_store_data (__main__.TestDynamicList) ... ERROR

======================================================================
ERROR: test_it_has_attribute_to_store_data (__main__.TestDynamicList)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "dynamic_list_test.py", line 8, in test_it_has_attribute_to_store_data
    self.assertEqual(dynamic_list.container, [])
AttributeError: 'DynamicList' object has no attribute 'container'

----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (errors=1)

implementasinya mudah:

class DynamicList:
    def __init__(self):
        self.container = []

hasilnya:

$ python dynamic_list_test.py -v
test_it_has_attribute_to_store_data (__main__.TestDynamicList) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

Kemudian kita ingin object tersebut bisa diambil datanya seperti mengambil elemen pada list biasa:

def test_it_behaves_like_list_when_fetching_its_item(self):
    dynamic_list = DynamicList()
    dynamic_list.container = [1]
    self.assertEqual(dynamic_list[0], 1)

hasil testnya:

$ python dynamic_list_test.py -v
test_it_behaves_like_list_when_fetching_its_item (__main__.TestDynamicList) ... ERROR
test_it_has_attribute_to_store_data (__main__.TestDynamicList) ... ok

======================================================================
ERROR: test_it_behaves_like_list_when_fetching_its_item (__main__.TestDynamicList)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "dynamic_list_test.py", line 13, in test_it_behaves_like_list_when_fetching_its_item
    self.assertEqual(dynamic_list[0], 1)
TypeError: 'DynamicList' object does not support indexing

----------------------------------------------------------------------
Ran 2 tests in 0.000s

FAILED (errors=1)

galat test yang perlu diperhatikan adalah bagian ini:

    self.assertEqual(dynamic_list[0], 1)
TypeError: 'DynamicList' object does not support indexing

Kita berusaha mengaksesnya seperti list, tetapi object tersebut belum mendukung indexing seperti list biasa. Di sinilah kita perlu mengimplementasikan method __getitem__.

def __getitem__(self, position):
    return self.container[position]

hasilnya:

$ python dynamic_list_test.py -v
test_it_behaves_like_list_when_fetching_its_item (__main__.TestDynamicList) ... ok
test_it_has_attribute_to_store_data (__main__.TestDynamicList) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

Lalu seperti dijanjikan, kita ingin bisa mengambil nilai pada posisi manapun tanpa error. Jika tidak ada nilai pada posisi tersebut, nilai yang diharapkan adalan None:

$ python dynamic_list_test.py -v
test_it_behaves_like_list_when_fetching_its_item (__main__.TestDynamicList) ... ok
test_it_has_attribute_to_store_data (__main__.TestDynamicList) ... ok
test_it_returns_none_when_fetching_nonexistent_item_instead_of_throwing_error (__main__.TestDynamicList) ... ERROR

======================================================================
ERROR: test_it_returns_none_when_fetching_nonexistent_item_instead_of_throwing_error (__main__.TestDynamicList)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "dynamic_list_test.py", line 18, in test_it_returns_none_when_fetching_nonexistent_item_instead_of_throwing_error
    self.assertEqual(dynamic_list[10], None)
  File "/home/wildtype/Documents/Sandbox/dl/dynamic_list.py", line 8, in __getitem__
    return self.container[position]
IndexError: list index out of range

----------------------------------------------------------------------
Ran 3 tests in 0.002s

FAILED (errors=1)

Nah, alih-alih mendapat nilai None kita malah mendapat galat IndexError. Implementasinya, ditangkap saja galat tersebut:

def __getitem__(self, position):
    try:
        return self.container[position]
    except IndexError:
        return None

hasilnya:

$ python dynamic_list_test.py -v
test_it_attribute_to_store_data (__main__.TestDynamicList) ... ok
test_it_behaves_like_list_when_fetching_its_item (__main__.TestDynamicList) ... ok
test_it_returns_none_when_fetching_nonexistent_item_instead_of_throwing_error (__main__.TestDynamicList) ... ok

----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

Sudah bisa mengambil nilai, bagaimana dengan mengatur nilai?

def test_it_behaves_like_list_when_setting_item_value(self):
    dynamic_list = DynamicList()
    dynamic_list.container = [None]
    dynamic_list[0] = 100
    self.assertEqual(dynamic_list[0], 100)

Kita masih belum bisa mengatur nilai elemen object tersebut seperti mengatur element list:

$ python dynamic_list_test.py -v
test_it_behaves_like_list_when_fetching_its_item (__main__.TestDynamicList) ... ok
test_it_behaves_like_list_when_setting_item_value (__main__.TestDynamicList) ... ERROR
test_it_has_attribute_to_store_data (__main__.TestDynamicList) ... ok
test_it_returns_none_when_fetching_nonexistent_item_instead_of_throwing_error (__main__.TestDynamicList) ... ok

======================================================================
ERROR: test_it_behaves_like_list_when_setting_item_value (__main__.TestDynamicList)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "dynamic_list_test.py", line 24, in test_it_behaves_like_list_when_setting_item_value
    dynamic_list[0] = 100
TypeError: 'DynamicList' object does not support item assignment

----------------------------------------------------------------------
Ran 4 tests in 0.001s

FAILED (errors=1)

Nah, kita perlu mengimplementasikan method lainnya, yaitu __setitem__

def __setitem__(self, position, value):
    self.container[position] = value

hasilnya:

$ python dynamic_list_test.py -v
test_it_behaves_like_list_when_fetching_its_item (__main__.TestDynamicList) ... ok
test_it_behaves_like_list_when_setting_item_value (__main__.TestDynamicList) ... ok
test_it_has_attribute_to_store_data (__main__.TestDynamicList) ... ok
test_it_returns_none_when_fetching_nonexistent_item_instead_of_throwing_error (__main__.TestDynamicList) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.000s

OK

Sejauh ini object tersebut hanya bisa diatur nilai elemennya pada posisi yang sebelumnya sudah ada. Kita ingin agar bisa diatur pada posisi manapun:

$ python dynamic_list_test.py -v
test_it_able_to_set_item_value_at_any_position (__main__.TestDynamicList) ... ERROR
test_it_behaves_like_list_when_fetching_its_item (__main__.TestDynamicList) ... ok
test_it_behaves_like_list_when_setting_item_value (__main__.TestDynamicList) ... ok
test_it_has_attribute_to_store_data (__main__.TestDynamicList) ... ok
test_it_returns_none_when_fetching_nonexistent_item_instead_of_throwing_error (__main__.TestDynamicList) ... ok

======================================================================
ERROR: test_it_able_to_set_item_value_at_any_position (__main__.TestDynamicList)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "dynamic_list_test.py", line 30, in test_it_able_to_set_item_value_at_any_position
    dynamic_list[100] = 100
  File "/home/wildtype/Documents/Sandbox/dl/dynamic_list.py", line 13, in __setitem__
    self.container[position] = value
IndexError: list assignment index out of range

----------------------------------------------------------------------
Ran 5 tests in 0.002s

FAILED (errors=1)

Implementasinya, kita perlu mengisi container dengan None sampai setidaknya sepanjang 2 kali posisi index yang ingin diisi. Kita buat 2 kali supaya tidak terlalu sering mengubah ukuran container karena ada ruang tersisa.

def __setitem__(self, position, value):
    try:
        self.container[position] = value
    except IndexError:
        self.container += [None] * position * 2
        self.container[position] = value

hasilnya:

$ python dynamic_list_test.py -v
test_it_able_to_set_item_value_at_any_position (__main__.TestDynamicList) ... ok
test_it_behaves_like_list_when_fetching_its_item (__main__.TestDynamicList) ... ok
test_it_behaves_like_list_when_setting_item_value (__main__.TestDynamicList) ... ok
test_it_has_attribute_to_store_data (__main__.TestDynamicList) ... ok
test_it_returns_none_when_fetching_nonexistent_item_instead_of_throwing_error (__main__.TestDynamicList) ... ok

----------------------------------------------------------------------
Ran 5 tests in 0.001s

OK

Dynamic list tersebut sudah cukup lengkap, bisa diambil dan diatur nilainya pada posisi manapun. Begini hasil akhir implementasi selengkapnya:

class DynamicList:

    def __init__(self):
        self.container = []

    def __getitem__(self, position):
        try:
            return self.container[position]
        except IndexError:
            return None

    def __setitem__(self, position, value):
        try:
            self.container[position] = value
        except IndexError:
            self.container += [None] * position * 2
            self.container[position] = value

Sedangkan kode testnya:

import unittest
from dynamic_list import DynamicList

class TestDynamicList(unittest.TestCase):

    def test_it_has_attribute_to_store_data(self):
        dynamic_list = DynamicList()
        self.assertEqual(dynamic_list.container, [])

    def test_it_behaves_like_list_when_fetching_its_item(self):
        dynamic_list = DynamicList()
        dynamic_list.container = [1]
        self.assertEqual(dynamic_list[0], 1)

    def test_it_returns_none_when_fetching_nonexistent_item_instead_of_throwing_error(self):
        dynamic_list = DynamicList()
        dynamic_list.container = [1]
        self.assertEqual(dynamic_list[10], None)


    def test_it_behaves_like_list_when_setting_item_value(self):
        dynamic_list = DynamicList()
        dynamic_list.container = [None]
        dynamic_list[0] = 100
        self.assertEqual(dynamic_list[0], 100)

    def test_it_able_to_set_item_value_at_any_position(self):
        dynamic_list = DynamicList()
        dynamic_list.container = []
        dynamic_list[100] = 100
        self.assertEqual(dynamic_list[100], 100)


if __name__ == '__main__':
    unittest.main()

Tentu saja, masih banyak yang perlu diimplementasikan supaya bisa lebih mirip list bawaan python, misalnya method append(), atau supaya bisa dihitung jumlah elemennya dengan function len(). Selain itu mungkin kode tersebut bisa direfactor supaya lebih baik. Silakan pembaca mencoba sendiri.

Tinggalkan Balasan

Alamat email Anda tidak akan dipublikasikan.