บทที่ 5
โครงสร้างข้อมูลแบบซ้อนใน (Nested Data Structure)#

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

  1. ข้อมูลแบบค่าเดี่ยว ได้แก่ เลขจำนวนเต็ม สตริง เลขทศนิยม และบูลีน

  2. ข้อมูลแบบคอลเลกชัน ได้แก่ ลิสต์ ทูเปิล ดิกชันนารี เซ็ต

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

basic data structures

โครงสร้างข้อมูลทั้งสองประเภทนี้ไม่ตอบโจทย์การใช้งานบางประเภท ที่ต้องการโครงสร้างที่ซับซ้อนขึ้น โครงสร้างข้อมูลแบบซ้อนใน (nested data structure) คือ โครงสร้างข้อมูลที่เก็บโครงสร้างข้อมูลอีกประเภทนึงเอาไว้ โครงสร้างข้อมูลทั้งหมดที่เราได้เรียนมาแล้วสามารถเก็บโครงสร้างข้อมูลอื่น ๆ ซ้อนเข้าไปได้อีกชั้นหนึ่ง เพื่อรองรับข้อมูลที่มีโครงสร้างซับซ้อนขึ้น เช่น

  • ลิสต์ของลิสต์ของสตริง เพื่อใช้เก็บชื่อไฟล์และโฟลเดอร์ที่อยู่ในโฟลเดอร์อีกชั้นหนึ่ง

files = [
    ['file1.txt', 'file2.txt', 'file3.txt'],
    ['file4.txt', 'file5.txt', 'file6.txt'],
    ['file7.txt', 'file8.txt']
]
  • ลิสต์ของทูเปิลของสตริง เพื่อใช้เก็บรายชื่อที่มีทั้งชื่อจริงและชื่อเล่น

names = [
    ('เศรษฐา', 'นิด'),
    ('ประยุทธ์', 'ตู่'),
    ('ยิ่งลักษณ์', 'ปู'),
    ('อภิสิทธิ์', 'มาร์ค')
]
  • ลิสต์ของดิกชันนารี เพื่อใช้เก็บบทสนทนาในกลุ่มไลน์

tweets = [
    {'user': 'nong-p1', 'message': 'พี่คะ โปรแกรมหนู error ค่ะ'},
    {'user': 'nipun', 'message': 'ขอดูหน่อยครับ'},
    {'user': 'nipun', 'message': 'ส่ง screenshot มาเลยครับ'},
    {'user': 'p-p2', 'message': 'ปัญหาเดียวกันเลยครับ'}
]

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

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

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

ลิสต์ของลิสต์#

ในการใช้งานจริงเราใช้ลิสต์ของลิสต์เพื่อเก็บข้อมูลประเภทเดี่ยวทั้ง 3 ประเภท ลิสต์ของลิสต์ของตัวเลข (ลิสต์ที่มีลิสต์ย่อยที่เก็บตัวเลข) ลิสต์ของลิสต์ของสตริง และ ลิสต์ของลิสต์ของบูลีน

ลิสต์ของลิสต์ของตัวเลข#

ขอยกตัวอย่างการใช้ลิสต์ของลิสต์ของตัวเลข ซึ่งเป็นลิสต์ของคะแนนของนักเรียน โดยแต่ละคะแนนจะประกอบไปด้วย คะแนนเก็บ คะแนนสอบกลางภาค และคะแนนสอบปลายภาค โดยเราจะเก็บคะแนนของนักเรียนแต่ละคนไว้ในลิสต์ของลิสต์ของตัวเลข ดังนี้

scores = [
    [10, 20, 30],
    [40, 50, 60],
    [70, 80, 90]
]

เราสามารถเข้าถึงคะแนนของนักเรียนคนที่ 2 ได้ดังนี้

scores[1]

ผลลัพธ์ที่ได้คือ

[40, 50, 60]

ซึ่งเป็นลิสต์ของคะแนนของนักเรียนคนที่ 2

ลิสต์ของลิสต์ของสตริง#

ในการประมวลผลภาษาธรรมชาติ ลิสต์ของลิสต์มักจะใช้เก็บประโยคหลาย ๆ ประโยค เราจะเก็บประโยค 1 ประโยคไว้ในลิสต์ของสตริงคำ เมื่อเราต้องเก็บหลาย ๆ ประโยคจึงกลายมาเป็นลิสต์ของลิสต์ของสตริงคำ

[['I', 'want', 'you'], ['I', 'need', 'you'], ['I', 'love', 'you']]

ถ้าหากเรามีข้อมูลเป็นลิสต์ของสตริงที่เป็นประโยคภาษาอังกฤษ เราสามารถเปลี่ยนให้เป็นลิสต์ของลิสต์ของคำได้ดังนี้

sentences = [
    'I want you',
    'I need you',
    'I love you'
]
words = [sentence.split() for sentence in sentences]

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

[['I', 'want', 'you'], ['I', 'need', 'you'], ['I', 'love', 'you']]

ซึ่งเป็นลิสต์ของลิสต์ของสตริง

คำถามชวนคิด

สมมติว่าเรามีข้อมูลต่อไปนี้

list_sen = [['แซม','ซื้อ','tv','ใหม่'],['android', 'ไม่', 'ได้','ถูก', 'กว่า', 'iphone']]
list_lang = [['th','th','en','th'],['en', 'th', 'th','th', 'th', 'en']]
list_lang2 = [[False, False, True, False], [True, False, False, False, False, True]]
list_len = [[3, 4, 2, 4], [7, 3, 3, 3, 4, 6]]

คำสั่งต่อไปนี้คืนค่าเป็นอะไร ซึ่งจัดเป็นข้อมูลประเภทใด

  1. list_sen[0]

  2. list_lang[1][0]

  3. list_lang2[1][2]

  4. list_len[0][3]

เฉลย

คำสั่ง

คืนค่า

ประเภทข้อมูล

คำอธิบาย

list_sen[0]

['แซม','ซื้อ','tv','ใหม่']

ลิสต์ของสตริง

[0]เข้าถึงข้อมูลในลิสต์ของลิสต์ของสตริง (list_sen) จึงแกะลิสต์ออกหนึ่งชั้น ได้~~ลิสต์ของ~~ลิสต์ของสตริง

list_lang[1][0]

'en'

สตริง

[1]เข้าถึงข้อมูลในลิสต์ของลิสต์ของสตริง (list_lang)จึงแกะลิสต์ออกหนึ่งชั้น ได้~~ลิสต์ของ~~ลิสต์ของสตริง จากนั้น [0] เข้าถึงสตริงในลิสต์ของสตริง ได้สตริง

