Dictionary #
Dictionary adalah struktur data inti Python untuk menyimpan pasangan kunci-nilai (key-value pairs) dengan akses O(1) berdasarkan kunci. Sejak Python 3.7, dictionary menjamin urutan penyisipan — artinya saat kamu iterasi, elemen keluar dalam urutan yang sama seperti saat dimasukkan. Dictionary sangat serbaguna: digunakan untuk konfigurasi, caching, pengelompokan data, counting, dan sebagai alternatif ringan dari kelas untuk data terstruktur. Memahami idiom dan varian dictionary yang tepat — defaultdict, Counter, TypedDict — membuat kode kamu jauh lebih ekspresif dan bebas boilerplate.
Membuat Dictionary #
# Cara-cara membuat dictionary
kosong = {}
kosong2 = dict()
# Literal — cara paling umum
pengguna = {
"nama": "Budi Santoso",
"umur": 28,
"email": "[email protected]",
"aktif": True,
}
# Dari keyword arguments — hanya untuk kunci yang valid sebagai identifier Python
konfigurasi = dict(host="localhost", port=5432, ssl=False)
# Dari list of tuples
dari_tuple = dict([("a", 1), ("b", 2), ("c", 3)])
# Dict comprehension
kuadrat = {x: x**2 for x in range(1, 6)}
print(kuadrat) # → {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
# Dict dari dua list menggunakan zip
kunci = ["nama", "umur", "kota"]
nilai = ["Budi", 28, "Jakarta"]
profil = dict(zip(kunci, nilai))
print(profil) # → {'nama': 'Budi', 'umur': 28, 'kota': 'Jakarta'}
# Inisialisasi dengan nilai default untuk semua kunci
skor_awal = dict.fromkeys(["matematika", "fisika", "kimia"], 0)
print(skor_awal) # → {'matematika': 0, 'fisika': 0, 'kimia': 0}
Mengakses Nilai #
Cara mengakses nilai adalah perbedaan paling penting yang perlu dipahami — ada dua cara, dan masing-masing tepat untuk situasi yang berbeda:
data = {"nama": "Budi", "umur": 28, "kota": "Jakarta"}
# Akses via [] — melempar KeyError jika kunci tidak ada
print(data["nama"]) # → Budi
# print(data["email"]) # → KeyError: 'email'
# Akses via get() — kembalikan None (atau default) jika kunci tidak ada
print(data.get("email")) # → None (tidak error)
print(data.get("email", "kosong")) # → kosong (nilai default eksplisit)
print(data.get("nama", "anonim")) # → Budi (kunci ada, default diabaikan)
# ANTI-PATTERN: akses langsung untuk kunci yang mungkin tidak ada
def tampilkan_profil(pengguna: dict) -> None:
print(pengguna["nama"]) # crash jika "nama" tidak ada
print(pengguna["telepon"]) # hampir pasti crash
# BENAR: gunakan get() dengan default yang masuk akal
def tampilkan_profil(pengguna: dict) -> None:
print(pengguna.get("nama", "Pengguna"))
print(pengguna.get("telepon", "Tidak tersedia"))
# ANTI-PATTERN: pola cek-then-access yang verbose
if "email" in pengguna:
email = pengguna["email"]
else:
email = "[email protected]"
# BENAR: get() lebih ringkas
email = pengguna.get("email", "[email protected]")
Cara Kerja Pencarian Kunci di Balik Layar #
Di balik layar, dictionary Python diimplementasikan menggunakan Hash Table. Mulai dari Python 3.6+ (dan resmi menjadi standar di 3.7+), struktur tabel hash dioptimalkan untuk menghemat memori dan menjaga urutan penyisipan (insertion-ordered). Struktur ini memisahkan tabel menjadi array indeks yang jarang (sparse indices array) dan array entri yang padat (dense entries array).
Berikut adalah diagram alur pencarian kunci (key lookup) dan hubungannya dengan struktur memori modern Python:
flowchart TD
Start["Mulai Pencarian Kunci: key"] --> Hash["Hitung Nilai Hash: hash(key)"]
Hash --> Mask["Petakan ke Indeks: hash & mask"]
Mask --> LookSparse["Cek Slot di Sparse Indices Array"]
LookSparse -->|Slot Kosong -1| NotFound["Kunci Tidak Ditemukan (KeyError / Default)"]
LookSparse -->|Slot Berisi idx| GetEntry["Ambil Entri ke-idx di Dense Entries Array"]
GetEntry --> Compare{"Apakah entries[idx].key == key?"}
Compare -->|Ya| Found["Kunci Cocok: Kembalikan entries[idx].value"]
Compare -->|"Tidak (Kolisi Hash)"| Probe["Hitung Indeks Baru (Probing / Open Addressing)"]
Probe --> LookSparse
Penjelasan singkat tentang struktur penyimpanan modern:
- Sparse Indices Array: Array kecil berisi integer yang berfungsi sebagai pointer indeks (misalnya
[-1, 0, -1, 1, -1]). - Dense Entries Array: Array data padat yang menyimpan pasangan kunci-nilai sebenarnya dalam urutan penyisipan:
entries[0] = (hash_code, key1, value1)entries[1] = (hash_code, key2, value2)
Pemisahan ini membuat dictionary Python 3.7+ menggunakan memori hingga 30% lebih sedikit dibandingkan versi sebelumnya, sekaligus mempertahankan kecepatan akses rata-rata sebesar (O(1)).
Modifikasi Dictionary #
data = {"a": 1, "b": 2}
# Tambah atau update kunci
data["c"] = 3 # tambah kunci baru
data["a"] = 10 # update nilai kunci yang sudah ada
print(data) # → {'a': 10, 'b': 2, 'c': 3}
# setdefault() — tambah kunci HANYA jika belum ada
data.setdefault("d", 0) # tambah 'd': 0 karena belum ada
data.setdefault("a", 99) # 'a' sudah ada, tidak diubah
print(data) # → {'a': 10, 'b': 2, 'c': 3, 'd': 0}
# update() — perbarui dari dict lain atau keyword args
data.update({"e": 5, "f": 6})
data.update(g=7, h=8)
print(data) # → {'a': 10, 'b': 2, 'c': 3, 'd': 0, 'e': 5, 'f': 6, 'g': 7, 'h': 8}
# Menghapus elemen
d = {"a": 1, "b": 2, "c": 3, "d": 4}
nilai_b = d.pop("b") # hapus dan kembalikan nilai: O(1)
print(nilai_b, d) # → 2 {'a': 1, 'c': 3, 'd': 4}
nilai_x = d.pop("x", None) # pop dengan default — tidak error jika tidak ada
print(nilai_x) # → None
item_terakhir = d.popitem() # hapus dan kembalikan pasangan terakhir (LIFO)
print(item_terakhir) # → ('d', 4)
del d["a"] # hapus kunci tertentu tanpa kembalikan nilai
d.clear() # hapus semua elemen
Iterasi Dictionary #
menu = {"nasi goreng": 25000, "mie ayam": 20000, "es teh": 5000}
# Iterasi kunci (default)
for kunci in menu:
print(kunci)
# Iterasi kunci secara eksplisit — sama hasilnya
for kunci in menu.keys():
print(kunci)
# Iterasi nilai
for harga in menu.values():
print(f"Rp{harga:,}")
# Iterasi pasangan kunci-nilai — paling umum digunakan
for nama_menu, harga in menu.items():
print(f"{nama_menu}: Rp{harga:,}")
# → nasi goreng: Rp25,000
# → mie ayam: Rp20,000
# → es teh: Rp5,000
# ANTI-PATTERN: akses nilai via kunci saat iterasi
for kunci in menu:
print(menu[kunci]) # tidak perlu — ada cara lebih baik
# BENAR: gunakan .values() atau .items()
for harga in menu.values():
print(harga)
for nama, harga in menu.items():
print(nama, harga)
Dict Comprehension #
Dict comprehension memungkinkan pembuatan atau transformasi dictionary secara ringkas:
# Transformasi nilai
harga = {"apel": 5000, "jeruk": 8000, "mangga": 12000}
# Terapkan diskon 10%
harga_diskon = {nama: int(h * 0.9) for nama, h in harga.items()}
print(harga_diskon)
# → {'apel': 4500, 'jeruk': 7200, 'mangga': 10800}
# Filter berdasarkan kondisi
mahal = {nama: h for nama, h in harga.items() if h >= 8000}
print(mahal) # → {'jeruk': 8000, 'mangga': 12000}
# Balik kunci dan nilai (invert dict)
kode_negara = {"Indonesia": "ID", "Malaysia": "MY", "Singapura": "SG"}
negara_dari_kode = {v: k for k, v in kode_negara.items()}
print(negara_dari_kode)
# → {'ID': 'Indonesia', 'MY': 'Malaysia', 'SG': 'Singapura'}
# Buat lookup dari list objek
produk_list = [
{"id": 1, "nama": "Laptop"},
{"id": 2, "nama": "Mouse"},
{"id": 3, "nama": "Keyboard"},
]
# Buat dict untuk lookup O(1) berdasarkan id
produk_lookup = {p["id"]: p for p in produk_list}
print(produk_lookup[2]) # → {'id': 2, 'nama': 'Mouse'}
Penggabungan Dictionary #
d1 = {"a": 1, "b": 2}
d2 = {"b": 20, "c": 3} # 'b' ada di keduanya — d2 menang
# update() — modifikasi d1 in-place (Python semua versi)
d1_copy = d1.copy()
d1_copy.update(d2)
print(d1_copy) # → {'a': 1, 'b': 20, 'c': 3}
# {**d1, **d2} — buat dict baru (Python 3.5+)
gabung = {**d1, **d2}
print(gabung) # → {'a': 1, 'b': 20, 'c': 3}
# Operator | — buat dict baru (Python 3.9+) — paling ringkas
gabung = d1 | d2
print(gabung) # → {'a': 1, 'b': 20, 'c': 3}
# Operator |= — update in-place (Python 3.9+)
d1 |= d2
print(d1) # → {'a': 1, 'b': 20, 'c': 3}
# Gabungkan banyak dict dengan urutan prioritas
default = {"debug": False, "timeout": 30, "max_retry": 3}
env = {"timeout": 60}
user = {"debug": True}
# user menang atas env, env menang atas default
config = default | env | user
print(config)
# → {'debug': True, 'timeout': 60, 'max_retry': 3}
Varian Dictionary dari collections
#
defaultdict — Nilai Default Otomatis
#
defaultdict menghilangkan kebutuhan cek “apakah kunci sudah ada” sebelum menggunakannya:
from collections import defaultdict
# ANTI-PATTERN: pola manual yang verbose
kelompok = {}
data = [("A", 1), ("B", 2), ("A", 3), ("C", 4), ("B", 5)]
for kunci, nilai in data:
if kunci not in kelompok:
kelompok[kunci] = [] # inisialisasi manual
kelompok[kunci].append(nilai)
# BENAR: defaultdict menginisialisasi otomatis
kelompok = defaultdict(list) # default factory: list()
for kunci, nilai in data:
kelompok[kunci].append(nilai) # langsung append — tidak perlu cek
print(dict(kelompok)) # → {'A': [1, 3], 'B': [2, 5], 'C': [4]}
# defaultdict dengan berbagai factory
hitung = defaultdict(int) # default: 0
total = defaultdict(float) # default: 0.0
nested = defaultdict(dict) # default: {}
set_unik = defaultdict(set) # default: set()
# Contoh: hitung frekuensi karakter
teks = "mississippi"
freq = defaultdict(int)
for huruf in teks:
freq[huruf] += 1
print(dict(freq))
# → {'m': 1, 'i': 4, 's': 4, 'p': 2}
# Contoh: kelompokkan siswa per nilai
siswa = [("Budi", "A"), ("Ani", "B"), ("Citra", "A"), ("Dedi", "B"), ("Eko", "C")]
per_nilai = defaultdict(list)
for nama, nilai in siswa:
per_nilai[nilai].append(nama)
print(dict(per_nilai))
# → {'A': ['Budi', 'Citra'], 'B': ['Ani', 'Dedi'], 'C': ['Eko']}
Counter — Hitung Frekuensi
#
Counter adalah subkelas dict yang dioptimalkan untuk menghitung kemunculan elemen:
from collections import Counter
# Hitung dari iterable
kata = ["apel", "jeruk", "apel", "mangga", "jeruk", "apel"]
hitungan = Counter(kata)
print(hitungan)
# → Counter({'apel': 3, 'jeruk': 2, 'mangga': 1})
# Hitung karakter dalam string
huruf = Counter("abracadabra")
print(huruf)
# → Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
# N elemen paling sering muncul
print(huruf.most_common(3))
# → [('a', 5), ('b', 2), ('r', 2)]
# Operasi aritmatika antar Counter
c1 = Counter({"apel": 3, "jeruk": 2})
c2 = Counter({"apel": 1, "mangga": 4})
print(c1 + c2) # → Counter({'mangga': 4, 'apel': 4, 'jeruk': 2})
print(c1 - c2) # → Counter({'jeruk': 2, 'apel': 2})
print(c1 & c2) # → Counter({'apel': 1}) (minimum)
print(c1 | c2) # → Counter({'mangga': 4, 'apel': 3, 'jeruk': 2}) (maksimum)
# Update counter
hitungan.update(["apel", "mangga"])
hitungan["jeruk"] += 5
ChainMap — Gabungkan Beberapa Dict sebagai Satu View
#
ChainMap menggabungkan beberapa dictionary menjadi satu tampilan tanpa menyalin data:
from collections import ChainMap
default_config = {"debug": False, "timeout": 30, "theme": "light"}
user_config = {"debug": True, "theme": "dark"}
env_config = {"timeout": 60}
# Pencarian: env_config → user_config → default_config
config = ChainMap(env_config, user_config, default_config)
print(config["debug"]) # → True (dari user_config)
print(config["timeout"]) # → 60 (dari env_config)
print(config["theme"]) # → dark (dari user_config)
# Modifikasi hanya mempengaruhi map pertama
config["baru"] = "nilai"
print(env_config) # → {'timeout': 60, 'baru': 'nilai'}
TypedDict — Dictionary dengan Type Safety
#
TypedDict (Python 3.8+) memungkinkan mendefinisikan tipe eksak untuk setiap kunci, sehingga type checker bisa memvalidasi penggunaan:
from typing import TypedDict, Optional
class Pengguna(TypedDict):
nama: str
email: str
umur: int
telepon: Optional[str] # boleh None
class PenggunaParsial(TypedDict, total=False):
# total=False: semua kunci menjadi opsional
nama: str
email: str
# Type checker memvalidasi ini
pengguna: Pengguna = {
"nama": "Budi",
"email": "[email protected]",
"umur": 28,
"telepon": None,
}
# mypy akan menandai ini sebagai error:
# pengguna["gaji"] = 5000 # Extra key 'gaji' not allowed
# pengguna["umur"] = "28" # 'str' incompatible with 'int'
Pola-pola Idiomatis #
Grouping Data #
# Kelompokkan transaksi berdasarkan kategori
transaksi = [
{"kategori": "makan", "jumlah": 50000},
{"kategori": "transport", "jumlah": 30000},
{"kategori": "makan", "jumlah": 75000},
{"kategori": "hiburan", "jumlah": 100000},
{"kategori": "transport", "jumlah": 25000},
]
from collections import defaultdict
per_kategori = defaultdict(list)
for t in transaksi:
per_kategori[t["kategori"]].append(t["jumlah"])
total_per_kategori = {k: sum(v) for k, v in per_kategori.items()}
print(total_per_kategori)
# → {'makan': 125000, 'transport': 55000, 'hiburan': 100000}
Caching Hasil Komputasi (Memoization Manual) #
# Simpan hasil kalkulasi mahal agar tidak dihitung ulang
_cache_fibonacci: dict[int, int] = {}
def fibonacci(n: int) -> int:
if n in _cache_fibonacci:
return _cache_fibonacci[n]
if n <= 1:
return n
hasil = fibonacci(n - 1) + fibonacci(n - 2)
_cache_fibonacci[n] = hasil
return hasil
# Atau gunakan functools.lru_cache (lebih direkomendasikan)
from functools import lru_cache
@lru_cache(maxsize=None)
def fibonacci_lru(n: int) -> int:
if n <= 1:
return n
return fibonacci_lru(n - 1) + fibonacci_lru(n - 2)
Nested Dictionary #
# Akses nested dict yang aman
config = {
"database": {
"host": "localhost",
"port": 5432,
}
}
# ANTI-PATTERN: akses langsung — crash jika kunci tidak ada di manapun
host = config["database"]["host"] # aman hanya jika tahu struktur pasti
port = config["server"]["port"] # KeyError: 'server'
# BENAR: gunakan get() berantai
host = config.get("database", {}).get("host", "localhost")
port = config.get("server", {}).get("port", 8080)
# Untuk nested yang lebih dalam, pertimbangkan fungsi helper
def deep_get(d: dict, *kunci, default=None):
"""Akses nested dict secara aman."""
for k in kunci:
if not isinstance(d, dict):
return default
d = d.get(k, {})
return d if d != {} else default
print(deep_get(config, "database", "host")) # → localhost
print(deep_get(config, "server", "port", default=8080)) # → 8080
Kompleksitas Waktu Operasi Dictionary #
| Operasi | Kompleksitas | Catatan |
|---|---|---|
d[key] |
O(1) avg | worst case O(n) — hash collision |
d[key] = val |
O(1) avg | |
del d[key] |
O(1) avg | |
key in d |
O(1) avg | jauh lebih cepat dari list! |
d.get(key) |
O(1) avg | |
d.keys() |
O(1) | kembalikan view, bukan list |
d.values() |
O(1) | kembalikan view, bukan list |
d.items() |
O(1) | kembalikan view, bukan list |
for k in d |
O(n) | iterasi semua kunci |
d.copy() |
O(n) | shallow copy |
d | d2 |
O(n+m) | buat dict baru |
Operasi O(1) untuk kunci adalah keunggulan utama dict dibanding list. Gunakan dict jika kamu perlu lookup cepat berdasarkan identifier — misalnya cache, index, atau konfigurasi.
Ringkasan #
- Gunakan
.get(key, default)untuk kunci yang mungkin tidak ada — lebih aman dan ringkas dari polaif key in d: ... else: ....dict.fromkeys(keys, default)untuk inisialisasi dict dengan nilai default seragam untuk semua kunci.setdefault(key, default)menambah kunci hanya jika belum ada — berguna untuk inisialisasi lazy.- Operator
|(Python 3.9+) adalah cara paling ringkas menggabungkan dua dict. Gunakan{**d1, **d2}untuk kompatibilitas Python 3.5+.defaultdictmenghilangkan polaif key not in d: d[key] = []— cukup langsung gunakan kunci tersebut.Counteruntuk menghitung frekuensi, mendukung operasi aritmatika antar counter dan.most_common(n).TypedDictuntuk dict dengan struktur tetap yang perlu divalidasi oleh type checker.key in dictadalah O(1) — jauh lebih cepat darikey in listyang O(n). Gunakan dict/set untuk lookup berulang.- Dict comprehension untuk transformasi dan filter dict — lebih ekspresif dari loop manual.
- Jangan akses nested dict dengan
[]berantai tanpa jaminan struktur — gunakan.get()berantai atau fungsideep_gethelper.