ดิกชันนารีใน Python (dict): การสร้าง เมธอด การวนลูป การเรียงลำดับ และ frozendict
ดิกชันนารี (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.dumpspickleและ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: ตั้งแต่มือใหม่ถึงมืออาชีพ" ผ่านตัวอย่างโปรแกรมติดตามค่าใช้จ่าย
คำถามที่พบบ่อย
ค่าในดิกชันนารีเก็บชนิดต่างกันได้หรือไม่?
ได้ ค่าสามารถเป็นอะไรก็ได้และต่างชนิดกันในดิกชันนารีเดียวกัน ทั้งตัวเลข สตริง ลิสต์ หรือดิกชันนารีอื่น ข้อจำกัดมีเฉพาะคีย์ ซึ่งต้องแฮชได้
จะรู้จำนวนรายการในดิกชันนารีได้อย่างไร?
ฟังก์ชัน 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) เพื่ออ่าน โดยคีย์จะถูกแปลงเป็นสตริง
หัวข้อนี้สอนอย่างละเอียดพร้อมฝึกปฏิบัติในคอร์ส “Python 3.15: от новичка до профи”
ไปที่คอร์ส