list_lang2[1][2]

False

บูลีน

[1]เข้าถึงข้อมูลในลิสต์ของลิสต์ของบูลีน (list_lang2) จึงแกะลิสต์ออกหนึ่งชั้น ได้~~ลิสต์ของ~~ลิสต์ของบูลีน จากนั้น [2] เข้าถึงบูลีนในลิสต์ของบูลีน ได้บูลีน

list_len[0][3]

4

จำนวนเต็ม

[0]เข้าถึงข้อมูลในลิสต์ของลิสต์ของจำนวนเต็ม (list_len) จึงแกะลิสต์ออกหนึ่งชั้น ได้~~ลิสต์ของ~~ลิสต์ของจำนวนเต็ม จากนั้น [3] เข้าถึงจำนวนเต็มในลิสต์ของจำนวนเต็ม ได้จำนวนเต็ม

รูปแบบการใช้งานที่พบเห็นเป็นประจำ ในการประมวลผลข้อมูล ได้แก่

  • ไล่วนซ้ำบนสมาชิกทุกตัวที่อยู่ในลิสต์ย่อย

  • รันฟังก์ชันไปบนสมาชิกทุกตัวที่อยู่ในลิสต์ย่อย

  • ทำลิสต์ให้แบนเรียบ (flatten)

ไล่วนซ้ำบนสมาชิกทุกตัวที่อยู่ในลิสต์ย่อย#

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

for ลิสต์ย่อย in ลิสต์ใหญ่
    for ข้อมูลเดี่ยว in ลิสต์ย่อย
        ทำอะไรกับข้อมูลเดี่ยว

ตัวอย่างที่ 1#

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

sentences = [['I', 'want', 'you'], ['I', 'need', 'you'], ['I', 'love', 'you']]
total_words = 0
for sentence in sentences:
    for word in sentence:
        total_words += 1
print(total_words)

สิ่งที่ปรากฏบนหน้าจอ คือ

9

ซึ่งเป็นจำนวนคำทั้งหมดในลิสต์ของลิสต์ของสตริง

โปรแกรมข้างต้นมีการใช้วงวนฟอร์ซ้อนกัน (double for loop) ที่มีวงวนฟอร์ชั้นนอก และชั้นใน

  • วงวนฟอร์ชั้นนอกเป็นการไล่เอาสมาชิกแต่ละตัวที่เป็นลิสต์ออกมาจากลิสต์ของลิสต์ (sentences) โดยในแต่ละรอบให้ใช้ตัวแปร sentence มาใช้เก็บลิสต์ย่อย

  • วงวนฟอร์ชั้นในเป็นการไล่เอาสมาชิกแต่ละตัวที่เป็นสตริงออกมาจากลิสต์ของสตริง (sentence) โดยในแต่ละรอบให้ใช้ตัวแปร word มาใช้เก็บสตริง และในแต่ละรอบให้นับคำที่อยู่ในตัวแปร word แล้วบวกเพิ่มในตัวแปร total_words ที่เป็นจำนวนคำทั้งหมด

ตัวอย่างที่ 2#

สมมติว่าเราต้องการหาคำที่สะกดด้วยตัวอักษรภาษาอังกฤษทั้งหมดโดยไม่ต้องนับตัวซ้ำ ข้อมูลที่ได้มาถูกจัดเก็บอยู่ในลิสต์ของลิสต์ของสตริงคำ เช่นเดียวกันกับตัวอย่างที่ 1

list_sen = [['แซม','ซื้อ','tv','ใหม่'],['android', 'ไม่', 'ได้','ถูก', 'กว่า', 'iphone']]
eng_word_set = set()
for sen in list_sen:
    for word in sen:
        if word.isalpha():
            eng_word_set.add(word)

วงวนฟอร์ชั้นนอกเป็นการไล่เอาสมาชิกแต่ละตัวที่เป็นลิสต์ออกมาจากลิสต์ของลิสต์ (list_sen) โดยในแต่ละรอบให้ใช้ตัวแปร sen มาใช้เก็บลิสต์ย่อย และวงวนฟอร์ชั้นในเป็นการไล่เอาสมาชิกแต่ละตัวที่เป็นสตริงออกมาจากลิสต์ของสตริง (sen) โดยในแต่ละรอบให้ใช้ตัวแปร word มาใช้เก็บสตริง และในแต่ละรอบให้ตรวจสอบว่าสตริงที่อยู่ในตัวแปร word เป็นสตริงที่มีแต่ตัวอักษรภาษาอังกฤษหรือไม่ โดยใช้เมท็อดของสตริง .isalpha() ซึ่งจะคืนค่าเป็นบูลีน ถ้าเป็นค่า True ให้เพิ่มสตริงที่อยู่ในตัวแปร word ลงในเซ็ต eng_word_set โดยใช้คำสั่ง eng_word_set.add(word) ซึ่งเป็นการเพิ่มสตริงลงในเซ็ต และเนื่องจากเราใช้เซ็ตในการเก็บ สตริงที่ซ้ำกันจะถูกลบออกไปโดยอัตโนมัติ

รันฟังก์ชันไปบนสมาชิกทุกตัวที่อยู่ในลิสต์ย่อย#

การรันฟังก์ชันไปบนสมาชิกทุกตัว เราเรียกว่าการแปลงส่งฟังก์ชัน หรือการแม็ปฟังก์ชัน ให้ลองนึกทบทวนถึงการแม็ปฟังก์ชันกับลิสต์ที่ไม่มีการซ้อนลิสต์ จากตัวอย่างต่อไปนี้

สมมติว่า เราต้องการแม็ปฟังก์ชันด้วยฟังก์ชันยกกำลังสองไปบนลิสต์ของตัวเลข numbers = [2, 10, 12, 20] (ซึ่งเป็นลิสต์ที่ยังไม่มีการซ้อนลิสต์) เริ่มจากการสร้างฟังก์ชัน square ที่รับตัวเลขเข้ามา แล้วคืนค่ากลับมาเป็นค่าที่ยกกำลังสอง

def square(x):
    return x ** 2

เราอยากได้ผลลัพธ์จากการรัน square ไปทุกตัวบนลิสต์ numbers

results = [square(2), square(10), square(12), square(20)]

หรือ

results = [square(numbers[0]), square(numbers[1]), square(numbers[2]), square(numbers[3])]

