# ดิกชันนารีใน Python (dict): การสร้าง เมธอด การวนลูป การเรียงลำดับ และ frozendict

URL: https://skillaria.top/th/guides/python/python-dicts

ดิกชันนารี (dict) ใน Python คือคอลเลกชันของคู่ "คีย์ — ค่า" ที่ค้นหาค่าจากคีย์ได้แทบทันที สร้างด้วยวงเล็บปีกกา เช่น {"อาหาร": 250} หรือฟังก์ชัน dict() คีย์ต้องไม่ซ้ำและแฮชได้ ตั้งแต่ Python 3.7 รักษาลำดับการเพิ่ม และใน 3.15 มีชนิดข้อมูลแบบแก้ไขไม่ได้ในตัวคือ frozendict

ดิกชันนารี (`dict`) ใน Python คือคอลเลกชันของคู่ "คีย์ — ค่า" ที่หาค่าจากคีย์ได้แทบจะทันที ไม่ว่าจะมีข้อมูลกี่รายการ หากต้องการนับค่าใช้จ่ายแยกตามหมวดหมู่ เก็บการตั้งค่า หรือค้นหาผู้ใช้จาก id ก็ใช้ดิกชันนารี บทความนี้ครอบคลุมทุกอย่างที่ต้องใช้กับ dict: การสร้าง เมธอด การวนลูป การเรียงลำดับ การรวม และ frozendict ที่มาใน Python 3.15

## ดิกชันนารีใน Python คืออะไร

ดิกชันนารีเก็บคู่ที่มีคีย์ไม่ซ้ำกัน หากเขียนค่าด้วยคีย์ที่มีอยู่แล้ว ค่าเก่าจะถูกแทนที่ ภายในใช้ตารางแฮช ทำให้การค้นหา เพิ่ม และลบด้วยคีย์ใช้เวลาเฉลี่ยคงที่ ไม่ต้องไล่ดูทีละรายการ

คีย์ต้องเป็นอ็อบเจกต์ที่แฮชได้ เช่น สตริง ตัวเลข หรือทูเพิลที่ประกอบด้วยค่าที่แก้ไม่ได้ ลิสต์ไม่สามารถเป็นคีย์ได้ ซึ่งจะได้ `TypeError: unhashable type: 'list'` ส่วนค่าสามารถเป็นอะไรก็ได้ รวมถึงดิกชันนารีและลิสต์อื่น

ตั้งแต่ Python 3.7 ดิกชันนารีรับประกันว่าจะจำลำดับการเพิ่มคีย์ เมื่อวนลูปก็จะได้คีย์ตามลำดับที่เพิ่มเข้ามา

## วิธีสร้างดิกชันนารี

มีหลายวิธีในการสร้างดิกชันนารี และทุกวิธีพบได้ในโค้ดจริง

```
spent = {"อาหาร": 1200, "แท็กซี่": 450}   # ลิเทอรัล
empty = {}                                 # ดิกชันนารีว่าง
also_empty = dict()                        # เหมือนกันแต่ใช้ฟังก์ชัน dict
user = dict(name="สมชาย", age=30)          # อาร์กิวเมนต์แบบมีชื่อ
pairs = dict([("a", 1), ("b", 2)])         # จากลิสต์ของคู่
keys = dict.fromkeys(["x", "y"], 0)        # {'x': 0, 'y': 0}
```

สังเกตว่า วงเล็บปีกกาว่าง `{}` สร้างดิกชันนารีว่าง ไม่ใช่เซต เซตว่างต้องสร้างด้วย `set()` เท่านั้น

วิธีสร้างดิกชันนารีจากลิสต์สองลิสต์ที่สะดวกคือใช้ `zip`:

```
names = ["อาหาร", "แท็กซี่", "หนัง"]
sums = [1200, 450, 600]
spent = dict(zip(names, sums))
print(spent)   # {'อาหาร': 1200, 'แท็กซี่': 450, 'หนัง': 600}
```

## อ่านค่าด้วยคีย์: d[k] และ get

อ่านค่าด้วยวงเล็บเหลี่ยม หากไม่มีคีย์นั้น Python จะเกิด `KeyError` หากต้องการหลีกเลี่ยงข้อผิดพลาด ให้ใช้เมธอด `get` ซึ่งคืน `None` หรือค่าเริ่มต้นที่กำหนดไว้

