Collections #
Modul collections menyediakan tipe data container alternatif yang dirancang untuk menyelesaikan pola-pola umum yang terasa canggung dengan dict, list, atau tuple biasa. Kamu tidak perlu menulis boilerplate untuk menghitung frekuensi, menginisialisasi nilai default, atau membuat antrian dua arah — semuanya sudah tersedia dengan antarmuka yang bersih. Memahami modul ini adalah salah satu cara paling efektif untuk membuat kode Python kamu lebih ringkas dan lebih ekspresif.
Counter — Menghitung Frekuensi #
Counter adalah subclass dari dict yang dirancang khusus untuk menghitung kemunculan elemen. Ia sangat berguna untuk analisis teks, statistik sederhana, atau apapun yang melibatkan perhitungan frekuensi.
from collections import Counter
# ANTI-PATTERN: hitung frekuensi manual dengan dict biasa
kata = ["apel", "jeruk", "apel", "mangga", "jeruk", "apel"]
frekuensi = {}
for buah in kata:
if buah in frekuensi:
frekuensi[buah] += 1
else:
frekuensi[buah] = 0
# BENAR: gunakan Counter
frekuensi = Counter(kata)
print(frekuensi)
# Counter({'apel': 3, 'jeruk': 2, 'mangga': 1})
Membuat Counter #
from collections import Counter
# Dari iterable
c1 = Counter("mississippi")
print(c1) # Counter({'s': 4, 'i': 4, 'p': 2, 'm': 1})
# Dari dictionary
c2 = Counter({"merah": 3, "biru": 2})
# Dari keyword arguments
c3 = Counter(kucing=5, anjing=2, burung=1)
# Counter yang tidak ada menghasilkan 0, bukan KeyError
print(c3["ikan"]) # 0 -- bukan KeyError
Method Berguna Counter #
from collections import Counter
teks = "the quick brown fox jumps over the lazy dog the fox"
c = Counter(teks.split())
# most_common(n) -- n elemen paling sering muncul
print(c.most_common(3))
# [('the', 3), ('fox', 2), ('quick', 1)]
# most_common() tanpa argumen -- semua elemen, urut dari terbanyak
print(c.most_common())
# elements() -- iterator yang mengulangi tiap elemen sebanyak count-nya
print(list(Counter("aab").elements())) # ['a', 'a', 'b']
# update() -- tambah hitungan
c.update(["the", "fox", "fox"])
print(c["the"]) # 4
print(c["fox"]) # 4
# subtract() -- kurangi hitungan (bisa negatif)
c.subtract(["the", "the"])
print(c["the"]) # 2
Aritmatika Counter #
from collections import Counter
a = Counter(apel=3, jeruk=2, mangga=1)
b = Counter(apel=1, jeruk=4, pisang=2)
print(a + b) # Counter({'jeruk': 6, 'apel': 4, 'pisang': 2, 'mangga': 1})
print(a - b) # Counter({'apel': 2, 'mangga': 1}) -- negatif dibuang
print(a & b) # Counter({'jeruk': 2, 'apel': 1}) -- minimum tiap elemen
print(a | b) # Counter({'jeruk': 4, 'apel': 3, 'pisang': 2, 'mangga': 1}) -- maksimum
defaultdict — Dict dengan Nilai Default #
defaultdict menyelesaikan masalah KeyError yang sering terjadi saat kamu mencoba mengakses atau mengupdate key yang belum ada di dictionary.
from collections import defaultdict
# ANTI-PATTERN: cek manual sebelum update
groups = {}
data = [("buah", "apel"), ("sayur", "wortel"), ("buah", "jeruk"), ("sayur", "bayam")]
for kategori, item in data:
if kategori not in groups:
groups[kategori] = []
groups[kategori].append(item)
# BENAR: gunakan defaultdict
groups = defaultdict(list)
for kategori, item in data:
groups[kategori].append(item) # tidak perlu cek dulu
print(dict(groups))
# {'buah': ['apel', 'jeruk'], 'sayur': ['wortel', 'bayam']}
Tipe Default yang Umum Digunakan #
from collections import defaultdict
# defaultdict(list) -- nilai default adalah list kosong
dd_list = defaultdict(list)
dd_list["a"].append(1)
dd_list["a"].append(2)
dd_list["b"].append(3)
print(dict(dd_list)) # {'a': [1, 2], 'b': [3]}
# defaultdict(int) -- nilai default adalah 0 (dari int())
dd_int = defaultdict(int)
for huruf in "banana":
dd_int[huruf] += 1
print(dict(dd_int)) # {'b': 1, 'a': 3, 'n': 2}
# defaultdict(set) -- nilai default adalah set kosong
dd_set = defaultdict(set)
dd_set["warna"].add("merah")
dd_set["warna"].add("biru")
dd_set["ukuran"].add("besar")
print(dict(dd_set)) # {'warna': {'merah', 'biru'}, 'ukuran': {'besar'}}
# defaultdict(dict) -- nilai default adalah dict kosong
dd_dict = defaultdict(dict)
dd_dict["user1"]["nama"] = "Budi"
dd_dict["user1"]["usia"] = 28
print(dict(dd_dict)) # {'user1': {'nama': 'Budi', 'usia': 28}}
Menggunakan Fungsi Kustom sebagai Default #
from collections import defaultdict
# Lambda atau fungsi apapun yang callable
dd = defaultdict(lambda: "tidak diketahui")
dd["nama"] = "Budi"
print(dd["nama"]) # "Budi"
print(dd["kota"]) # "tidak diketahui" -- key baru dibuat dengan nilai default
# Nested defaultdict untuk struktur hierarki
nested = defaultdict(lambda: defaultdict(int))
nested["Jakarta"]["Selatan"] += 5
nested["Jakarta"]["Utara"] += 3
nested["Bandung"]["Barat"] += 2
print(nested["Jakarta"]["Selatan"]) # 5
print(nested["Surabaya"]["Pusat"]) # 0 -- dibuat otomatis
Mengakses key yang tidak ada didefaultdictakan membuat key baru dengan nilai default. Ini berbeda daridict.get()yang hanya mengembalikan nilai tanpa membuat key. Jika kamu hanya ingin membaca nilai tanpa side effect, gunakandd.get(key, default).
deque — Antrian Dua Arah #
deque (double-ended queue) adalah struktur data yang dioptimalkan untuk operasi tambah dan hapus di kedua ujung dengan kompleksitas O(1). Ini jauh lebih efisien dari list untuk operasi di ujung kiri.
from collections import deque
# ANTI-PATTERN: gunakan list sebagai antrian
antrian = []
antrian.append("a") # tambah di kanan -- O(1)
antrian.insert(0, "b") # tambah di kiri -- O(n), lambat untuk list besar!
antrian.pop(0) # hapus di kiri -- O(n), lambat untuk list besar!
# BENAR: gunakan deque
antrian = deque()
antrian.append("a") # tambah di kanan -- O(1)
antrian.appendleft("b") # tambah di kiri -- O(1)
antrian.pop() # hapus di kanan -- O(1)
antrian.popleft() # hapus di kiri -- O(1)
Operasi deque #
from collections import deque
d = deque([1, 2, 3, 4, 5])
# Tambah elemen
d.append(6) # [1, 2, 3, 4, 5, 6]
d.appendleft(0) # [0, 1, 2, 3, 4, 5, 6]
d.extend([7, 8]) # [0, 1, 2, 3, 4, 5, 6, 7, 8]
d.extendleft([-2, -1]) # [-1, -2, 0, 1, 2, 3, 4, 5, 6, 7, 8] -- perhatikan urutan terbalik
# Hapus elemen
d.pop() # hapus dari kanan
d.popleft() # hapus dari kiri
d.remove(3) # hapus kemunculan pertama nilai 3
# Rotasi
d = deque([1, 2, 3, 4, 5])
d.rotate(2) # [4, 5, 1, 2, 3] -- geser 2 ke kanan
d.rotate(-1) # [5, 1, 2, 3, 4] -- geser 1 ke kiri
deque dengan maxlen — Sliding Window #
maxlen membuat deque dengan kapasitas tetap — elemen lama otomatis dibuang saat elemen baru masuk. Sangat berguna untuk implementasi sliding window atau menyimpan N log terakhir.
from collections import deque
# Simpan 5 log terakhir saja
log = deque(maxlen=5)
for i in range(10):
log.append(f"event-{i}")
print(list(log))
# event-0: ['event-0']
# event-1: ['event-0', 'event-1']
# ...
# event-5: ['event-1', 'event-2', 'event-3', 'event-4', 'event-5'] -- event-0 dibuang
# event-9: ['event-5', 'event-6', 'event-7', 'event-8', 'event-9']
# Moving average sederhana
def moving_average(data: list, window: int):
buffer = deque(maxlen=window)
hasil = []
for nilai in data:
buffer.append(nilai)
hasil.append(sum(buffer) / len(buffer))
return hasil
print(moving_average([1, 2, 3, 4, 5, 6], window=3))
# [1.0, 1.5, 2.0, 3.0, 4.0, 5.0]
namedtuple — Tuple dengan Nama Field #
namedtuple membuat tuple yang field-nya bisa diakses dengan nama, bukan hanya indeks. Ini membuat kode lebih mudah dibaca tanpa overhead memori dari class penuh.
from collections import namedtuple
# ANTI-PATTERN: tuple biasa, tidak jelas field apa itu
koordinat = (10.5, -6.2)
print(koordinat[0]) # apa ini? latitude? longitude? x? y?
# BENAR: namedtuple memberi nama yang jelas
Titik = namedtuple("Titik", ["x", "y"])
p = Titik(x=10.5, y=-6.2)
print(p.x) # 10.5 -- jelas ini x
print(p.y) # -6.2
print(p) # Titik(x=10.5, y=-6.2)
Mendefinisikan dan Menggunakan namedtuple #
from collections import namedtuple
# Definisi -- berbagai cara penulisan field
Mahasiswa = namedtuple("Mahasiswa", ["nama", "nim", "ipk"])
Produk = namedtuple("Produk", "id nama harga stok") # bisa pakai string spasi
# Buat instance
mhs = Mahasiswa(nama="Budi", nim="2021001", ipk=3.75)
produk = Produk(id=1, nama="Laptop", harga=15000000, stok=10)
# Akses dengan nama atau indeks
print(mhs.nama) # "Budi"
print(mhs[0]) # "Budi" -- tetap bisa akses via indeks
print(mhs.ipk) # 3.75
# Tuple unpacking tetap berfungsi
nama, nim, ipk = mhs
print(nama) # "Budi"
# _asdict() -- konversi ke dict
print(mhs._asdict())
# {'nama': 'Budi', 'nim': '2021001', 'ipk': 3.75}
# _replace() -- buat salinan dengan beberapa field diubah (immutable!)
mhs_baru = mhs._replace(ipk=3.90)
print(mhs_baru) # Mahasiswa(nama='Budi', nim='2021001', ipk=3.9)
print(mhs.ipk) # 3.75 -- original tidak berubah
namedtuple vs dict vs dataclass #
# namedtuple: immutable, efisien memori, cocok untuk data sederhana
Titik = namedtuple("Titik", ["x", "y"])
p = Titik(1, 2)
# p.x = 3 -- AttributeError: tidak bisa diubah
# dict: mutable, fleksibel, tapi akses kurang ekspresif
p = {"x": 1, "y": 2}
p["x"] = 3 # bisa diubah
# dataclass (Python 3.7+): mutable by default, fitur lebih lengkap
# -- dibahas di artikel Dataclasses
Untuk kebutuhan yang lebih kompleks — seperti nilai default, validasi, atau method — pertimbangkandataclasses.dataclass(dibahas di artikel terpisah).namedtuplepaling tepat untuk data sederhana yang tidak perlu diubah setelah dibuat.
OrderedDict — Dict yang Mengingat Urutan Insersi #
Sejak Python 3.7, dict bawaan sudah mempertahankan urutan insersi, sehingga OrderedDict tidak lagi diperlukan untuk tujuan itu. Namun OrderedDict masih relevan untuk dua kasus spesifik: saat kamu perlu move_to_end() atau saat membandingkan dua dict dengan urutan yang diperhitungkan.
from collections import OrderedDict
# Untuk kebutuhan dict biasa dengan urutan -- dict standar sudah cukup (Python 3.7+)
d = {"a": 1, "b": 2, "c": 3} # urutan terjaga
# OrderedDict masih berguna untuk move_to_end()
od = OrderedDict([("a", 1), ("b", 2), ("c", 3)])
od.move_to_end("a") # pindahkan "a" ke akhir
print(list(od.keys())) # ['b', 'c', 'a']
od.move_to_end("a", last=False) # pindahkan ke awal
print(list(od.keys())) # ['a', 'b', 'c']
# OrderedDict memperhitungkan urutan saat dibandingkan
od1 = OrderedDict([("a", 1), ("b", 2)])
od2 = OrderedDict([("b", 2), ("a", 1)])
print(od1 == od2) # False -- urutan berbeda dianggap tidak sama
d1 = {"a": 1, "b": 2}
d2 = {"b": 2, "a": 1}
print(d1 == d2) # True -- dict biasa tidak mempedulikan urutan
LRU Cache Sederhana dengan OrderedDict #
from collections import OrderedDict
class LRUCache:
"""Least Recently Used cache dengan kapasitas tetap."""
def __init__(self, kapasitas: int):
self.kapasitas = kapasitas
self.cache = OrderedDict()
def get(self, key):
if key not in self.cache:
return None
self.cache.move_to_end(key) # tandai sebagai recently used
return self.cache[key]
def put(self, key, value):
if key in self.cache:
self.cache.move_to_end(key)
self.cache[key] = value
if len(self.cache) > self.kapasitas:
self.cache.popitem(last=False) # hapus yang paling lama tidak dipakai
cache = LRUCache(3)
cache.put("a", 1)
cache.put("b", 2)
cache.put("c", 3)
cache.get("a") # akses "a", pindah ke akhir
cache.put("d", 4) # "b" dibuang karena paling lama tidak dipakai
print(list(cache.cache.keys())) # ['c', 'a', 'd']
ChainMap — Gabungkan Beberapa Dict #
ChainMap menggabungkan beberapa mapping menjadi satu tampilan terpadu tanpa menyalin data. Pencarian dilakukan secara berurutan dari mapping pertama.
from collections import ChainMap
# Contoh kasus: konfigurasi dengan prioritas
default_config = {"debug": False, "port": 8080, "host": "localhost"}
env_config = {"port": 9000} # override dari environment
user_config = {"debug": True} # override dari user
# ChainMap mencari dari kiri ke kanan
config = ChainMap(user_config, env_config, default_config)
print(config["debug"]) # True -- dari user_config
print(config["port"]) # 9000 -- dari env_config
print(config["host"]) # "localhost" -- dari default_config
# Update hanya mempengaruhi mapping pertama
config["timeout"] = 30
print(user_config) # {'debug': True, 'timeout': 30}
print(default_config) # tidak berubah
Ringkasan #
Counteruntuk menghitung frekuensi elemen — lebih ringkas dari dict manual. Gunakanmost_common(n)untuk top-N elemen.defaultdictuntuk dict yang otomatis membuat nilai default saat key baru diakses — hindari boilerplateif key not in d. Ingat: mengakses key yang tidak ada akan membuatnya.dequeuntuk antrian dua arah yang efisien — operasiappendleft()danpopleft()adalah O(1), berbeda darilist.insert(0)yang O(n). Gunakanmaxlenuntuk sliding window atau buffer berukuran tetap.namedtupleuntuk data terstruktur yang immutable dan ringan — lebih ekspresif dari tuple biasa, lebih hemat memori dari class penuh. Gunakan_replace()untuk membuat salinan dengan field berbeda.OrderedDictmasih relevan untukmove_to_end()dan perbandingan dict yang memperhitungkan urutan, meskidictbiasa sudah menjaga urutan insersi sejak Python 3.7.ChainMapuntuk menggabungkan beberapa dict dengan prioritas — berguna untuk sistem konfigurasi berlapis.