2.10. เซต

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

2.10.1. การสร้างเซต

ใช้วงเล็บปีกกาสำหรับเซตที่ไม่ว่าง หรือ set() สำหรับเซตว่าง:

colours = {"red", "green", "blue"}
empty = set()

วงเล็บปีกกาดูเหมือนลิเทอรัล dict และ {} เพียงอย่างเดียวคือ dict ว่างเปล่า ไม่ใช่เซตว่างเปล่า ซึ่งเป็นหนึ่งในความไม่สม่ำเสมอของ Python ใช้ set() สำหรับกรณีว่าง

set() ยังสร้างเซตจาก iterable ใดก็ได้ ซึ่งเป็นวิธีมาตรฐานในการลบซ้ำออกจากลำดับ:

nums = [1, 2, 2, 3, 1, 4]
unique = set(nums)
print(unique)

ผลลัพธ์:

{1, 2, 3, 4}

ลำดับการพิมพ์อาจแตกต่างกัน เซตไม่รับประกันการวนซ้ำตามลำดับใดเป็นพิเศษ

2.10.2. เซตกับ dict

เซตและ dict ทั้งคู่เก็บรายการที่ไม่ซ้ำกันในตารางแฮช สิ่งที่แต่ละรายการพาติดตัวไปด้วยคือความแตกต่าง:

  • dict เก็บ คู่คีย์-ค่า การค้นหาคีย์คืนค่าของมัน

  • set เก็บ เฉพาะรายการ การค้นหารายการบอกว่ามันอยู่ที่นั่นหรือไม่

การเลือกระหว่างสองแบบขึ้นอยู่กับว่า ค่าที่อยู่ข้างแต่ละรายการ มีความหมายหรือไม่:

  • ใช้ เซต เมื่อไม่มีค่าที่ต้องเก็บไว้ข้างแต่ละรายการ คุณสนใจเพียงว่ารายการนั้นมีอยู่หรือไม่ หรือคุณกำลังรวมกลุ่มของรายการที่ไม่ซ้ำกันด้วย union / intersection

  • ใช้ dict เมื่อแต่ละรายการจับคู่กับข้อมูลที่การค้นหาต้องการดึงออกมา เช่น แผนที่ config แคช ตัวนับที่คีย์ด้วยชื่อ

สองประเภทนี้ใช้ไวยากรณ์พื้นผิวร่วมกันมาก ซึ่งเป็นที่มาของความสับสนส่วนใหญ่ ความแตกต่างในบล็อกเดียว:

set

dict

เก็บ

รายการที่ไม่ซ้ำกัน

คีย์ที่ไม่ซ้ำกัน แต่ละคีย์มีค่า

ลิเทอรัลที่มีข้อมูล

{1, 2, 3}

{"a": 1, "b": 2}

ลิเทอรัลว่าง

set()

{}

ทดสอบสมาชิก

x in s

k in d (เฉพาะคีย์)

ดึงค่า

ไม่มี

d[k]

เพิ่มรายการ

s.add(x)

d[k] = v

วนซ้ำ

ให้รายการ

ให้คีย์ (ใช้ d.items() สำหรับคู่)

ความไม่สมมาตรระหว่างลิเทอรัลที่มีข้อมูลและลิเทอรัลว่างเป็นประเด็นที่ควรระบุ:

  • วงเล็บปีกกาที่มี รายการอยู่ภายใน -- {1, 2, 3} -- คือลิเทอรัลเซต วงเล็บปีกกาที่มี คู่คีย์-ค่า -- {"a": 1} -- คือลิเทอรัล dict ตัวแยกวิเคราะห์แยกแยะด้วยสิ่งที่อยู่ภายใน

  • วงเล็บปีกกาที่ ไม่มีอะไรอยู่ภายใน -- {} -- คือ dict ว่างเปล่า ไม่ใช่เซตว่างเปล่า dict มาก่อน ดังนั้นลิเทอรัลว่างจึงเป็นของ dict เซตว่างไม่มีลิเทอรัลวงเล็บปีกกาเลยและต้องเขียนเป็น set()

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

2.10.3. การเพิ่มและลบ

  • set.add() -- แทรกรายการหนึ่ง

  • set.discard() -- ลบรายการหากมีอยู่ ไม่ทำอะไรหากไม่มี

  • set.remove() -- ลบรายการ หากไม่มีจะเกิด KeyError

  • set.clear() -- ทำให้เซตว่าง

s = {1, 2, 3}
s.add(4)
s.discard(99)            # silent: 99 not in s
s.remove(2)
print(s)

ผลลัพธ์:

{1, 3, 4}

2.10.4. การตรวจสอบสมาชิก