```
spent = {"อาหาร": 1200}
print(spent["อาหาร"])            # 1200
print(spent.get("หนัง"))         # None
print(spent.get("หนัง", 0))      # 0
spent["หนัง"]                    # KeyError: 'หนัง'
```

ตรวจว่ามีคีย์หรือไม่ใช้ตัวดำเนินการ `in`: `"อาหาร" in spent` คืน `True` โดยตรวจเฉพาะคีย์ ไม่ใช่ค่า หากต้องการตรวจค่าให้เขียน `1200 in spent.values()`

## เพิ่ม แก้ไข และลบ

การเพิ่มหรือแก้ไขรายการทำได้ในบรรทัดเดียวด้วยการกำหนดค่า สำหรับการนับค่า การใช้ร่วมกับ `get` สะดวกมาก

```
spent = {}
for category, amount in [("อาหาร", 250), ("แท็กซี่", 300), ("อาหาร", 400)]:
    spent[category] = spent.get(category, 0) + amount
print(spent)   # {'อาหาร': 650, 'แท็กซี่': 300}
```

ลบคีย์ได้ด้วย `del d[k]` (จะเกิด `KeyError` หากไม่มีคีย์) หรือ `d.pop(k, default)` ที่ยังคืนค่าที่ลบออกด้วย ล้างดิกชันนารีทั้งหมดด้วย `d.clear()` และความยาว (จำนวนคู่) หาได้ด้วย `len(d)`

## ตารางเมธอดของดิกชันนารี

| เมธอด | หน้าที่ |
| --- | --- |

| `get(k, default)` | คืนค่าของคีย์ หรือ default โดยไม่เกิดข้อผิดพลาด |

| `setdefault(k, default)` | คืนค่า; หากไม่มีคีย์จะเขียน default ลงไปก่อน |

| `update(other)` | เพิ่มและเขียนทับคู่จากดิกชันนารีอื่น |

| `pop(k, default)` | ลบคีย์และคืนค่า |

| `popitem()` | ลบและคืนคู่ที่เพิ่มล่าสุด |

| `keys()`, `values()`, `items()` | มุมมองของคีย์ ค่า และคู่ |

| `copy()` | คัดลอกแบบตื้น |

| `clear()` | ล้างดิกชันนารี |

| `fromkeys(keys, value)` | สร้างดิกชันนารีใหม่ที่ทุกคีย์มีค่าเดียวกัน |

### setdefault: จัดกลุ่มข้อมูลเข้าลิสต์

เมธอด `setdefault` มีประโยชน์เมื่อค่าเป็นลิสต์และต้องการจัดกลุ่มข้อมูล:

```
groups = {}
for item, cat in [("กาแฟ", "อาหาร"), ("รถไฟฟ้า", "เดินทาง"), ("พิซซ่า", "อาหาร")]:
    groups.setdefault(cat, []).append(item)
print(groups)   # {'อาหาร': ['กาแฟ', 'พิซซ่า'], 'เดินทาง': ['รถไฟฟ้า']}
```

### update: ปรับปรุงดิกชันนารี

`update` แก้ดิกชันนารีในตัวเอง คีย์ใหม่จะถูกเพิ่ม และคีย์ที่ซ้ำกันจะถูกเขียนทับ

```
cfg = {"debug": False, "lang": "th"}
cfg.update({"debug": True, "theme": "dark"})
print(cfg)   # {'debug': True, 'lang': 'th', 'theme': 'dark'}
```

## defaultdict: ดิกชันนารีที่มีค่าเริ่มต้น

`collections.defaultdict` เป็นคลาสย่อยของ dict ที่สร้างค่าให้เองด้วยฟังก์ชันโรงงาน (factory) เมื่อเข้าถึงคีย์ที่ยังไม่มี โค้ดสำหรับนับหรือจัดกลุ่มจึงสั้นลง

```
from collections import defaultdict

spent = defaultdict(int)
for cat, amount in [("อาหาร", 250), ("แท็กซี่", 300), ("อาหาร", 400)]:
    spent[cat] += amount
print(dict(spent))   # {'อาหาร': 650, 'แท็กซี่': 300}

groups = defaultdict(list)
groups["อาหาร"].append("กาแฟ")
```

สำหรับการนับจำนวนแบบธรรมดายังมี `collections.Counter`: `Counter(["a", "b", "a"])` ให้ผลเป็น `Counter({'a': 2, 'b': 1})`