แต่เนื่องจากเราไม่ทราบล่วงหน้าว่าลิสต์ numbers มีสมาชิกกี่ตัว เราจึงต้องเขียนโค้ดให้สามารถรัน square ไปบนสมาชิกทุกตัวของลิสต์ numbers ได้โดยไม่ต้องระบุจำนวนรอบของวงวนฟอร์ไว้ล่วงหน้า เราจึงจำเป็นต้องแม็ปฟังก์ชัน ซึ่งมี 3 วิธี ดังนี้

  1. วงวนฟอร์ซ้อนวงวนฟอร์: ใช้วงวนฟอร์แล้วเก็บผลลัพธ์ไว้ในลิสต์ที่ขึ้นมาเตรียมไว้

    results = []
    for ข้อมูลเดี่ยว in ลิสต์:
        ผลลัพธ์ = ฟังก์ชัน(ข้อมูลเดี่ยว)
        results.append(ผลลัพธ์)
    

    ตัวอย่าง เช่น

    numbers = [2, 10, 12, 20]
    results = []
    for number in numbers:
        results.append(square(number))
    print(results)
    

    ผลลัพธ์ที่จะแสดงผลบนหน้าจอ คือ

    [4, 100, 144, 400]
    
  2. ลิสต์คอมพริเฮนชัน

    results = [ฟังก์ชัน(ข้อมูลเดี่ยว) for ข้อมูลเดี่ยว in ลิสต์]
    

    ตัวอย่าง เช่น

    numbers = [2, 10, 12, 20]
    results = [square(number) for number in numbers]
    
  3. ฟังก์ชัน map: เป็นฟังก์ชันที่ใช้สำหรับการนำฟังก์ชันหนึ่งมาประยุกต์ใช้กับสมาชิกแต่ละตัวของคอลเลคชันที่ให้มา เช่น ลิสต์ ดิกชันนารี เซต โดยฟังก์ชัน map จะคืนค่าเป็น “map object” ซึ่งสามารถแปลงเป็นลิสต์ ด้วยการใช้คำสั่ง list() เพื่อให้สามารถดูผลลัพธ์ได้ในรูปแบบลิสต์

    โครงสร้างการใช้งาน map มีดังนี้

    results = list(map(ฟังก์ชัน, ลิสต์))
    

    ตัวอย่าง เช่น

    numbers = [2, 10, 12, 20]
    results = list(map(square, numbers))
    

    เราสามารถใช้วิธีเหล่านี้ในทำนองเดียวกันเพื่อแม็ปฟังก์ชันกับลิสต์ของลิสต์

วงวนซ้อนวงวน#

โครงสร้างของการใช้วงวนฟอร์เพื่อแม็ปฟังก์ชันไปบนลิสต์ซ้อนลิสต์ จะต้องมีการใช้วงวนซ้อนวงวน (nested for loop) ซึ่งมีโครงสร้างดังนี้

results = []
for ลิสต์ย่อย in ลิสต์ใหญ่:
    sub_results = []
    for ข้อมูลเดี่ยว in ลิสต์ย่อย:
        ผลลัพธ์ = ฟังก์ชัน(ข้อมูลเดี่ยว)
        sub_results.append(ผลลัพธ์)
    results.append(sub_results)

จุดที่ซับซ้อนและน่าสังเกตคือ การสร้างลิสต์ย่อย sub_results ขึ้นมาเพื่อเก็บผลลัพธ์ที่ได้จากการแม็ปฟังก์ชันกับสมาชิกแต่ละตัวในลิสต์ย่อย ถ้าเราแกะเอาวงวน ชั้นนอกออกเราก็จะเห็นว่าโครงสร้างนั้นเหมือนกับการแม็ปฟังก์ชันบนลิสต์ที่ไม่มีการซ้อนลิสต์

    sub_results = []
    for ข้อมูลเดี่ยว in ลิสต์ย่อย:
        ผลลัพธ์ = ฟังก์ชัน(ข้อมูลเดี่ยว)
        sub_results.append(ผลลัพธ์)

แต่เพราะว่าเราต้องการแม็ปฟังก์ชันไปบนลิสต์ย่อยทุกลิสต์ เราจึงต้องเอาโค้ดก้อนนี้ซ้อนเข้าไปในวงวนฟอร์ชั้นนอก 1 ชั้น

ตัวอย่าง

สมมติว่าเราต้องการหาความยาวของคำแต่ละคำในประโยคที่เก็บไว้ในลิสต์ของลิสต์ของสตริง เช่น ถ้าหาก input คือ

words = [['แซม','ซื้อ','tv','ใหม่'],['android', 'ไม่', 'ได้','ถูก', 'กว่า', 'iphone']]

โปรแกรมจะต้องคืนค่ามาเป็นลิสต์ของลิสต์ของตัวเลข ซึ่งตัวเลขแต่ละตัวคือความยาวของคำในแต่ละตำแหน่ง

[[3, 4, 2, 4], [7, 3, 3, 3, 4, 6]]

โจทย์ข้อนี้เราทราบแล้วว่าเราต้องมีการแม็ปฟังก์ชันโดยใช้คำสั่ง len ลงบนสมาชิกแต่ละตัวในลิสต์ย่อย ดังนั้นเราสามารถใช้โครงสร้างวงวนซ้อนวงวนมาประกอบกันได้ดังนี้

words = [['แซม','ซื้อ','tv','ใหม่'],['android', 'ไม่', 'ได้','ถูก', 'กว่า', 'iphone']]
lengths = []
for sen in words:
    sub_lengths = []
    for word in sen:
        sub_lengths.append(len(word))
    lengths.append(sub_lengths)
print(lengths)

ลิสต์คอมพริเฮนชันซ้อนใน#

เราสามารถใช้ลิสต์คอมพริเฮนชันซ้อนใน (nested list comprehension) ในการแม็ปฟังก์ชันได้ดังนี้

results = [[ฟังก์ชัน(ข้อมูลเดี่ยว) for ข้อมูลเดี่ยว in ลิสต์ย่อย] for ลิสต์ย่อย in ลิสต์ใหญ่]

สังเกตว่าเราซ้อนนิพจน์ [ฟังก์ชัน(ข้อมูลเดี่ยว) for ข้อมูลเดี่ยว in ลิสต์ย่อย] เข้าไปใน [นิพจน์ for ลิสต์ย่อย in ลิสต์ใหญ่]

ตัวอย่าง

