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 สำหรับสิ่งที่ตั้งใจให้อ่านอย่างเดียวหลังการสร้าง