## วนลูปดิกชันนารีด้วย for

เมื่อวนลูปด้วย `for` ดิกชันนารีจะให้คีย์เป็นค่าเริ่มต้น หากต้องการคีย์และค่าพร้อมกัน ให้วนผ่าน `items()` ซึ่งเป็นรูปแบบที่ใช้บ่อยที่สุด

```
spent = {"อาหาร": 650, "แท็กซี่": 300}

for cat in spent:                    # คีย์
    print(cat)

for amount in spent.values():        # ค่า
    print(amount)

for cat, amount in spent.items():    # คู่
    print(f"{cat}: {amount} บาท")
```

ห้ามเปลี่ยนขนาดดิกชันนารีระหว่างวนลูป ไม่เช่นนั้นจะเกิด `RuntimeError: dictionary changed size during iteration` หากต้องการลบบางคีย์ ให้วนผ่านสำเนา: `for k in list(d):`

## แสดงดิกชันนารีบนหน้าจอ

`print(d)` จะพิมพ์ดิกชันนารีในบรรทัดเดียว สำหรับข้อมูลขนาดใหญ่หรือซ้อนกันควรใช้โมดูล `pprint` หรือ `json.dumps(d, ensure_ascii=False, indent=2)` ซึ่งจะจัดย่อหน้าให้และแสดงภาษาไทยโดยไม่แปลงเป็นรหัสหลบอักขระ

## การเรียงลำดับดิกชันนารี

ตัวดิกชันนารีเองไม่สามารถเรียงลำดับในตัวเองได้ เพราะไม่มีเมธอด `sort` การเรียงดิกชันนารีคือการสร้างดิกชันนารีใหม่จากคู่ที่ถูกเรียงแล้วด้วย `sorted`

```
spent = {"taxi": 300, "food": 650, "movies": 120}

by_key = dict(sorted(spent.items()))
print(by_key)    # {'food': 650, 'movies': 120, 'taxi': 300}

by_value = dict(sorted(spent.items(), key=lambda p: p[1], reverse=True))
print(by_value)  # {'food': 650, 'taxi': 300, 'movies': 120}
```

หากต้องการเพียงหมวดหมู่ที่ใช้มากที่สุด N อันดับแรก ไม่จำเป็นต้องแปลงกลับเป็นดิกชันนารี: `sorted(spent.items(), key=lambda p: p[1], reverse=True)[:2]` คืนลิสต์ของคู่

การเรียงลิสต์ของดิกชันนารีตามฟิลด์ทำได้ด้วย `sorted(users, key=lambda u: u["age"])`

## รวมและบวกดิกชันนารี

ตั้งแต่ Python 3.9 ดิกชันนารีรวมกันด้วยตัวดำเนินการ `|` และ `|=` ปรับปรุงดิกชันนารีในตัวเอง เมื่อคีย์ซ้ำกัน ค่าจากตัวถูกดำเนินการด้านขวาจะชนะ

```
a = {"x": 1, "y": 2}
b = {"y": 20, "z": 3}
print(a | b)          # {'x': 1, 'y': 20, 'z': 3}
print({**a, **b})     # เหมือนกันด้วยการแตกค่า
a |= b                # เหมือน a.update(b)
```

สังเกตว่า `a + b` ใช้กับดิกชันนารีไม่ได้ จะเกิด `TypeError` หากต้องการบวกค่าของคีย์ที่ตรงกัน ให้ใช้ `Counter`: `Counter(a) + Counter(b)`

### การแตกดิกชันนารี

เครื่องหมายดอกจันสองตัว `**` แตกดิกชันนารีเป็นอาร์กิวเมนต์แบบมีชื่อของฟังก์ชัน หรือเป็นดิกชันนารีอีกอันหนึ่ง:

```
def greet(name, city):
    return f"{name} จาก {city}"

data = {"name": "สมชาย", "city": "เชียงใหม่"}
print(greet(**data))   # สมชาย จาก เชียงใหม่
```

## List comprehension สำหรับดิกชันนารี (dict comprehension)

การสร้างดิกชันนารีจากลำดับใดก็ได้สามารถทำได้ในบรรทัดเดียว:

