Danh sách bài viết

Bài 13: Dictionary — key-value lookup

Học dictionary trong Python: tạo dict, truy cập value, get với default, thêm/xóa key, kiểm tra tồn tại, iterate, update, đếm tần suất, nested dict và mapping cho dữ liệu AI/ML.

24/05/2026
12 phút đọc
1 lượt xem
1

Mục tiêu bài học

Sau bài này bạn sẽ:

  • Hiểu dictionary là gì, vì sao lookup theo key chỉ tốn thời gian gần như hằng số (O(1) trung bình).
  • Biết quy tắc hashable — kiểu nào dùng được làm key, kiểu nào không.
  • Tạo dict bằng nhiều cách (literal, dict(), list of tuples).
  • Truy cập an toàn bằng get(), thêm / xóa / kiểm tra tồn tại đúng cách.
  • Iterate qua dict với .items(), .keys(), .values().
  • Dùng dict cho 4 pattern phổ biến trong AI/data: counter, mapping, config, JSON.

Bài chạy trên Python 3.8 trở lên.

2

Dictionary là gì?

Dictionary (hay dict) là collection lưu các cặp key → value. Mỗi key là duy nhất, và bạn dùng key để tra ra value.

person = {
    "name": "Alice",
    "age": 30,
    "role": "ML engineer",
}

print(person["name"])   # Alice
print(person["age"])    # 30

Khác với list (truy cập bằng chỉ số 0, 1, 2...), dict truy cập bằng tên. Đây là điểm quyết định: khi dữ liệu có nhãn ý nghĩa (tên người, ID sản phẩm, mã ngôn ngữ), dict tự nhiên hơn list.

Tốc độ lookup: dict trong Python được cài bằng hash table. Trung bình, tra d[key] chỉ tốn O(1) — không phụ thuộc vào số phần tử. So với việc duyệt list bằng vòng for mất O(n), dict nhanh hơn rất nhiều khi dữ liệu lớn.

Insertion order: từ Python 3.7 trở đi, dict chính thức giữ nguyên thứ tự thêm vào (trong CPython 3.6 đã có nhưng là implementation detail). Có nghĩa là khi bạn iterate, các cặp xuất hiện đúng thứ tự bạn thêm.

d = {}
d["b"] = 2
d["a"] = 1
d["c"] = 3
print(list(d))   # ['b', 'a', 'c'] — không phải ['a', 'b', 'c']

Nếu cần ổn định cross-version (Python < 3.7), dùng collections.OrderedDict. Với Python hiện đại thì dict đã đủ.

3

Key phải hashable

Không phải kiểu nào cũng làm key được. Key phải hashable — Python tính ra được một số nguyên (hash) cố định cho nó. Các kiểu hashable phổ biến:

  • str — phổ biến nhất.
  • int, float — số.
  • bool, None.
  • tuple chứa toàn phần tử hashable (xem Bài 12).

Các kiểu không hashable (vì chúng mutable):

  • list
  • dict
  • set
# OK — string, int, tuple làm key
d = {
    "name": "Alice",
    42: "câu trả lời",
    (1, 2): "tọa độ",
}

# Lỗi — list không hashable
# d = {[1, 2]: "value"}
# TypeError: unhashable type: 'list'

Trực giác đằng sau: nếu cho phép list làm key, ai đó sửa list bằng .append() thì hash thay đổi, dict không tìm lại được — hỏng cấu trúc nội bộ. Tuple thì không sửa được nên hash ổn định, làm key được.

Value thì không có ràng buộc này — value có thể là bất kỳ kiểu nào, kể cả list, dict, hay object phức tạp.

4

Cách tạo dict

1. Literal với dấu ngoặc nhọn {} — cách thường dùng nhất:

user = {"name": "Alice", "age": 30}

empty = {}            # dict rỗng (KHÔNG phải set rỗng — set rỗng là set())

2. Hàm dict() với keyword argument — gọn khi key là string không có ký tự đặc biệt:

user = dict(name="Alice", age=30)
# Tương đương: {"name": "Alice", "age": 30}

3. Từ list of tuples — hữu ích khi data đã sẵn dạng cặp:

pairs = [("a", 1), ("b", 2), ("c", 3)]
d = dict(pairs)
print(d)   # {'a': 1, 'b': 2, 'c': 3}