words = [['แซม','ซื้อ','tv','ใหม่'],['android', 'ไม่', 'ได้','ถูก', 'กว่า', 'iphone']]
lengths = [[len(word) for word in sen] for sen in words]
print(lengths)

ทำลิสต์ให้แบนเรียบ#

การทำลิสต์ให้แบนเรียบ (flatten) คือ การเปลี่ยนลิสต์ที่เคยเป็นลิสต์ซ้อนลิสต์ให้เป็นลิสต์ธรรมดาที่เก็บข้อมูลเดี่ยวไว้ ในกรณีที่ข้อมูลที่เราได้รับมาเป็นข้อมูลที่มีโครงสร้างที่ซับซ้อนเกินไปอย่างลิสต์ของลิสต์ของคำซึ่งเก็บข้อมูลระดับประโยคไว้ด้วยว่ามีประโยคอยู่กี่ประโยค (ลิสต์ย่อย) และแต่ละประโยคมีคำอยู่กี่คำ (สตริงที่อยู่ในลิสต์ย่อย) หากเราไม่ต้องการข้อมูลในระดับประโยคเราอาจจะย่อลงให้เหลือเพียงลิสต์ของสตริงคำ โดยการทำลิสต์ให้เรียบ

โครงสร้างของโค้ดที่ใช้ในการทำลิสต์ให้เรียบ จะมีการใช้วงวนฟอร์ซ้อนวงวนฟอร์ซึ่งมีโครงสร้างดังนี้

results = []
for ลิสต์ย่อย in ลิสต์ใหญ่:
    for ข้อมูลเดี่ยว in ลิสต์ย่อย:
        results.append(ข้อมูลเดี่ยว)

หรือ จะใช้ลิสต์คอมพริเฮนชันซ้อนกันในการทำลิสต์ให้เรียบแบน ดังนี้

results = [ข้อมูลเดี่ยว for ลิสต์ย่อย in ลิสต์ใหญ่ for ข้อมูลเดี่ยว in ลิสต์ย่อย]

สังเกตว่าโครงสร้างลิสต์คอมพริเฮนชันซ้อนในแบบนี้มีลักษณะคล้ายคลึงกับ การแม็ปฟังก์ชันบนลิสต์ซ้อนในซึ่งมีโครงสร้างดังนี้

results = [[ฟังก์ชัน(ข้อมูลเดี่ยว) for ข้อมูลเดี่ยว in ลิสต์ย่อย] for ลิสต์ย่อย in ลิสต์ใหญ่]

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

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

illustration of the structures

ตัวอย่าง

words = [['แซม','ซื้อ','tv','ใหม่'],['android', 'ไม่', 'ได้','ถูก', 'กว่า', 'iphone']]
flattened_word_list = [word for sen in words for word in sen]
print(flattened_word_list)

ผลลัพธ์ที่ได้คือ

['แซม', 'ซื้อ', 'tv', 'ใหม่', 'android', 'ไม่', 'ได้', 'ถูก', 'กว่า', 'iphone']

ลิสต์ของทูเปิล#

ทูเปิลเป็นโครงสร้างข้อมูลที่เหมาะกับการเก็บข้อมูลหนึ่งหน่วยที่มีหลายมิติ เช่น พิกัด 1 พิกัด มีค่า x และ y ซึ่งเป็นตัวเลข 2 ค่า เช่น

coordinate = (3, 4)

หรือ คน 1 คน มีชื่อ และนามสกุล และชื่อเล่นซึ่งเป็นสตริง 3 สตริง เช่น

person_name = ('เศรษฐา', 'ทวีสิน', 'นิด')

เราใช้ลิสต์ของทูเปิลเวลาเราต้องการเก็บพิกัดหลาย ๆ พิกัด หรือชื่อนักเรียนหลาย ๆ คน เช่น

coordinates = [(3, 4), (5, 6), (7, 8)]
names = [('เศรษฐา', 'ทวีสิน', 'นิด'), ('ประยุทธ์', 'จันทร์โอชา', 'ตู่'), ('ยิ่งลักษณ์', 'ชินวัตร', 'ปู'), ('อภิสิทธิ์', 'เวชชาชีวะ', 'มาร์ค')]

ถ้าหากเราต้องการให้โค้ดดูอ่านง่ายขึ้น เราควรจัดรูปให้สมาชิกแต่ละตัวของลิสต์อยู่แยกบรรทัดกัน

coordinates = [
    (3, 4),
    (5, 6),
    (7, 8)
]
names = [
    ('เศรษฐา', 'ทวีสิน', 'นิด'),
    ('ประยุทธ์', 'จันทร์โอชา', 'ตู่'),
    ('ยิ่งลักษณ์', 'ชินวัตร', 'ปู'),
    ('อภิสิทธิ์', 'เวชชาชีวะ', 'มาร์ค')
]

เมื่อเราแจกแจงโครงสร้างดังโค้ดข้างต้น เราจะสังเกตได้ว่าข้อมูลที่เก็บอยู่ในตัวแปร names มีลักษณะดังนี้

  • โครงสร้างข้อมูลชั้นนอกสุดเป็นลิสต์ เพราะว่าข้อมูลถูกครอบอยู่ด้วย []

  • โครงสร้างข้อมูลชั้นในที่เป็นสมาชิกแต่ละตัวของลิสต์เป็นทูเปิล เพราะว่าข้อมูลถูกครอบอยู่ด้วย ()

  • ข้อมูลเดี่ยวชั้นในสุดเป็นสตริงเพราะมีการใช้ ''

คำถามชวนคิด

สมมติว่าเรามีข้อมูลต่อไปนี้

cities = [(37.774929, -122.419418), (13.756331, 100.501762)]

คำสั่งต่อไปนี้คืนค่าเป็นอะไร

  1. cities[0]

  2. cities[1][0]

เฉลย

คำสั่ง

คืนค่า

ประเภทข้อมูล

คำอธิบาย

cities[0]

(37.774929, -122.419418)

ทูเปิล

[0]เข้าถึงข้อมูลในลิสต์ของทูเปิล (cities) จึงแกะลิสต์ออกหนึ่งชั้น ได้ทูเปิล

cities[1][0]

13.756331

float

[1]เข้าถึงข้อมูลในลิสต์ของทูเปิล (cities) จึงแกะลิสต์ออกหนึ่งชั้น ได้ทูเปิล จากนั้น [0] เข้าถึงเลขทศนิยมในทูเปิล ได้เลขทศนิยม

