DHDataHọc
Lý thuyếtBài 26/304 phút đọc+10 XP

Dict lồng nhau

Bạn sẽ học được

  • Đọc và ghi dict nhiều tầng.
  • Chuỗi get() an toàn cho trường thiếu.
  • Duyệt cấu trúc lồng: items, list of dict.
  • Tính toán trên dữ liệu lồng như đơn hàng và khách hàng.
  • Cái bẫy sao chép nông và cách làm phẳng dữ liệu.

Khái niệm cốt lõi

Dữ liệu thực tế thường lồng nhau: một đơn hàng có khách hàng, địa chỉ, danh sách sản phẩm. Dict lồng nhau là cách tự nhiên để biểu diễn:

code
order = {
    "id": 1001,
    "customer": {"name": "An", "email": None, "address": {"city": "Hà Nội"}},
    "items": [
        {"sku": "A1", "qty": 2, "price": 150000},
        {"sku": "B2", "qty": 1, "price": 450000},
    ],
}

print(order["customer"]["name"])
code
An

Mỗi cặp ngoặc vuông đi xuống một tầng. order["customer"] là dict, tiếp ["name"] là chuỗi. Có thể đi sâu bao nhiêu tầng tuỳ cấu trúc.

Đọc trường có thể thiếu bằng [] sẽ ném KeyError nếu bất kỳ tầng nào không có:

code
print(order["customer"].get("email"))
print(order.get("customer", {}).get("address", {}).get("city", "không rõ"))
code
None
Hà Nội

Dòng thứ hai là chuỗi get an toàn: mỗi tầng dùng get với {} làm mặc định, nên thiếu tầng nào cũng không lỗi và cuối cùng rơi về "không rõ". Dài dòng, nhưng đúng cho dữ liệu từ API hoặc JSON chưa kiểm chứng.

Nếu cần dùng nhiều lần, viết hàm:

code
def lay(data, *keys, default=None):
    for key in keys:
        if not isinstance(data, dict) or key not in data:
            return default
        data = data[key]
    return data

print(lay(order, "customer", "address", "city"))
print(lay(order, "customer", "phone", default="chưa có"))
code
Hà Nội
chưa có

*keys nhận số đường dẫn tuỳ ý; vòng lặp dừng ngay khi gặp tầng không phải dict hoặc thiếu khóa. Đây là phiên bản đơn giản của "safe navigation" mà nhiều ngôn ngữ có sẵn (?.).

Duyệt dữ liệu lồng

Với list các item bên trong:

code
total = sum(item["qty"] * item["price"] for item in order["items"])
print(total)
code
750000

Generator đi qua từng dict con, nhân số lượng với đơn giá. Kết quả 2 × 150000 + 1 × 450000 = 750000.

In chi tiết:

code
for item in order["items"]:
    print(item["sku"], item["qty"] * item["price"])
code
A1 300000
B2 450000

Với dict của dict — ví dụ doanh thu theo tháng, mỗi tháng lại theo kênh — duyệt hai tầng:

code
revenue = {
    "2026-01": {"web": 120, "store": 80},
    "2026-02": {"web": 150, "store": 95},
}

for month, channels in revenue.items():
    total = sum(channels.values())
    print(month, total)
code
2026-01 200
2026-02 245

items() ở tầng ngoài cho cặp tháng → dict kênh; .values() ở tầng trong cộng các kênh.

Ghi và cập nhật

Gán vào khóa lồng phải đi đủ đường:

code
order["customer"]["phone"] = "0901234567"
order["customer"]["address"]["district"] = "Cầu Giấy"

Nếu tầng giữa chưa tồn tại, dòng trên ném KeyError. Với dữ liệu xây dần, dùng setdefault:

code
by_month = {}
for row in [("2026-01", "web", 120), ("2026-01", "store", 80)]:
    month, channel, amount = row
    by_month.setdefault(month, {})[channel] = amount
print(by_month)
code
{'2026-01': {'web': 120, 'store': 80}}

Hoặc defaultdict(dict) từ collections.

Cái bẫy sao chép nông

Sao chép dict bằng .copy() chỉ sao tầng ngoài; các dict con vẫn dùng chung:

code
a = {"customer": {"name": "An"}}
b = a.copy()
b["customer"]["name"] = "Bình"
print(a)
code
{'customer': {'name': 'Bình'}}

Sửa b làm đổi a. Đây cùng bản chất với list lồng ở chương 3. Muốn độc lập hoàn toàn:

code
import copy
b = copy.deepcopy(a)

Với dữ liệu chỉ đọc, sao chép nông thường đủ và rẻ hơn. Với dữ liệu sẽ sửa, hãy nghĩ tới deepcopy hoặc tạo cấu trúc mới thay vì sửa tại chỗ.

Làm phẳng để phân tích

Dict lồng tiện cho API nhưng khó tính toán. Bước làm phẳng (flatten) đưa về bảng:

code
rows = []
for item in order["items"]:
    rows.append({
        "order_id": order["id"],
        "customer": order["customer"]["name"],
        "city": order["customer"]["address"]["city"],
        "sku": item["sku"],
        "revenue": item["qty"] * item["price"],
    })

Mỗi dòng là một item kèm thông tin đơn và khách. Dạng này tương thích với CSV, pandas, SQL — nơi mọi phân tích đều bắt đầu từ bảng phẳng.

Ngược lại, khi dựng JSON gửi API, gom từ bảng phẳng về cấu trúc lồng bằng setdefault hoặc groupby của pandas.

Thực hành

  1. Đọc order["customer"]["name"], thử đọc khóa không tồn tại để thấy KeyError.
  2. Dùng chuỗi get() lấy city và một trường thiếu với mặc định.
  3. Viết hàm lay(data, *keys) và thử với đường dẫn sai.
  4. Tính tổng tiền các item bằng generator.
  5. Duyệt dict hai tầng doanh thu theo tháng/kênh.
  6. Làm phẳng order thành list dict và in từng dòng.

Lỗi thường gặp

Triệu chứngNguyên nhânCách sửa
KeyError giữa chuỗiTầng trước không có khóaDùng get từng tầng
TypeError: 'NoneType' object is not subscriptableget trả None rồi tiếp tục [...]Mặc định {} trong get
Sửa bản sao, bản gốc đổiSao chép nôngdeepcopy
AttributeError trên dictDùng d.key thay d["key"]Dùng ngoặc vuông
Tổng saiCộng chuỗi hoặc thiếu itemÉp kiểu, kiểm tra len
setdefault ghi đèGán thay vì cộng dồn+= giá trị hiện có
Dữ liệu lồng không đưa vào bảng đượcChưa flattenTạo list dict phẳng

Tóm tắt

  • Dict lồng truy cập bằng chuỗi ngoặc vuông; thiếu tầng sẽ KeyError.
  • get(k, {}) nối tầng an toàn; hàm lay gói lại cho gọn.
  • Duyệt bằng items(); list con bằng generator.
  • .copy() chỉ nông — dùng deepcopy khi cần độc lập.
  • Làm phẳng về list dict trước khi phân tích hoặc ghi CSV.

Dùng ← → để chuyển bài