4. Từ 2 list song song bằng zip():

keys = ["a", "b", "c"]
values = [1, 2, 3]
d = dict(zip(keys, values))
print(d)   # {'a': 1, 'b': 2, 'c': 3}

Nếu trùng key lúc tạo, key sau ghi đè key trước:

d = {"a": 1, "a": 2}
print(d)   # {'a': 2}
5

Truy cập value: [] vs get()

Hai cách lấy value, khác nhau ở chỗ khi key không tồn tại:

Cách 1: dấu ngoặc vuông d[key] — báo lỗi nếu key không có:

d = {"name": "Alice", "age": 30}

print(d["name"])     # Alice
# print(d["email"])  # KeyError: 'email'

Cách 2: d.get(key, default) — trả default (hoặc None nếu không truyền) khi key không có:

print(d.get("name"))            # Alice
print(d.get("email"))           # None — không crash
print(d.get("email", "N/A"))    # N/A — default tự định nghĩa

Khi nào dùng cái nào?

  • Dùng [] khi bạn chắc key tồn tại — nếu không có là bug, muốn lỗi nổ ngay để phát hiện sớm.
  • Dùng .get() khi key có thể không có và bạn muốn fallback an toàn (đọc config, parse JSON, xử lý input người dùng).

Ví dụ .get() trong AI: đọc hyperparameter từ config với giá trị mặc định nếu user không truyền:

config = {"learning_rate": 0.001}

lr = config.get("learning_rate", 0.01)
batch_size = config.get("batch_size", 32)    # không có → 32
epochs = config.get("epochs", 10)            # không có → 10

print(lr, batch_size, epochs)   # 0.001 32 10
6

Thêm và cập nhật

Gán d[key] = value dùng cho cả thêm mới lẫn cập nhật — Python không phân biệt:

user = {"name": "Alice"}

user["age"] = 30           # key chưa có → thêm mới
print(user)                # {'name': 'Alice', 'age': 30}

user["age"] = 31           # key đã có → ghi đè
print(user)                # {'name': 'Alice', 'age': 31}

Nếu muốn chỉ thêm khi chưa có (không ghi đè), dùng setdefault():

user = {"name": "Alice", "age": 30}

user.setdefault("age", 99)      # đã có "age" → giữ nguyên 30
user.setdefault("email", "[email protected]")   # chưa có → thêm
print(user)
# {'name': 'Alice', 'age': 30, 'email': '[email protected]'}

setdefault() hay được dùng khi build dict đếm nhóm — sẽ thấy ở mục Counter pattern.

7

Xóa: del, pop, popitem, clear

4 cách xóa, mục đích khác nhau:

del d[key] — xóa cặp, không trả giá trị; báo KeyError nếu key không có:

d = {"a": 1, "b": 2, "c": 3}
del d["b"]
print(d)   # {'a': 1, 'c': 3}

d.pop(key, default) — xóa và trả về giá trị; có default an toàn:

d = {"a": 1, "b": 2}
value = d.pop("a")
print(value, d)   # 1 {'b': 2}

# Có default → không crash khi key không có
missing = d.pop("z", None)
print(missing)    # None

d.popitem() — xóa và trả về cặp cuối cùng (LIFO theo insertion order, từ Python 3.7):

d = {"a": 1, "b": 2, "c": 3}
last = d.popitem()
print(last)   # ('c', 3)
print(d)      # {'a': 1, 'b': 2}

d.clear() — xóa sạch:

d = {"a": 1, "b": 2}
d.clear()
print(d)   # {}
8

Kiểm tra tồn tại với in

Toán tử in kiểm tra key có trong dict không (mặc định, KHÔNG kiểm tra value):

user = {"name": "Alice", "age": 30}

print("name" in user)       # True
print("email" in user)      # False
print("Alice" in user)      # False — "Alice" là value, không phải key

Phép in trên dict cũng là O(1) — nhanh hơn nhiều so với in trên list (O(n)).

Nếu muốn check value, dùng in d.values():

print("Alice" in user.values())   # True
print(30 in user.values())        # True

Pattern phổ biến: kiểm tra rồi truy cập (nhưng .get() thường gọn hơn):

# Cách thường — verbose
if "email" in user:
    email = user["email"]
else:
    email = "N/A"

# Gọn hơn với .get()
email = user.get("email", "N/A")
9