```
squares = {n: n * n for n in range(5)}
print(squares)   # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

big = {k: v for k, v in spent.items() if v > 200}

inverted = {v: k for k, v in {"a": 1, "b": 2}.items()}
print(inverted)  # {1: 'a', 2: 'b'}
```

ตัวอย่างสุดท้ายคือการสลับคีย์กับค่า ใช้ได้เมื่อค่าไม่ซ้ำกันและแฮชได้ ไม่เช่นนั้นคู่บางคู่จะหายไป

## ดิกชันนารีซ้อนกัน

ค่าของดิกชันนารีเป็นดิกชันนารีอีกอันได้ โครงสร้างแบบนี้พบได้ในการตอบกลับของ API ในรูปแบบ JSON หรือดิกชันนารีของดิกชันนารีผู้ใช้

```
users = {
    101: {"name": "สมชาย", "skills": ["python", "sql"]},
    102: {"name": "วิไล", "skills": ["go"]},
}
print(users[101]["skills"][0])            # python
print(users.get(103, {}).get("name"))     # None โดยไม่เกิด KeyError
```

## การคัดลอกดิกชันนารี: สำเนากับชื่อที่สอง

คำสั่ง `b = a` ไม่ได้คัดลอกดิกชันนารี แต่เป็นการตั้งชื่อที่สองให้อ็อบเจกต์เดิม หากแก้ไขผ่าน `b` คุณจะเห็นการเปลี่ยนแปลงใน `a` ด้วย เช่นเดียวกัน เมื่อส่งดิกชันนารีเข้าฟังก์ชัน ฟังก์ชันจะได้รับการอ้างอิงถึงอ็อบเจกต์เดิม และอาจแก้ไขข้อมูลโดยไม่ตั้งใจ

```
import copy

a = {"limits": [0, 500]}
b = a.copy()              # คัดลอกแบบตื้น
b["limits"].append(1000)  # ลิสต์ที่ซ้อนอยู่ยังใช้ร่วมกัน!
print(a)                  # {'limits': [0, 500, 1000]}

c = copy.deepcopy(a)      # สำเนาที่เป็นอิสระทั้งหมด
```

## การเปรียบเทียบดิกชันนารี

ตัวดำเนินการ `==` เปรียบเทียบดิกชันนารีตามเนื้อหา ถือว่าเท่ากันเมื่อคู่ทั้งหมดตรงกัน โดยไม่สนใจลำดับของคีย์: `{"a": 1, "b": 2} == {"b": 2, "a": 1}` ได้ `True` หากต้องการหาคีย์ที่ใช้ร่วมกัน ให้ใช้การดำเนินการของเซตกับมุมมองของคีย์: `a.keys() & b.keys()`

## ดิกชันนารีที่มีชนิดกำหนดไว้: การใส่ annotation

การระบุชนิดของดิกชันนารีเขียนได้เป็น `dict[str, int]` หากดิกชันนารีมีชุดฟิลด์คงที่ที่มีชนิดต่างกัน `TypedDict` จากโมดูล `typing` จะเหมาะกว่า:

```
from typing import TypedDict

class User(TypedDict):
    name: str
    age: int

u: User = {"name": "สมชาย", "age": 30}
```

Annotation ไม่ได้ถูกตรวจขณะรันโปรแกรม แต่ IDE และเครื่องมือวิเคราะห์อย่าง mypy จะอ่านไปใช้งาน

## ดิกชันนารีแก้ไขไม่ได้: frozendict ใน Python 3.15

Python 3.15 เพิ่มชนิดข้อมูลในตัว `frozendict` (PEP 814) ไม่ต้อง import และการพยายามแก้ไขจะเกิด `TypeError` ทันที ก่อนหน้านี้ดิกชันนารีที่แก้ไม่ได้ต้องใช้ตัวห่อจากโมดูล `types` หรือแพ็กเกจภายนอก เหมาะกับการตั้งค่าร่วมที่ไม่ควรถูกฟังก์ชันใดเปลี่ยนแปลงโดยไม่รู้ตัว

```
config = frozendict(min_amount=0, max_amount=500)
config["max_amount"] = 1000
# TypeError: 'frozendict' object does not support item assignment

strict = config | {"max_amount": 300}   # frozendict ใหม่ ต้นฉบับไม่เปลี่ยน
cache = {config: "รายงานเดือนพฤษภาคม"}  # แฮชได้ จึงใช้เป็นคีย์ได้
```