ไล่วนซ้ำสมาชิกทุกตัวที่อยู่ในลิสต์ของทูเปิล#

เราสามารถไล่เข้าถึงสมาชิกทุกตัวที่อยู่ในลิสต์ของทูเปิลได้ด้วยวิธีเดียวกันกับการไล่บนลิสต์ของข้อมูลประเภทอื่น ๆ โดยใช้วงวนฟอร์เช่นเดิม ดังนี้

for ทูเปิล in ลิสต์ของทูเปิล:
    ทำอะไรกับทูเปิล

สมมติว่าเรามีลิสต์ของพิกัดละติจูดและลองจิจูดของเมือง และเราต้องการสร้างอีกลิสต์เพื่อเก็บเฉพาะเมืองที่อยู่ซีกโลกตะวันตก โดยใช้วงวนฟอร์ดังนี้

cities = [(37.774929, -122.419418), (13.756331, 100.501762)]
western_cities = []
for city in cities:
    if city[1] < 0:
        western_cities.append(city)

หรือใช้ลิสต์คอมพริเฮนชัน

cities = [(37.774929, -122.419418), (13.756331, 100.501762)]
western_cities = [city for city in cities if city[1] < 0]

เวลาเราวนซ้ำบนลิสต์ของทูเปิล ปัญหาที่เรามักจะพบคือเราอาจจะลืมว่าสมาชิกแต่ละตัวของทูเปิลเก็บค่าอะไรอยู่ เช่น เราอาจจะลืมว่า city[0] เก็บละติจูดหรือลองจิจูด ทำให้เราเขียนโปรแกรมผิดพลาดได้ง่าย ด้วยเหตุนี้เรานิยมขยายทูเปิลออก (unpack tuple) หรือกระจายค่าแต่ละค่าของทูเปิลแยกออกมาเก็บไว้ในตัวแปรแต่ละตัวก่อน แล้วจึงใช้ตัวแปรเหล่านั้นในการประมวลผลข้อมูล

จากตัวอย่างข้างต้นเราควรแก้ไขโค้ดให้อ่านง่ายขึ้นด้วยการกระจายค่าของทูเปิลออกมาเก็บไว้ในตัวแปรแต่ละตัวก่อน ดังนี้

cities = [(37.774929, -122.419418), (13.756331, 100.501762)]
western_cities = []
for city in cities:
    lat, lon = city
    if lon < 0:
        western_cities.append((lat, lon))

หรือกระจายค่าของทูเปิลออกมาเก็บไว้ในตัวแปรในวงวนฟอร์โดยไม่ต้องสร้างแปรมาเก็บทูเปิลเลย

cities = [(37.774929, -122.419418), (13.756331, 100.501762)]
western_cities = []
for lat, lon in cities:
    if lon < 0:
        western_cities.append((lat, lon))

หรือใช้ลิสต์คอมพริเฮนชัน

results = [นิพจน์ for ตัวแปร1, ตัวแปร2, ... , ตัวแปร n in ลิสต์ของทูเปิล]

เราสามารถแก้ไขโค้ดเป็น

cities = [(37.774929, -122.419418), (13.756331, 100.501762)]
western_cities = [(lat,lon) for lat, lon in cities if lon < 0]

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

cities = [(37.774929, -122.419418), (13.756331, 100.501762)]
western_cities = []
for city in cities:
	latitude, longitude, height = city
	if longitude < 0:
		western_cities.append(city)

เราจะได้ผลลัพธ์เป็น

ValueError: not enough values to unpack (expected 3, got 2)

เนื่องจากเราขยายทูเปิลออกไปยังตัวแปรสามตัวแปร แต่ว่าสมาชิกของทูเปิลมีเพียงสองตัว ภาษาไพทอนไม่ยอมให้ height เป็นค่า None เพื่อให้โปรแกรมดำเนินต่อไปได้ จึงแจ้ง ValueError ออกมา

หากทูเปิลแต่ละตัวมีจำนวนสมาชิกที่แตกต่างกัน

cities = [(37.774929, -122.419418, 100), (13.756331, 100.501762)]
western_cities = []
for city in cities:
	latitude, longitude, height = city
	if longitude < 0:
		western_cities.append(city)

เราจะได้ผลลัพธ์เป็นข้อผิดพลาดขึ้นมาเหมือนกัน

ValueError: not enough values to unpack (expected 3, got 2)

เพราะว่าเมื่อเราวนซ้ำไปถึงทูเปิล (13.756331, 100.501762) จำนวนตัวแปรที่เตรียมไว้ยังคงมีสามตัวเช่นเดิม แต่ว่าสมาชิกของทูเปิลมีสองตัวเท่านั้น

จากโค้ดข้างเราสังเกตได้ว่าค่าของ latitude ไม่ได้ถูกใช้ในการประมวลผลข้อมูลเลย แต่เราจำเป็นต้องสร้างตัวแปรเข้ามารองรับค่าทุกค่าของทูเปิล ในกรณีดังกล่าวเรามีธรรมเนียมปฏิบัติ (convention) ว่าจะตั้งชื่อตัวแปรที่ไม่ได้ใช้เป็น _ เพื่อบ่งบอกว่าตัวแปรนั้นตั้งมาเพื่อรับค่าไม่ให้เกิดข้อผิดพลาด แต่ไม่ได้นำไปประมวลผลต่อ ดังนั้นเราสามารถแก้ไขโค้ดจากตัวอย่างเป็น

cities = [(37.774929, -122.419418), (13.756331, 100.501762)]
western_cities = []
for city in cities:
    _, longitude = city
    if lon < 0:
        western_cities.append(city)

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

ลิสต์ของดิกชันนารี#

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

messages = [
    ('โกโก้', 'แม่เป็นกำลังใจของหนู')
    ('โกโก้', 'หนูมีแม่คนเดียว'), 
    ('คริส', 'ทุกคนมีแม่คนเดียวโกโก้')
    ]

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

messages = [
    {'speaker': 'โกโก้', 'message': 'แม่เป็นกำลังใจของหนู'}, 
    {'speaker': 'โกโก้', 'message': 'หนูมีแม่คนเดียว'}, 
    {'speaker': 'คริส', 'message': 'ทุกคนมีแม่คนเดียวโกโก้'}
    ]

โดยผิวเผินแล้วการเก็บข้อมูลทั้งสองวิธีนั้นเก็บข้อมูลได้ครบถ้วนเหมือนกัน ให้ลองเปรียบเทียบโค้ดในการแสดงผลข้อความให้ดูเป็นบทสนทนา