ตัวดำเนินการ in ทดสอบการเป็นสมาชิก บนเซตจะใช้เวลาคงที่โดยไม่ขึ้นกับขนาด ซึ่งเป็นเหตุผลหลักในการเลือกเซตแทน list เมื่อคุณต้องการถามเพียงว่า "ค่านี้อยู่ในนั้นหรือไม่":

if "red" in colours:
    print("colour is allowed")

list ที่มีเนื้อหาเดียวกันจะสแกนจากต้นทุกครั้ง ซึ่งเหมาะสำหรับสิบรายการแต่ช้าสำหรับหมื่นรายการ

2.10.5. การดำเนินการเซต

เซตสองชุดสามารถรวมกันด้วยการดำเนินการทางคณิตศาสตร์ปกติ แต่ละอย่างมีทั้งรูปแบบตัวดำเนินการและรูปแบบเมธอด:

  • a | b หรือ a.union(b) -- ทุกอย่างในเซตใดเซตหนึ่ง

  • a & b หรือ a.intersection(b) -- เฉพาะสิ่งที่ปรากฏในทั้งสองเซต

  • a - b หรือ a.difference(b) -- อยู่ใน a แต่ไม่ใน b

  • a ^ b หรือ a.symmetric_difference(b) -- อยู่ในเซตหนึ่งแต่ไม่ใช่ทั้งคู่

a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
print(a | b)
print(a & b)
print(a - b)
print(a ^ b)

ผลลัพธ์:

{1, 2, 3, 4, 5, 6}
{3, 4}
{1, 2}
{1, 2, 5, 6}

รูปแบบตัวดำเนินการเป็นแบบอ่านอย่างเดียว รูปแบบเมธอดรับ iterable ใดก็ได้ทางขวา ไม่ใช่แค่เซตอีกชุด (a.union([5, 6])) เลือกแบบที่อ่านได้ดีกว่าในบริบท

2.10.6. อะไรสามารถอยู่ในเซตได้

องค์ประกอบเซตต้องสามารถ แฮช ได้ ซึ่งเป็นข้อจำกัดเดียวกับคีย์ dict ประเภท int, float, str, bool, bytes และ tuple (เมื่อเนื้อหาสามารถแฮชได้เอง) ล้วนใช้ได้ แต่ list และ dict ไม่ได้ การพยายามเพิ่มจะเกิด TypeError

2.10.7. frozenset

set ปกติสามารถเปลี่ยนแปลงได้ ทุกการเรียก add / remove / discard จะเปลี่ยนออบเจกต์ในที่เดียวกัน ความสามารถในการเปลี่ยนแปลงนี้ทำให้มันไม่สามารถแฮชได้ ดังนั้นเซต ไม่สามารถ ใช้เป็นคีย์ dict หรือสมาชิกของเซตอื่น

frozenset คือคู่ที่ไม่เปลี่ยนแปลง มีการค้นหาและตัวดำเนินการ (in, |, &, -, ^) เหมือนกับ set แต่ไม่มี add / remove และไม่มีเมธอดที่แก้ไข เนื่องจากไม่มีอะไรเปลี่ยนเนื้อหาได้ แฮชของ frozenset จึงมีนิยามชัดเจน ดังนั้นมัน สามารถ แฮชได้:

primary = frozenset({"red", "green", "blue"})
secondary = frozenset({"yellow", "purple", "orange"})

palettes = {
    primary: "RGB",
    secondary: "mixed",
}

print(palettes[primary])

ผลลัพธ์:

RGB

สร้าง frozenset จาก iterable ใดก็ได้ ใช้ frozenset() สำหรับกรณีว่าง และ frozenset(some_set) เพื่อถ่ายสแนปช็อตที่ไม่เปลี่ยนแปลงของเซตที่มีอยู่:

snapshot = frozenset(s)         # immutable copy of s
s.add("new")                    # snapshot does not change

เหตุผลทั่วไปสองประการในการเลือกใช้:

  • ใช้เป็นคีย์ dict หรือสมาชิกเซต ทุกที่ที่ค่าเดียวไม่สามารถรองรับสิ่งที่คุณต้องการได้ frozenset ของค่าสามารถทำได้ เช่น "เซตของลักษณะเด่นที่ไดรเวอร์นี้รองรับ" หรือ "เซตของพินที่โปรไฟล์นี้ใช้"

  • ล็อคค่าคงที่ frozenset ระดับโมดูลของชื่อที่อนุญาตไม่สามารถถูกแก้ไขโดยบังเอิญโดยผู้เรียก แต่ set ปกติสามารถถูกแก้ไขได้ ควรใช้ frozenset สำหรับสิ่งที่ตั้งใจให้อ่านอย่างเดียวหลังการสร้าง