Iterate: keys, values, items

for k in d duyệt qua key (mặc định, ngầm gọi .keys()):

user = {"name": "Alice", "age": 30, "role": "ML"}

for key in user:
    print(key)
# name
# age
# role

d.values() duyệt qua value:

for v in user.values():
    print(v)
# Alice
# 30
# ML

d.items() trả tuple (key, value) — gần như luôn dùng kiểu này khi cần cả 2:

for key, value in user.items():
    print(f"{key} = {value}")
# name = Alice
# age = 30
# role = ML

3 phương thức này trả về view object, không phải list. View phản ánh thay đổi của dict theo thời gian thực:

d = {"a": 1, "b": 2}
keys = d.keys()
print(list(keys))   # ['a', 'b']

d["c"] = 3
print(list(keys))   # ['a', 'b', 'c'] — view cập nhật theo

Nếu cần list "snapshot" cố định, ép kiểu: list(d.keys()).

10

update() — gộp nhiều cặp

d.update(other) gộp nhiều cặp vào d trong một bước. Key trùng → giá trị của other ghi đè:

defaults = {"learning_rate": 0.01, "batch_size": 32, "epochs": 10}
overrides = {"learning_rate": 0.001, "epochs": 50}

config = dict(defaults)        # copy để không sửa defaults
config.update(overrides)
print(config)
# {'learning_rate': 0.001, 'batch_size': 32, 'epochs': 50}

Cũng có thể truyền keyword argument hoặc list of tuples:

d = {"a": 1}
d.update(b=2, c=3)              # keyword
d.update([("d", 4), ("e", 5)])  # list of tuples
print(d)
# {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5}

Từ Python 3.9 có thêm toán tử | để gộp 2 dict thành dict mới (không sửa bản gốc):

a = {"x": 1, "y": 2}
b = {"y": 99, "z": 3}

merged = a | b
print(merged)   # {'x': 1, 'y': 99, 'z': 3}
print(a)        # {'x': 1, 'y': 2} — không đổi
11

Use case AI/data

1. Counter — đếm tần suất. Pattern dùng khắp NLP cơ bản (word count, label distribution):

words = ["python", "ai", "python", "ml", "ai", "python"]

counts = {}
for w in words:
    counts[w] = counts.get(w, 0) + 1

print(counts)   # {'python': 3, 'ai': 2, 'ml': 1}

counts.get(w, 0) trả 0 nếu chưa có key — tránh phải if w in counts.

Trong thực tế dùng collections.Counter còn ngắn hơn:

from collections import Counter

counts = Counter(words)
print(counts)              # Counter({'python': 3, 'ai': 2, 'ml': 1})
print(counts.most_common(2))   # [('python', 3), ('ai', 2)]

2. Mapping (lookup table) — đổi mã sang nhãn, label encoding:

label_to_id = {"negative": 0, "neutral": 1, "positive": 2}
id_to_label = {v: k for k, v in label_to_id.items()}   # đảo ngược

print(label_to_id["positive"])   # 2
print(id_to_label[2])            # positive

3. Config dict — gom hyperparameter:

config = {
    "model": "bert-base-uncased",
    "learning_rate": 2e-5,
    "batch_size": 16,
    "epochs": 3,
    "max_length": 128,
}

print(f"Training {config['model']} với lr={config['learning_rate']}")

4. JSON — gần như 1-1 với dict Python. Đọc/ghi qua json module:

import json

# dict → JSON string
data = {"name": "Alice", "scores": [90, 85, 88]}
text = json.dumps(data)
print(text)   # {"name": "Alice", "scores": [90, 85, 88]}

# JSON string → dict
parsed = json.loads(text)
print(parsed["name"])   # Alice

API response, file cấu hình, dataset metadata — tất cả thường là JSON, và khi load vào Python sẽ thành dict (lồng list, lồng dict).

12

Nested dict

Value của dict có thể là dict khác — biểu diễn cấu trúc cây / JSON nhiều tầng:

users = {
    "alice": {
        "age": 30,
        "skills": ["python", "pytorch"],
    },
    "bob": {
        "age": 25,
        "skills": ["sql", "pandas"],
    },
}

# Truy cập theo từng tầng
print(users["alice"]["age"])         # 30
print(users["alice"]["skills"][0])   # python