for m in messages:
    print(f'{m[0]}: {m[1]}')

กับ

for m in messages:
    print(f'{m["speaker"]}: {m["message"]}')

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

ดิกชันนารีที่เราใช้ลักษณะดังตัวอย่างข้างต้น จะมีคีย์ที่เป็นสตริงที่เป็นเขตข้อมูล หรือฟีลด์ (field) บ่งบอกว่าข้อมูลที่เป็น แวลูเป็นข้อมูลอะไร เช่น ดิกชันนารีในตัวอย่างข้างต้นมี 2 ฟีลด์ คือ speaker และ message ซึ่งบ่งบอกว่าแวลูของดิกชันนารีคือผู้พูด และข้อความที่คนนั้นพูด เราขอเรียกดิกชันนารีที่มีคีย์เป็นฟีลด์ว่า ดิกชันนารีคู่ฟีลด์แวลู (field-value dictionary)

ตัวอย่างที่แล้วอาจจะยังไม่ได้ทำให้เห็นภาพว่าเหตุใดเราจึงจำเป็นต้องใช้ดิกชันนารีคู่ฟีลด์แวลู ให้ลองพิจารณากรณีที่เราต้องเก็บทวิตจำนวนมาก

[ {'id': 'BarackObama', 'date': 'October 17, 2019', 'num comments': 243000,
  'num retweets': 37200, 'num fav': 250300, 'text': 'I was proud to work with Justin Trudeau as President'},
 {'id': 'SpeakerPelosi', 'date': 'October 17, 2019', 'num comments': 22700,
  'num retweets': 10000, 'num fav': 45600, 'text': 'What courage does it take to pass legislation that will save lives?'},
 {'id': 'BarackObama', 'date': 'October 15, 2019', 'num comments': 2000,
  'num retweets': 5400, 'num fav': 52800, 'text': 'In December, Michelle and I will head to Malaysia for the first @ObamaFoundation Leaders: Asia-Pacific gathering'}
]

ดิกชันนารีที่เราใช้ในตัวอย่างข้างต้นเป็น ดิกชันนารีคู่ฟีลด์แวลู ที่มีฟีลด์ดังต่อไปนี้

  • id บ่งบอกว่าทวิตนั้นมาจากบัญชีใคร

  • date บ่งบอกว่าทวิตนั้นถูกโพสต์เมื่อวันที่ไหน

  • num comments บ่งบอกว่าทวิตนั้นมีคอมเมนต์กี่คอมเมนต์

  • num retweets บ่งบอกว่าทวิตนั้นถูกรีทวิตกี่ครั้ง

  • num fav บ่งบอกว่าทวิตนั้นทำให้เป็น favorite โดยการกดปุ่มหัวใจบนแอปกี่ครั้ง

  • text บ่งบอกว่าทวิตนั้นมีข้อความอะไร

สมมติต่ออีกว่าเราต้องการเขียนโปรแกรมเพื่อหาว่าแต่ละคนถูกรีทวิตทั้งหมดกี่ครั้ง

import collections
counter = collections.Counter()
for tweet in tweet_list:
  twitter_id = tweet['id']
  retweets = tweet['num retweets']
  counter[twitter_id] += retweets
print(counter.most_common())

ผลลัพธ์ที่ได้คือ

[('BarackObama', 42600), ('SpeakerPelosi', 10000)]

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

counter = collections.Counter()
for tweet in tweet_list:
  twitter_id = tweet[0]
  retweets = tweet[3]
  counter[twitter_id] += retweets

หากเราต้องกลับมาอ่านโค้ดของเราเอง เราก็อาจจะลืมไปแล้วว่าตำแหน่งที่ 0 และ 3 เก็บอะไรไว้อยู่ หรือถ้าส่งต่อให้ผู้อื่นในทีมเพื่อตรวจสอบหรือเขียนโค้ดต่อจากเรา ก็จะเข้าใจยากด้วยเหตุผลเดียวกัน ปัญหาอีกประการหนึ่งคือ ถ้าหากเพิ่มข้อมูลเข้าไปในทูเปิลก็อาจจะทำให้ตำแหน่งที่เก็บเปลี่ยนไปได้ เนื่องจากทูเปิลเป็นโครงสร้างข้อมูลแบบมีลำดับ จากตัวอย่างนี้ทำให้เห็นว่าถ้าหากข้อมูลหนึ่งตัวเก็บข้อมูลอยู่หลายอย่างในลักษณะคล้ายกับกับการกรอกฟอร์มที่มีหลาย ๆ ฟีลด์ การใช้ดิกชันนารีคู่ฟีลด์แวลูเป็นวิธีที่ดีกว่าการใช้ทูเปิล

ดิกชันนารีที่คีย์เป็นทูเปิล#

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

../../_images/dict-tuple.jpeg

ภาพที่ 14 ภาพอธิบายโครงสร้างของดิกชันนารี#

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

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

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

สมมติว่าเราต้องการเก็บข้อมูลทำสมุดโทรศัพท์มีตัวอย่างโครงสร้างข้อมูลแบบต่าง ๆ ดังนี้

entry_list = [ ('Mark', 'Wahlberg', '111-222-3333'),
  ('Jane', 'Doe', '222-333-4444'),
  ('Jane', 'Eyre', '333-444-5555')
]

friend_to_phone = { ('Mark', 'Wahlberg') : '111-222-3333',
  ('Jane', 'Doe'): '222-333-4444',
  ('Jane', 'Eyre'): '333-444-5555'
}

ตัวแปร entry_list มีโครงสร้างข้อมูลเป็นลิสต์ของทูเปิลโดยที่ทูเปิลเก็บข้อมูลชื่อ นามสกุล และเบอร์โทรศัพท์เป็นสตริง วิธีการดึงข้อมูลเบอร์โทรศัพท์จากตัวแปร entry_list มีดังนี้

phone_list = []
for first, last, phone in entry_list:
	phone_list.append(phone)

หรือใช้ลิสต์คอมพริเฮนชัน

phone_list = [phone for first, last, phone in entry_list]

ตัวแปร friend_to_phone มีโครงสร้างข้อมูลเป็นดิกชันนารีที่มีคีย์เป็นทูเปิล และมีแวลูเป็นสตริง วิธีการดึงข้อมูลเบอร์โทรจากตัวแปร friend_to_phone มีดังนี้

phone_list = list(friend_to_phone.values())

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