ข้อควรรู้ตามเนื้อหาของคอร์ส:

- `frozendict` สืบทอดจาก `object` ไม่ใช่จาก `dict` ดังนั้น `isinstance(cfg, dict)` จะคืน `False` หากต้องการตรวจว่าเป็นดิกชันนารีชนิดใดก็ได้ ให้ใช้ `collections.abc.Mapping` หรือ `isinstance(x, (dict, frozendict))`
- มันแฮชได้ก็ต่อเมื่อคีย์และค่าทั้งหมดแฮชได้ เช่น `frozendict(a=[1])` สร้างได้ แต่ `hash()` จะล้มเหลว ความแก้ไขไม่ได้เป็นแบบตื้น เหมือนกับทูเพิล
- เก็บลำดับไว้ แต่ `==` ไม่นำลำดับมาพิจารณา
- `json.dumps` `pickle` และ `copy` รองรับ frozendict

## ข้อผิดพลาดที่พบบ่อย

- **KeyError** ตอนอ่านค่า ให้ใช้ `get` หรือตรวจด้วย `in` ก่อน
- **ใช้ลิสต์เป็นคีย์** ให้แปลงเป็นทูเพิลก่อน: `d[tuple(lst)]`
- **แก้ไขดิกชันนารีระหว่างวนลูป** ให้วนผ่าน `list(d)`
- **คาดหวังว่าการกำหนดค่าจะสร้างสำเนา** ต้องใช้ `copy()` หรือ `deepcopy`
- **ใช้ `{}` เป็นเซตว่าง** แต่นั่นคือดิกชันนารีว่าง

## สรุป

dict คือโครงสร้างข้อมูลหลักของ Python ค้นหาด้วยคีย์ได้รวดเร็ว รักษาลำดับการเพิ่ม มีเมธอดที่สะดวก เช่น `get` `setdefault` `update` วนลูปผ่าน `items()` เรียงด้วย `sorted` และรวมด้วยตัวดำเนินการ `|` ควรระวังเรื่องการคัดลอกและคีย์ที่แฮชได้ และสำหรับการตั้งค่าใน Python 3.15 ให้ใช้ `frozendict` ดิกชันนารี ลิสต์ ทูเพิล และเซตถูกนำมาใช้จริงในคอร์ส "Python 3.15: ตั้งแต่มือใหม่ถึงมืออาชีพ" ผ่านตัวอย่างโปรแกรมติดตามค่าใช้จ่าย

## FAQ

**ค่าในดิกชันนารีเก็บชนิดต่างกันได้หรือไม่?**

ได้ ค่าสามารถเป็นอะไรก็ได้และต่างชนิดกันในดิกชันนารีเดียวกัน ทั้งตัวเลข สตริง ลิสต์ หรือดิกชันนารีอื่น ข้อจำกัดมีเฉพาะคีย์ ซึ่งต้องแฮชได้

**จะรู้จำนวนรายการในดิกชันนารีได้อย่างไร?**

ฟังก์ชัน len(d) คืนจำนวนคู่ของคีย์กับค่า ดิกชันนารีว่างจะได้ 0 และดิกชันนารีว่างจะถือเป็นเท็จในเงื่อนไข if

**dict ต่างจาก defaultdict อย่างไร?**

defaultdict เป็นคลาสย่อยของ dict ที่ไม่เกิด KeyError เมื่อเข้าถึงคีย์ที่ยังไม่มี แต่จะสร้างค่าเริ่มต้นผ่านฟังก์ชันโรงงาน เช่น int หรือ list ส่วนการทำงานอื่นเหมือน dict ทุกประการ

**จะหาคีย์จากค่าได้อย่างไร?**

ไม่มีเมธอดตรง ๆ ต้องวนลูปเอง เช่น next((k for k, v in d.items() if v == value), None) หากต้องค้นหาบ่อย ควรสร้างดิกชันนารีกลับด้านด้วย {v: k for k, v in d.items()}

**จะบันทึกดิกชันนารีลงไฟล์อย่างไร?**

ส่วนใหญ่ใช้โมดูล json เช่น json.dump(d, f, ensure_ascii=False, indent=2) เมื่อเปิดไฟล์เพื่อเขียน และ json.load(f) เพื่ออ่าน โดยคีย์จะถูกแปลงเป็นสตริง


---
Source: Skillaria.Top (https://skillaria.top)