Khi đường truy cập sâu mà không chắc tồn tại, lồng .get():

# An toàn — không crash nếu thiếu key giữa chừng
email = users.get("alice", {}).get("email", "N/A")
print(email)   # N/A

Iterate qua nested dict thường dùng 2 vòng for lồng nhau:

for username, info in users.items():
    print(f"{username}: tuổi {info['age']}")
    for skill in info["skills"]:
        print(f"  - {skill}")

Cấu trúc này gặp khắp nơi: response từ REST API, model card của HuggingFace, output của model.state_dict() trong PyTorch.

13

Preview: dict comprehension

Tương tự list comprehension (Bài 15), dict có cú pháp tạo ngắn gọn:

# Bình phương các số 0..4
squares = {n: n * n for n in range(5)}
print(squares)   # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

# Đảo key ↔ value
label_to_id = {"neg": 0, "pos": 1}
id_to_label = {v: k for k, v in label_to_id.items()}
print(id_to_label)   # {0: 'neg', 1: 'pos'}

# Lọc theo điều kiện
scores = {"alice": 90, "bob": 55, "carol": 78}
passed = {name: s for name, s in scores.items() if s >= 60}
print(passed)   # {'alice': 90, 'carol': 78}

Chi tiết cú pháp, các bước biến đổi và pitfall sẽ học ở Bài 15: List comprehension (cùng đụng đến set / dict comprehension).

14

Bài tập

Bài 1: Đếm tần suất từ trong câu.

Viết hàm word_count(sentence) nhận một chuỗi, tách bằng .split(), trả về dict {từ: số lần}. Sau đó in 3 từ xuất hiện nhiều nhất.

text = "ai engineer học python python data ai engineer ai"
# Kết quả mong đợi:
# {'ai': 3, 'engineer': 2, 'học': 1, 'python': 2, 'data': 1}

Yêu cầu: dùng .get(w, 0) + 1, không dùng Counter ở phần này. Sau đó so sánh với cách dùng Counter.

Bài 2: Merge 2 config dict.

Cho:

defaults = {"lr": 0.01, "batch_size": 32, "epochs": 10, "optimizer": "sgd"}
user_config = {"lr": 0.001, "epochs": 50}

Viết hàm merge_config(defaults, user_config) trả về dict mới trong đó key nào có trong user_config thì lấy giá trị từ đó, còn lại lấy từ defaults. Không sửa defaults hay user_config. Test:

result = merge_config(defaults, user_config)
print(result)
# {'lr': 0.001, 'batch_size': 32, 'epochs': 50, 'optimizer': 'sgd'}
print(defaults)      # phải giữ nguyên
print(user_config)   # phải giữ nguyên

Gợi ý: dùng dict(defaults) để copy, hoặc toán tử | (Python 3.9+).

Bài 3 (mở rộng): cho một list điểm thi scores = [("alice", 90), ("bob", 75), ("alice", 88), ("bob", 80), ("carol", 95)]. Viết hàm trả về dict {tên: điểm trung bình}. Gợi ý: build 2 dict — tổng và đếm — rồi chia.

15

Tóm tắt

  • dict là collection cặp key → value, lookup O(1) trung bình nhờ hash table.
  • Key phải hashable: str, int, float, bool, None, tuple; list, dict, set không được.
  • Từ Python 3.7, dict giữ insertion order theo spec.
  • Tạo: literal {"a": 1}, dict(a=1), dict([("a", 1)]), dict(zip(keys, values)).
  • Truy cập: d[k] (raise KeyError) hoặc d.get(k, default) (an toàn).
  • Thêm / cập nhật: d[k] = v; setdefault chỉ thêm khi chưa có.
  • Xóa: del d[k], d.pop(k, default), d.popitem() (cặp cuối), d.clear().
  • Kiểm tra: k in d — luôn check theo key, không phải value.
  • Iterate: for k in d, d.values(), d.items() (cho k, v).
  • Gộp: d.update(other); Python 3.9+ có toán tử |.
  • Use case AI/data: counter (collections.Counter), lookup table, config dict, JSON ↔ dict.
  • Nested dict biểu diễn cây / JSON; truy cập sâu nên lồng .get().

Bài tiếp theo sẽ học set — collection các phần tử duy nhất, hỗ trợ phép toán tập hợp (giao, hợp, hiệu).