def find_mark_from_list(entry_list):
query = ('Mark', 'Wahlberg')
for entry in entry_list:
	first, last, phone = entry
	first_last = (first, last)
	if query == first_last: 
		return phone

คำถามชวนคิด

จากโปรแกรมข้างต้น ตัวแปรต่อไปนี้ชนิดข้อมูลเป็นอะไร

  1. entry

  2. first

เฉลย

  1. ทูเปิล

  2. สตริง

ให้เปรียบเทียบวิธีข้างต้นกับวิธีที่ 2 ซึ่งเราเก็บข้อมูลในดิกชันนารี

def find_mark_from_dict(friend_to_phone):
    query = ('Mark', 'Wahlberg')
    return friend_to_phone[query]

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

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

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

ดิกชันนารีที่แวลูเป็นลิสต์#

การเก็บข้อมูลทำสมุดโทรศัพท์ในกรณีที่คนหนึ่งคนอาจมีเบอร์โทรศัพท์หลายเบอร์ด้วยโครงสร้างข้อมูลแบบลิสต์ของทูเปิลและดิกชันนารีที่มีแวลูเป็นลิสต์ เขียนได้ดังนี้

entry_list = [ ('Mark', 'Wahlberg', '111-222-3333', '444-555-6666'),
  ('Jane', 'Doe', '222-333-4444'),
  ('Jane', 'Eyre', '333-444-5555', '777-555-1111')
]

name_to_phone_list = { ('Mark', 'Wahlberg') : ['111-222-3333', '444-555-6666'] ,
  ('Jane', 'Doe'): ['222-333-4444'],
  ('Jane', 'Eyre'): ['333-444-5555', '777-555-1111']
}

การที่บางคนอาจมีเบอร์โทรศัพท์หลายเบอร์จะเป็นปัญหากับตัวแปร entry_list ที่เป็นลิสต์ของทูเปิลในการเปิดหาเบอร์โทรทั้งหมด เนื่องจากทูเปิลแต่ละตัวในลิสต์มีจำนวนสมาชิกไม่เท่ากัน จึงทำให้ไม่สามารถขยายทูเปิลออกแบบเดิมได้ ตัวแปร name_to_phone_list ซึ่งเป็นดิกชันนารีที่มีคีย์เป็น tuple และมีแวลูเป็นลิสต์ของสตริงที่เก็บข้อมูลเบอร์โทรจึงเป็นทางเลือกที่เหมาะสมในกรณีนี้

ตัวอย่างการเขียนโปรแกรมเพื่อค้นหาเบอร์โทรของคนหนึ่งคน

results = []
first_last = ('Mark', 'Wahlberg')
phone_list = name_to_phone_list[first_last]
for phone in phone_list:
	results.append(phone)

จากโปรแกรมข้างต้นจะเป็นการค้นหาเบอร์โทรของ Mark Wahlberg จึงใช้คีย์('Mark', 'Wahlberg') ในการเข้าถึงแวลูซึ่งเป็นลิสต์ของสตริงที่เป็นเบอร์โทรด้วยคำสั่ง name_to_phone_list[first_last] จากนั้นใช้วงวนฟอร์วนสตริงแต่ละตัวในลิสต์ของสตริงนั้นโดยใช้ตัวแปร phone มารับค่าสตริงและเพิ่มเข้าไปในลิสต์ผลลัพธ์ของเรา

เราจะได้ผลลัพธ์ลัพธ์ของ results เป็น

['111-222-3333', '444-555-6666']

ในการปรับโค้ดให้เราสามารถเปิดค้นหาเบอร์ของทุกคนได้ เราจะใช้โครงสร้างวงวนซ้อนวงวน โดยเพิ่มวงวนฟอร์ชั้นนอกเข้ากับโค้ดข้างต้น ดังนี้

results = []
first_last_list = list(name_to_phone_list.keys()) 
for first_last in first_last_list:
	phone_list = name_to_phone_list[first_last]
	for phone in phone_list:
		results.append(phone)

คำถามชวนคิด

จากโปรแกรมข้างต้น คำสั่งต่อไปนี้คืนค่าเป็น type อะไร

  1. name_to_phone_list[first_last]

  2. list(name_to_phone_list.keys())

เฉลย

  1. ลิสต์ของสตริง โดยที่สตริงเป็นเบอร์โทร

  2. ลิสต์ของทูเปิล โดยที่ทูเปิลเป็นทูเปิลของสตริงสองตำแหน่งที่เป็นชื่อและนามสกุล

ตัวแปร first_last_list ซึ่งเป็นลิสต์ของทูเปิลจะเก็บคีย์ทั้งหมดของดิกชันนารี name_to_phone_list ไว้ จากนั้น วงวนฟอร์ชั้นนอกจะวนไปที่ลิสต์ของทูเปิลนี้ ใช้ตัวแปร first_last เก็บคีย์ของดิกชันนารีแต่ละตัวไว้ เพื่อนำไปใช้ในคำสั่ง name_to_phone_list[first_last] ในการเรียกคืนค่าแวลูที่เป็นลิสต์ของสตริงที่เก็บเบอร์โทรไว้ และเก็บไว้ในตัวแปร phone_list จากนั้นจะเข้าสู่วงวนฟอร์ชั้นในเหมือนโปรแกรมก่อนหน้า

จากโปรแกรมทั้งหมด สามารถเขียนให้กระชับได้เป็น

results = []
for first_last in name_to_phone_list:
	for phone in name_to_phone_list[first_last]:
		results.append(phone)

เนื่องจากเวลาวงวนฟอร์ไปบนดิกชันนารี สิ่งที่เราได้ออกมาจะเป็นคีย์ดังนั้นจึงไม่จำเป็นต้องใช้คำสั่ง list(name_to_phone_list.keys()) เพื่อทำลิสต์ของคีย์เหมือนโค้ดก่อนหน้า

ดิกชันนารีที่แวลูเป็นดิกชันนารี#

ในการเก็บข้อมูลทำสมุดโทรศัพท์บางทีเราอาจอยากได้ข้อมูลช่องทางติดต่ออื่น ๆ นอกจากเบอร์โทร การใช้ดิกชันนารีที่มีแวลูเป็น ดิกชันนารีคู่ฟีลด์แวลู โดยที่แวลูแต่ละตัวจะมีฟีลด์ที่เหมือนกันทั้งหมด ทำให้เราสามารถเพิ่มช่องทางติดต่ออื่น ๆ ที่เราต้องการได้โดยง่าย เช่น ในตัวอย่างมีฟีลด์ที่เป็น 'phone' และ 'line'

