พื้นฐาน

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

อัปเดต: · Skillaria.Top

ดิกชันนารี (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: ตั้งแต่มือใหม่ถึงมืออาชีพ" ผ่านตัวอย่างโปรแกรมติดตามค่าใช้จ่าย

คำถามที่พบบ่อย

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

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

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

ฟังก์ชัน 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: от новичка до профи”

ไปที่คอร์ส