name_to_info = { 
  ('Mark', 'Wahlberg') : {'phone': ['111-222-3333', '444-555-6666'] , 'line': 'itsmemark'},
  ('Jane', 'Doe'): {'phone': ['222-3333-44444'] , 'line': 'janehomegirl'}
  ('Jane', 'Eyre'): {'phone': ['333-444-5555', '777-555-1111'], 'line': 'eyesonme'}
}

คำถามชวนคิด

จากโปรแกรมข้างต้น คำสั่งต่อไปนี้คืนค่าเป็น type อะไร

  1. name_to_info[('Jane', 'Doe')]

  2. name_to_info[('Mark', 'Wahlberg')]['phone']

  3. name_to_info[('Mark', 'Wahlberg')]['phone'][0]

  4. name_to_info[('Jane', 'Eyre')]['address']

เฉลย

คำสั่ง

คืนค่า

ประเภทข้อมูล

คำอธิบาย

name_to_info[('Jane', 'Doe')]

{'phone': ['222-3333-44444'] , 'line': 'janehomegirl'}

ดิกชันนารีคู่ฟีลด์แวลู

('Jane', 'Doe')เป็นคีย์เข้าถึงข้อมูลในดิกชันนารี (name_to_info) จึงได้แวลูที่เป็น ดิกชันนารีคู่ฟีลด์แวลู

name_to_info[('Mark', 'Wahlberg')]['phone']

['111-222-3333', '444-555-6666']

ลิสต์ของสตริง

('Mark', 'Wahlberg')เป็นคีย์เข้าถึงข้อมูลในดิกชันนารี (name_to_info) จึงได้แวลูที่เป็น ดิกชันนารีคู่ฟีลด์แวลู จากนั้น ['phone'] เป็นฟีลด์จึงได้แวลูเป็นลิสต์ของสตริง

name_to_info[('Mark', 'Wahlberg')]['phone'][0]

'111-222-3333'

สตริง

('Mark', 'Wahlberg')เป็นคีย์เข้าถึงข้อมูลในดิกชันนารี (name_to_info) จึงได้แวลูที่เป็น ดิกชันนารีคู่ฟีลด์แวลู จากนั้น ['phone'] เป็นฟีลด์จึงได้แวลูเป็นลิสต์ของสตริง จากนั้น [0] เข้าถึงข้อมูลในลิสต์ของสตริง ได้สตริง

name_to_info[('Jane', 'Eyre')]['address']

KeyError

-

('Jane', 'Eyre')เป็นคีย์เข้าถึงข้อมูลในดิกชันนารี (name_to_info) จึงได้แวลูที่เป็น ดิกชันนารีคู่ฟีลด์แวลู จากนั้นเพราะ ['address'] ไม่ได้เป็นฟีลด์ใน ดิกชันนารีคู่ฟีลด์แวลู จึงได้ KeyError

ตัวอย่าง: การนับไบแกรม#

ไบแกรม (bigram) คือ คำสองคำที่อยู่ติดกันในข้อความ เราสามารถเก็บไบแกรมและจำนวนครั้งที่เจอ ในโครงสร้างข้อมูลดังนี้

bigram = { ('a', 'dog') : 1235,
  ('a', 'very') : 2356,
  ('in', 'the'): 6222,
  ('in', 'a'): 8533,
  ('in', 'here'): 233,
  ('in', 'my'): 551
}

ข้อมูลข้างต้นถูกเก็บในดิกชันนารีที่มีลักษณะดังนี้

  • คีย์เป็นทูเปิลของสตริงสองตำแหน่ง ซึ่งแสดงถึงไบแกรม

  • แวลูเป็นตัวเลข ซึ่งแสดงถึงจำนวนครั้งที่พบไบแกรมนั้น

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

bigram = { 'a': {'dog': 1235,
        'very' : 2356},
  'in': {'the' : 6222,
         'a'   : 8533,
         'here': 233,
         'my' : 551} 
}

ข้อมูลข้างต้นเป็นดิกชันนารีที่มีลักษณะดังนี้

  • คีย์เป็นสตริง ซึ่งแสดงถึงคำแรกในไบแกรม

  • แวลูเป็นดิกชันนารีที่รวมคำทั้งหมดที่สามารถต่อท้ายคำแรกในไบแกรมได้ โดยมีลักษณะดังนี้

    • คีย์เป็นสตริง ซึ่งแสดงถึงคำที่สองในไบแกรม

    • แวลูเป็นตัวเลข ซึ่งแสดงถึงจำนวนครั้งที่พบไบแกรมนั้น

โครงสร้างข้อมูลลักษณะนี้นอกจากจะทำให้เรารู้จำนวนครั้งที่พบไบแกรมแล้ว ยังทำให้เรารู้ได้ว่าคำ ๆ หนึ่งสามารถต่อท้ายด้วยคำว่าอะไรได้บ้าง เช่น หากอยากรู้ว่าคำว่า ‘in’ มีคำอะไรต่อท้ายได้บ้าง สามารถเขียนโปรแกรมได้ ดังนี้

bigram['in']

จะได้ผลลัพธ์ออกมาเป็น

{'the' : 6222,
  'a'   : 8533,
  'here': 233,
  'my' : 551} 

ซึ่งแสดงให้เห็นว่าคำ ‘in’ ต่อท้ายด้วยคำ ‘the’ 6222 ครั้ง ต่อท้ายด้วยคำว่า ‘a’ 8533 ครั้ง ต่อท้ายด้วยคำว่า ‘here’ 233 ครั้ง และคำว่า ‘my’ 551 ครั้ง

สรุป#

ลิสต์ของลิสต์ ลิสต์ของทูเปิล ลิสต์ของดิกชันนารีเป็นโครงสร้างข้อมูลที่ชั้นนอกสุดคือลิสต์ที่มีหน้าที่เก็บข้อมูลที่มีลักษณะคล้ายกัน เช่น ลิสต์ที่เก็บข้อมูลเดี่ยวประเภทเดียวกัน ทูเปิลที่มีจำนวนสมาชิกเท่ากัน และข้อมูลเดี่ยวประเภทเดียวกัน ดิกชันนารีที่มีคีย์เหมือนกัน เป็นการเพิ่มกรณีการใช้งานของลิสต์จากที่เราเคยใช้เก็บข้อมูลเดี่ยว เพิ่มมาเป็นการเก็บข้อมูลแบบคอลเลกชัน

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