บทนี้นำเสนอชนิดข้อมูลสำเร็จที่มีอยู่ในตัว (built-in type) อีกชนิด ที่เรียกว่า ดิกชันนารี (dictionary) ดิกชันนารี เป็นลักษณะเฉพาะของไพธอนที่จัดว่าดีที่สุดอันหนึ่ง มันเป็นเสมือนส่วนประกอบพื้นฐานของอัลกอริทึมที่มีประสิทธิภาพที่แจ๋วๆ ต่างๆ
ดิกชันนารีคล้ายกับลิสต์ แต่ใช้งานได้กว้างขวางกว่า สำหรับลิสต์ ดัชนีต้องเป็นเลขจำนวนเต็มเท่านั้น แต่ดิกชันนารีสามารถกำหนดใช้ดัชนีเป็นข้อมูลใดก็ได้ (เกือบทุกชนิด)
ดิกชันนารีประกอบไปด้วยกลุ่มหมู่ของดัชนีต่างๆ เรียกว่า กุญแจ (keys) และกลุ่มหมู่ของค่าต่างๆ ที่เชื่อมโยงกับดัชนี แต่ละกุญแจจะเชื่อมไปหาค่า (value) การเชื่อมโยงระหว่างกุญแจกับค่าจะเรียกว่า คู่กุญแจกับค่า (key-value pair) หรือบางครั้งอาจเรียก รายการ (item)
หากพูดในเชิงคณิตศาสตร์ ดิกชันนารีก็คือการแปลง (mapping) จาก กุญแจ (keys) ไปเป็น ค่า (values) ซึ่งเราสามารถพูดได้ว่า แต่ละดัชนีกุญแจ “แปลงไปเป็น” ค่า ดังตัวอย่างนี้ เราจะสร้างดิกชันนารีที่แปลงจากคำในภาษาอังกฤษไปเป็นคำในภาษาสเปน ดังนั้น ทั้งกุญแจและค่า จะเป็นชนิดข้อมูลสายอักขระ
ฟังก์ชัน dict
สร้างดิกชันนารีใหม่ขึ้นมา โดยยังไม่มีรายการใดๆ อยู่ภายใน เนื่องจาก dict
เป็นชื่อของฟังก์ชันสำเร็จ เราจึงต้องไม่ใช้มันไปตั้งเป็นชื่อตัวแปร
>>> eng2sp = dict() >>> eng2sp {}
วงเล็บปีกกา {}
แทนดิกชันนารีที่ว่างอยู่ เพื่อจะเพิ่มรายการเข้าไปในดิกชันนารี เราสามารถใช้วงเล็บสี่เหลี่ยมดังนี้:
>>> eng2sp['one'] = 'uno'
คำสั่งบรรทัดนี้สร้างรายการขึ้นมา โดยแปลงจากกุญแจ 'one'
ไปเป็นค่า 'uno'
ถ้าเราสั่งพิมพ์ค่าดิกชันนารีออกมาดู เราจะเห็น รายการคู่กุญแจค่า ที่มีเครื่องจุดคู่ (ทวิภาค หรือ colon) คั่นระหว่างกุญแจกับค่า:
>>> eng2sp {'one': 'uno'}
รูปแบบเอาต์พุตนี้ ก็สามารถใช้เป็นรูปแบบอินพุตได้ เช่น เราสามารถสร้างดิกชันนารีใหม่ ที่มีสามรายการได้ดังนี้:
>>> eng2sp = {'one': 'uno', 'two': 'dos', 'three': 'tres'}
แต่ถ้าเราสั่งพิมพ์ eng2sp
ออกมาดู เราอาจจะแปลกใจที่เห็น:
>>> eng2sp {'one': 'uno', 'three': 'tres', 'two': 'dos'}
ลำดับของรายการคู่กุญแจค่าอาจจะไม่เหมือนเดิม หรือแม้แต่เวลาที่คุณไปทดลองตัวอย่างนี้ในเครื่องของคุณ คุณก็อาจจะได้ผลลัพธ์ต่างออกไปก็ได้ โดยทั่วไปแล้ว ลำดับของรายการในดิกชันนารี อาจเปลี่ยนแปลงไป
แต่ลำดับของรายการในดิกชันนารีไม่ใช่ปัญหา เพราะว่ารายการต่างๆ ในดิกชั่นารีจะไม่ถูกอ้างจากดัชนีลำดับ สำหรับดิกชันนารี เราจะใช้ดัชนีกุญแจเพื่อหาค่าที่เป็นผูกกับมัน:
>>> eng2sp['two'] 'dos'
กุญแจ 'two'
จะแปลงไปเป็นค่า 'dos'
เสมอ ดังนั้น ลำดับของรายการจึงไม่สำคัญ
ถ้าหากกุญแจไม่มีอยู่ในดิกชันนารี เราจะได้รับแจ้งข้อผิดพลาดมาแทน
>>> eng2sp['four'] KeyError: 'four'
ฟังก์ชัน len
ก็ได้งานได้กับดิกชันนารี มันจะให้ค่าจำนวนของรายการคู่กุญแจค่าออกมา:
>>> len(eng2sp) 3
ตัวดำเนินการ in
ก็ทำงานได้กับดิกชันนารี มันจะบอกเราว่า สิ่งที่เราสงสัยมีปรากฏเป็น กุญแจ (key) ในดิกชันนารีหรือไม่ (ปรากฏเป็นค่าไม่ดีพอ)
>>> 'one' in eng2sp True >>> 'uno' in eng2sp False
เพื่อจะดูว่ามีอะไรเป็นค่าของกุญแจในดิกชันนารีบ้าง เราสามารถใช้เมธอด values
ที่ให้ค่ากุญแจต่างๆ ในดิกชันนารีออกมา จากนั้น เราสามารถใช้ตัวดำเนินการ in
ได้:
>>> vals = eng2sp.values() >>> 'uno' in vals True
ตัวดำเนินการ in
ใช้อัลกอริทึมสำหรับดิกชันนารี ต่างกับอัลกอริทึมที่ใช้สำหรับลิสต์ สำหรับลิสต์ มันจะค้นหาอิลิเมนต์ของลิสต์ตามลำดับ เช่นในหัวข้อ 8.6 ถ้าลิสต์ยาวขึ้น เวลาในการค้นหาก็จะนานขึ้น เวลาค้นหากับความยาวลิสต์เป็นสัดส่วนกันโดยตรง
สำหรับดิกชันนารี ไพธอนใช้อัลกิริธึมที่เรียกว่า ตารางแฮช (hashtable) ซึ่งมีคุณสมบัติที่ยอดเยี่ยม นั่นคือ ตัวดำเนินการ in
ใช้เวลาพอๆ กันในการค้นหา ไม่ว่าดิกชันนารีจะมีรายการอยู่มากเท่าไร รายละเอียดของตารางแฮชอธิบายอยู่ในหัวข้อ B.4 แต่คำอธิบายอาจจะเข้าใจได้ยาก หากไม่ได้ศึกษาบทต่อๆ ไปก่อน
สมมติว่าเราได้สายอักขระมา และเราต้องการนับจำนวนว่าตัวอักษรแต่ละตัวมีปรากฎอยู่ในสายอักขระนั้นอีกครั้ง มีหลายวิธีที่เราทำได้ เช่น:
if-elif-else
) ช่วยord
) และใช้ตัวเลขที่ได้เป็นดัชนีของลิสต์ และเพิ่มจำนวนนับในลิสต์แต่ละวิธีจะทำงานเดียวกัน แต่ว่า ทำด้วยวิธีที่ต่างกัน
วิธีการสร้างโปรแกรม (implementation) เป็นวิธีที่จะทำงาน วิธีที่จะทำการคำนวณ วิธีที่จะเขียนโปรแกรม บางวิธีจะดีกว่าวิธีอื่น ตัวอย่าง เช่น ข้อดีของวิธีที่ใช้ดิกชันนารีคือ เราไม่จำเป็นต้องรู้ก่อนว่า ตัวอักษรมีอยู่ในสายอักขระหรือไม่ เราแค่สร้างที่เก็บตัวนับให้มัน ถ้ามันมีปรากฏอยู่ หน้าตาของโปรแกรมอาจจะเป็นดังนี้:
def histogram(s): d = dict() for c in s: if c not in d: d[c] = 1 else: d[c] += 1 return d
ชื่อของฟังก์ชันข้างต้นคือ histogram
ซึ่งเป็นคำศัพท์ทางสถิติที่ใช้สำหรับกลุ่มหมู่ของตัวนับ (หรือ ความถี่)
บรรทัดแรกของฟังก์ชันสร้างดิกชันนารีว่างๆ ขึ้นมา คำสั่งลูป for
ไล่สำรวจอักษรต่างๆ ในสายอักขระ แต่ละรอบในลูป ถ้าตัวอักษรของตัวแปร c
ไม่อยู่ในดิกชันนารี เราจะสร้างรายการใหม่ด้วยกุญแจ c
และให้ค่าเริ่มต้นเป็น 1 (เนื่องจากเราเจอตัวอักษรแล้วหนึ่งครั้ง) ถ้าตัวอักษรของตัวแปร c
มีอยู่แล้วในดิกชันนารี เราก็เพิ่มค่า d[c]
มันทำงานดังนี้:
>>> h = histogram('brontosaurus') >>> h {'a': 1, 'b': 1, 'o': 2, 'n': 1, 's': 2, 'r': 2, 'u': 2, 't': 1}
ฮิสโตแกรมที่ได้บอกวว่า อักษร a
และอักษร b
มีปรากฎหนึ่งครั้ง อักษร o
มีปรากฎสองครั้ง เป็นต้น
ดิกชันนารีมีเมธอด get
ที่รับกุญแจ และค่าโดยปริยาย (ค่าดีฟอลท์) ของมัน ถ้ากุญแจนั้นมีอยู่ในดิกชันนารีอยู่แล้ว get
จะให้ค่าของกุญแจนั้นออกมา ไม่อย่างนั้นก็จะให้ค่าโดยปริยายออกมา ตัวอย่าง:
>>> h = histogram('a') >>> h {'a': 1} >>> h.get('a', 0) 1 >>> h.get('b', 0) 0
เพื่อเป็นการฝึกใช้ ให้ลองใช้ get
เพื่อเขียน histogram
ให้กระชับยิ่งขึ้น เราน่าจะสามารถเอาคำสั่ง if
ออกไปได้
ถ้าเราใช้ดิกชันนารีในคำสั่ง for
มันจะท่องสำรวจกุญแจของดิกชันนารี ตัวอย่าง print_hist
พิมพ์กุญแจ และค่าของกุญแจ แต่ละรายการออกมา:
def print_hist(h): for c in h: print(c, h[c])
ผลลัพธ์ที่ได้จะเป็นดังนี้:
>>> h = histogram('parrot') >>> print_hist(h) a 1 p 1 r 2 t 1 o 1
ย้ำอีกครั้งว่า กุญแจจะไม่ได้เรียงตามลำดับใดๆ ถ้าหากต้องการทำลูปสำรวจกุญแจตามลำดับ เราสามารถใช้ฟังก์ชันสำเร็จ sorted
:
>>> for key in sorted(h): ... print(key, h[key]) a 1 o 1 p 1 r 2 t 1
ถ้าให้ดิกชันนารี d
และกุญแจ k
เราสามารถหาค่าของกุญแจ v = d[k]
ได้ง่ายๆ การดำเนินการแบบนี้เรียกว่า การเทียบค้น (lookup)
แต่หากเรามี v
และต้องการหาค่า k
หละ? เราเจอปัญหาสองอย่างคือ: หนึ่ง มันอาจจะมีกุญแจมากกว่าหนึ่งกุญแจที่เชื่อมไปหาค่า v
เหมือนกัน อันนี้ก็ขึ้นกับงาน เราอาจจจะเลือกสักกุญแจหนึ่ง หรือ เราอาจจะสร้างลิสต์ของกุญแจทั้งหมดของค่านั้นขึ้นมา สอง ไม่มีคำสั่งง่ายๆ ที่จะทำการเทียบค้นย้อนกลับให้ เราต้องค้นหาเอง
ข้างล่างนี้ คือ ฟังก์ชันที่รับค่าของกุญแจ และส่งกุญแจแรกที่เจอออกมา:
def reverse_lookup(d, v): for k in d: if d[k] == v: return k raise LookupError()
ฟังก์ชันนี้เป็นอีกตัวอย่างหนึ่งของการค้นหารูปแบบ เพียงแต่ว่า มีการใช้คำสั่งใหม่ คำสั่ง raise
คำสั่ง raise (raise statement) จะทำให้เกิดเอ็กเซ็ปชั่น (exception) ขึ้น ในกรณีนี้ มันจะไปเรียก LookupError
ซึ่งเป็นเอ็กเซ็ปชั่นสำเร็จรูป (built-in exception) เพื่อแจ้งว่าการดำเนินการค้นหาล้มเหลว
ถ้าเราค้นหาไปจนสุดจบลูป นั่นหมายความว่า v
ไม่ได้ปรากฏเป็นค่าในดิกชันนารี ดังนั้นเราจึงส่งเอ็กเซ็ปชั่นออกไป
ตัวอย่างนี้แสดง การเทียบค้นย้อนกลับที่สำเร็จ:
>>> h = histogram('parrot') >>> key = reverse_lookup(h, 2) >>> key 'r'
และนี่คือตัวอย่างอันที่ไม่สำเร็จ:
>>> key = reverse_lookup(h, 3) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 5, in reverse_lookup LookupError
ผลลัพธ์จากที่เราส่งเอ็กเซ็ปชั่นออกมา จะเหมือนกับตอนที่ไพธอนส่งออกมา: มันจะพิมพ์ข้อความสืบย้อน (trackback message) และข้อความแสดงข้อผิดพลาดออกมา (error message)
คำสั่ง raise
สามารถรับข้อความแสดงข้อผิดพลาด เป็นอาร์กิวเมนต์เสริมได้ ตัวอย่างเช่น:
>>> raise LookupError('value does not appear in the dictionary') Traceback (most recent call last): File "<stdin>", line 1, in ? LookupError: value does not appear in the dictionary
การเทียบค้นย้อนกลับจะช้ากว่าการเทียบค้นธรรมดามาก ถ้าเราต้องทำบ่อยๆ หรือถ้าดิกชันนารีมีขนาดใหญ่ ประสิทธิภาพของโปรแกรมจะแย่มาก.
ลิสต์สามารถเป็นค่าในดิกชันนารีได้ ตัวอย่างเช่น ถ้าเราได้ดิกชันนารี ที่แปลงจากตัวอักษรไปเป็นความถี่ เราอาจจะต้องการทำผกผันมัน นั่นคือ สร้างดิกชันนารี ที่แปลงจากความถี่ไปเป็นตัวอักษร
เนื่องจากอาจจะมีอักษรหลายตัวที่มีความถี่เดียวกัน แต่ละค่าของดิกชันนารีผกผัน (inverted dictionary ซึ่งคือ ดิกชันนารีที่แปลงจากความถี่ ไปเป็นตัวอักษร) ควรจะเป็นลิสต์ของตัวอักษร
นี่เป็นฟังก์ชันที่ทำผกผันดิกชันนารี:
def invert_dict(d): inverse = dict() for key in d: val = d[key] if val not in inverse: inverse[val] = [key] else: inverse[val].append(key) return inverse
แต่ละรอบของลูป key
รับกุญแจจาก d
และ val
รับค่าที่คู่กับกุญแจ ถ้าค่า val
ไม่อยู่ใน inverse
นั่นหมายความว่า เราไม่เคยเจอมันมาก่อน ดังนั้นเราจะสร้างรายการใหม่ และให้ค่าเริ่มต้นมันเป็น เซตโทน (singleton ซึ่งคือลิสต์ที่มีอิลิเมนต์เดียว) ถ้าไม่อย่างนั้น ก็คือเราเคยเห็นค่านี้มาก่อน ดังนั้นเราจะเพิ่มค่านี้เข้าไปในลิสต์
ตัวอย่าง:
>>> hist = histogram('parrot') >>> hist {'a': 1, 'p': 1, 'r': 2, 't': 1, 'o': 1} >>> inverse = invert_dict(hist) >>> inverse {1: ['a', 'p', 't', 'o'], 2: ['r']}
รูป 11.1 เป็นแผนภาพสถานะ ที่แสดง hist
และ inverse
ดิกชันนารี แสดงด้วยกล่องที่มีชนิด dict
อยู่ข้างบน และมีคู่กุญแจ-ค่า เป็นคู่ๆ อยู่ข้างใน ถ้าค่าเป็นเลขจำนวนเต็ม เลขทศนิยม หรือสายอักขระ มันจะวาดอยู่ในกล่อง แต่ถ้าเป็นลิสต์ อาจจะวาดอยู่นอกกล่อง เพื่อให้แผนภาพดูง่าย
ลิสต์สามารถเป็นค่าในดิกชันนารีได้ เช่นที่แสดงในตัวอย่างนี้ แต่ลิสต์ไม่สามารถใช้เป็นกุญแจได้ ข้างล่างนี้เป็นตัวอย่างว่าจะเกิดอะไรขึ้น ถ้าเราลอง:
>>> t = [1, 2, 3] >>> d = dict() >>> d[t] = 'oops' Traceback (most recent call last): File "<stdin>", line 1, in ? TypeError: list objects are unhashable
ตามที่ได้บอกไว้ตอนต้นว่า ดิกชันนารีถูกสร้างขึ้นมาด้วยตารางแฮช (hashtable) และนั่นหมายถึงว่า กุญแจต่างๆ ต้องสามารถทำแฮชได้ (hashable)
แฮช (hash) เป็นฟังก์ชันที่รับค่า (ของชนิดข้อมูลใดๆ) และคืนค่าเลขจำนวนเต็มออกมา ดิกชันนารีให้ค่าเลขจำนวนเต็มต่างๆ ที่ได้นี้ ซึ่งเรียกว่า ค่าแฮช (hash values) เพื่อเก็บและค้นหาคู่กุญแจค่าของดิกชันนารี
ระบบนี้ทำงานได้อย่างดี ถ้ากุญแจต่างๆ ไม่สามารถถูกเปลี่ยนแปลงได้ แต่ถ้ากุญแจต่างๆ เปลี่ยนได้ แบบเดียวกับลิสต์ ปัญหาจะเกิดขึ้น ตัวอย่างเช่น ถ้าเราสร้างคู่กุญแจค่า ไพธอนจะทำแฮชกุญแจ และเก็บค่าของกุญแจตามตำแหน่งที่แฮชได้ ถ้าเราไปเปลี่ยนกุญแจ เมื่อทำแฮช มันจะวิ่งไปหาที่ตำแหน่งอื่น ในกรณีนั้น เราอาจจะได้ข้อมูลสองที่สำหรับกุญแจเดียวกัน หรือเราอาจจะไม่สามารถหากุญแจได้เลย ไม่ว่าอย่างไร ดิกชันนารีจะทำงานผิดพลาด
นั่นจึงเป็นเหตุผลที่กุญแจต่างๆ ต้องสามารถทำแฮชได้ และทำไมชนิดข้อมูลที่เปลี่ยนแปลงได้ เช่น ลิสต์ถึงไม่สามารถทำแฮชได้ วิธีง่ายที่สุดที่จะก้าวข้ามข้อจำกัดนี้ คือการใช้ ทูเพิล (tuples) ที่เราจะได้เห็นในบทต่อไป
เนื่องจาก ตัวดิกชันนารีเองก็เป็นข้อมูลที่สามารถเปลี่ยนแปลงได้ มันจึงไม่สามารถใช้เป็นกุญแจได้ แต่มันสามารถ ใช้เป็นค่าได้.
ตอนที่เราลองเล่นกับฟังก์ชัน fibonacci
ในหัวข้อ 6.7 สังเกตว่า ยิ่งเราใส่ค่าอาร์กิวเมนต์ใหญ่เท่าไร ฟังก์ชันยิ่งใช้เวลารันนานเท่านั้น นอกจากนั้น สังเกตว่าเวลารันเพิ่มขึ้นเร็วมาก
เพื่อดูว่าทำไมจึงเป็นเช่นนั้น ดูรูป 11.2 ที่แสดง กราฟการเรียกใช้ (call graph) สำหรับ fibonacci
ที่ใช้ n=4
:
กราฟการเรียกใช้แสดงฟังก์ชัน พร้อมลูกศรเชื่อมฟังก์ชันที่เรียกกับฟังก์ชันที่ถูกเรียก บนสุดของกราฟ fibonacci
กับ n=4
เรียก fibonacci
กับ n=3
และกับ n=2
ในทำนองเดียวกัน fibonacci
กับ n=3
เรียก fibonacci
กับ n=2
และกับ n=1
และต่อๆ ไปแบบเดียวกัน
ถ้านับจำนวนครั้งที่ fibonacci(0)
และ fibonacci(1)
ถูกเรียกใช้ จะพบว่า นี่เป็นวิธีที่ไม่มีประสิทธิภาพ และมันจะยิ่งแย่ถ้าค่าอาร์กิวเมนต์ใหญ่ขึ้น
วิธีแก้ปัญหา คือ เก็บค่าที่ได้คำนวณแล้วไว้ในดิกชันนารี ค่าที่ได้เคยคำนวณไว้แล้วและเก็บไว้ใช้ภายหลังจะเรียกว่า เมโม (memo) ข้างล่างนี้คือเวอร์ชั่นที่ทำเมโมของ fibonacci
:
known = {0:0, 1:1} def fibonacci(n): if n in known: return known[n] res = fibonacci(n-1) + fibonacci(n-2) known[n] = res return res
ตัวแปร known
เป็นดิกชันนารีที่เก็บค่าของเลขฟีโบนัชชี ที่ได้คำนวณไว้แล้ว มันเริ่มจากสองรายการ: กุญแจ 0 ไปหาค่า 0 และกุญแจ 1 ไปหาค่า 1
เมื่อไรที่ fibonacci
ถูกเรียก มันจะดู known
ถ้าผลที่ต้องการมีอยู่แล้วในตัวแปร known
fibonacci
สามารถคืนค่านั้นออกมาได้เลย หรือถ้าผลที่ต้องการยังไม่มี ก็คำนวณผลออกมา และเก็บไว้ในดิกชันนารี แล้วค่อยคืนค่านั้นออกมา
ถ้าลองรันเวอร์ชั่นนี้ของ fibonacci
และเปรียบเทียบกับเวอร์ชั่นเดิม จะพบว่า มันเร็วกว่ามาก
ตัวอย่างที่แล้ว ตัวแปร known
ถูกสร้างอยู่นอกฟังก์ชัน ดังนั้นมันจะอยู่ภายใต้ขอบเขตพิเศษ เรียกว่า __main__
ตัวแปรต่างๆ ที่อยู่ภายใต้ขอบเขต __main__
บางครั้งจะเรียกว่า เป็นส่วนกลาง (global) เพราะว่าตัวแปรเหล่านี้สามารถถูกเข้าถึงได้จากทุกๆ ฟังก์ชัน ต่างจากตัวแปรเฉพาะที่ ซึ่งจะหายไปเมื่อฟังก์ชันจบ ตัวแปรส่วนกลางจะคงอยู่ได้ ผ่านการเรียกใช้แต่ละครั้ง
ตัวแปรส่วนกลางนิยมใช้สำหรับเป็น ตัวบ่งชี้ (flags) นั่นคือ ตัวแปรบูลีนต่างๆ ที่ใช้บอกสถานะ (แบบเดียวกับ ธง) ว่าสถานะเป็นจริงหรือเปล่า ตัวอย่างเช่น บางโปรแกรมใช้ตัวบ่งชี้ ชื่อ verbose
เพื่อใช้บอกระดับความละเอียดของเอาต์พุต:
verbose = True def example1(): if verbose: print('Running example1')
ถ้าลองกำหนดค่าใหม่ให้กับตัวแปรส่วนกลางดู เราจะฉงนได้ ตัวอย่างต่อไปนี้พยายามที่จะเก็บบันทึกว่าฟังก์ชันถูกเรียกใช้แล้วหรือไม่:
been_called = False def example2(): been_called = True # WRONG
พอเรารันโปรแกรมนี้ดู เราจะเห็นว่าค่า been_called
ไม่ได้เปลี่ยนไปเลย ปัญหาคือ example2
สร้างตัวแปรเฉพาะที่ใหม่ขึ้นมา ชื่อ been_called
ตัวแปรเฉพาะที่หายไป ตอนฟังก์ชันจบ และไม่ได้มีผลอะไรกับตัวแปรส่วนกลาง
ถ้าจะกำหนดค่าใหม่ให้กับตัวแปรส่วนกลางจากภายในฟังก์ชัน เราต้องประกาศตัวแปรส่วนกลาง ก่อนที่จะใช้มัน:
been_called = False def example2(): global been_called been_called = True
คำสั่ง global
บอกอินเตอร์พรีเตอร์ ทำนองว่า “ในฟังก์ชันนี้ ถ้าเราพูดว่า been_called
เราหมายถึงตัวแปรส่วนกลาง ไม่ต้องสร้างตัวแปรเฉพาะที่ขึ้นมาใหม่”
นี่เป็นตัวอย่างที่พยายามกำหนดค่าให้กับตัวแปรส่วนกลาง:
count = 0 def example3(): count = count + 1 # WRONG
ถ้ารันไป เราจะได้:
UnboundLocalError: local variable 'count' referenced before assignment
ไพธอนจะคิดว่า count
เป็นตัวแปรเฉพาะที่ และเข้าใจว่า เราพยายามจะใช้ค่าของมัน ก่อนกำหนดค่าให้มัน วิธีแก้ไขก็แบบเดิม คือ ประกาศ count
เป็นตัวแปรส่วนกลาง
def example3(): global count count += 1
ถ้าตัวแปรส่วนกลางอ้างถึง ค่าข้อมูลที่สามารถเปลี่ยนแปลงได้ (mutable value) เราสามารถแก้ไขค่านั้นได้เลย โดยไม่ต้องประกาศตัวแปร:
known = {0:0, 1:1} def example4(): known[2] = 1
ดังนั้น เราสามารถเพิ่ม ลบ แก้ไข ค่าของลิสต์ส่วนกลาง หรือดิกชันนารีส่วนกลางได้ แต่ถ้าเราต้องการกำหนดค่าข้อมูลใหม่ให้กับตัวแปร เราต้องประกาศให้ชัดเจน:
def example5(): global known known = dict()
ตัวแปรส่วนกลางนั้นนับว่ามีประโยชน์อยู่ แต่ถ้าเรามีตัวแปรส่วนกลางมากเกินไป และเราแก้ไขค่าข้อมูลของมันบ่อยเกินไป มันก็อาจจะทำให้โปรแกรมดีบักได้ยาก
ตอนที่เราทำงานกับชุดข้อมูลที่ใหญ่ๆ มันอาจจะไม่ค่อยสะดวก ที่จะดีบักด้วยการพิมพ์ออกหน้าจอ และค่อยๆ ไล่ตรวจสอบผลลัพธ์ด้วยตา ข้างล่างนี้เป็นคำแนะนำสำหรับการดีบักชุดข้อมูลขนาดใหญ่:
ปรับขนาดของอินพุตลง:
ถ้าทำได้ ให้ลดขนาดของชุดข้อมูลลง ตัวอย่างเช่น ถ้าโปรแกรมอ่านไฟล์ข้อความ ให้เริ่มจาก 10 บรรทัดแรกก่อน หรือเริ่มจากตัวอย่างบางส่วนที่เล็กที่สุดที่จะหาได้ก่อน อาจจะเข้าไปแก้ไขไฟล์โดยตรงเลย หรือ (ดีกว่า) อาจจะเข้าไปแก้ไขโปรแกรม ให้มันอ่านเฉพาะ n
บรรทัดแรกๆ ก่อน
ถ้ามีข้อผิดพลาดอยู่ อาจจะลองลด n
ลงให้เป็นค่าเล็กที่สุด เท่าที่ยังมองเห็นปัญหาได้อยู่ แล้วค่อยเพิ่มมันทีละขั้นๆ ตอนที่เจอและแก้ไขปัญหาได้
ตรวจสอบสรุปและชนิดข้อมูล:
แทนที่จะพิมพ์ข้อมูลทั้งหมดออกหน้าจอ และไล่ตรวจสอบรายละเอียดทั้งหมดด้วยตา อาจจะลองพิมพ์เฉพาะสรุปข้อมูลออกหน้าจอ: ตัวอย่างเช่น จำนวนรายการในดิกชันนารี หรือจำนวนรายการของลิสต์
สาเหตุหนึ่งที่พบบ่อยมากๆ สำหรับข้อผิดพลาดเวลาดำเนินการ (runtime errors) คือ ค่าข้อมูลไม่ใช่ชนิดข้อมูลที่ถูกต้อง (type) การดีบักข้อผิดพลาดแบบนี้ ก็แค่พิมพ์ชนิดของค่าข้อมูลออกมาหน้าจอเท่านั้น
เขียนโปรแกรมให้มีการตรวจสอบตัวเอง:
บางครั้ง เราอาจจะเขียนโปรแกรมให้ตรวจสอบข้อผิดพลาดโดยอัตโนมัติไว้เลย ตัวอย่างเช่น ถ้าเราคำนวณค่าเฉลี่ยของลิสต์ของตัวเลข เราอาจจะตรวจสอบว่า ผลลัพธ์ที่ได้ไม่ใหญ่กว่าค่าในลิสต์ที่ใหญ่ที่สุด และผลลัพธ์ที่ได้ไม่เล็กกว่าค่าในลิสต์ที่เล็กที่สุด การทำแบบนี้ เขาเรียกว่า “เซนิตี้เช็ค” (sanity check) หรือตรวจสอบว่ายังไม่บ้า เพราะว่า โปรแกรมมันตรวจหาผลที่มันดู “บ้าๆ” (insane)
การตรวจสอบอีกแบบหนึ่งที่เปรียบเทียบผลการคำนวณสองวิธีที่แตกต่างกัน เพื่อดูว่าผลที่ได้สอดคล้องกันหรือไม่ แบบนี้จะเรียกว่า “การตรวจสอบความสอดคล้อง” (consistency check)
จัดรูปแบบของเอาต์พุต:
การจัดรูปแบบของผลแสดงจากการดีบัก จะช่วยให้มองเห็นข้อผิดพลาดได้ง่ายขึ้น เราได้เห็นตัวอย่างไปแล้วในหัวข้อ 6.9 เครื่องมืออีกตัวที่อาจจะมีประโยชน์ คือ โมดูล pprint
ที่มีฟังก์ชัน pprint
ซึ่งสามารถแสดงชนิดข้อมูลในตัว (built-in types) ในรูปแบบที่ช่วยให้มนุษย์อ่านได้ง่ายขึ้น (pprint
ย่อจาก “pretty print”)
ย้ำอีกครั้ง เวลาที่เราทำโครงโปรแกรมสามารถช่วยลดเวลาที่เราให้ดีบักโปรแกรม
global
ที่บอกอินเตอร์พรีเตอร์บางอย่างเกี่ยวกับตัวแปร
แบบฝึกหัด 1
เขียนฟังก์ชันที่อ่านคำใน words.txt
และเก็บคำเหล่านั้นเป็นกุญแจของดิกชันนารี มันไม่สำคัญว่าค่าของกุญแจเป็นเท่าไร เราสามารถใช้ตัวปฏิบัติการ in
เป็นวิธีที่รวดเร็วในการตรวจสอบว่าคำที่สนใจมีอยู่ในดิกชันนารีหรือไม่
ถ้าทำ แบบฝึกหัด 10.10มาแล้ว เราสามารถเปรียบเทียบความเร็วของอิมพลิเมนเตชั่นนี้ กับลิสต์ที่ใช้ in
และวิธีค้นหาแบบแบ่งสอง (bisection search)
แบบฝึกหัด 2
อ่านเอกสารประกอบของดิกชันนารีเมธอด setdefault
และใช้เมธอดนี้เพื่อเขียนโปรแกรมในแบบที่กระชับขึ้นของ invert_dict
เฉลย: http://thinkpython2.com/code/invert_dict.py
แบบฝึกหัด 3
ใช้เมโม เพื่อทำฟังก์ชันแอกเคอมันน์ (Ackermann function) จากแบบฝึกหัด 6.2 และดูว่า การใช้เมโม ช่วยให้สามารถรันฟังก์ชัน เมื่อใช้กับอาร์กิวเมนต์ที่ใหญ่ขึ้นได้หรือไม่
คำใบ้: ไม่ได้ เฉลย: http://thinkpython2.com/code/ackermann_memo.py
แบบฝึกหัด 4
จากแบบฝึกหัด 10.7 เราจะมีฟังก์ชันชื่อ has_duplicates
ที่รับลิสต์เป็นพารามิเตอร์ และคืนค่า True
ออกมา ถ้ามีรายการที่ซ้ำอยู่ในลิสต์
ใช้ดิกชันนารีเพื่อเขียนโปรแกรมแบบที่เร็วขึ้นและง่ายขึ้นของฟังก์ชัน has_duplicates
เฉลย: http://thinkpython2.com/code/has_duplicates.py
แบบฝึกหัด 5
คำสองคำเรียกว่าเป็น “คู่หมุนเปลี่ยน” (rotate pair) เมื่อเราหมุนคำหนึ่งและผลลัพธ์ได้เป็นอีกคำหนึ่ง (ดู rotate_word
ใน แบบฝึกหัด 8.5)
เขียนโปรแกรมที่อ่านลิสต์ของคำ และหาคู่หมุนเปลี่ยนออกมาทั้งหมด
เฉลย: http://thinkpython2.com/code/rotate_pairs.py
แบบฝึกหัด 6
นี่เป็นปริศนาจาก Car Talk (http://www.cartalk.com/content/puzzlers):
เรื่องนี้ส่งมาจากสหายชื่อว่า แดน โอเลียรี่ แดนฉงนกับคำพยางค์เดียวห้าอักษร ที่มีคุณสมบัติพิเศษ นั่นคือ เมื่อเราเอาอักษรแรกออก ตัวอักษรที่เหลือจะเป็นคำพ้องเสียง (homophone) ของคำเดิม นั่นคือคำใหม่จะออกเสียงเหมือนเดิมเลย แถมถ้าไม่เอาอักษรตัวแรกออก แต่เอาอักษรตัวที่สองออก ผลก็ยังเป็นคำพ้องเสียงเดิมอยู่ คำถามคือ คำนั้นคือคำอะไร
เดี๋ยวผมจะให้ตัวอย่างคำที่ไม่ใช่. ลองคำว่า ‘wrack’ W-R-A-C-K ก็แบบที่รู้ เช่น ‘wrack with pain.’ ถ้าผมเอาอักษรตัวแรกออก ผมจะเหลือแค่ ’R-A-C-K’ เหมือนที่คำพูด ‘Holy cow, did you see the rack on that buck!’ เกือบได้แล้ว เพียงแต่เมื่อเราใส่ ‘w’ กลับเข้าไป แต่เอา ‘r’ ออกแทน เราจะได้ ‘wack’ ซึ่งไม่ได้พ้องเสียงกับอีกสองคำ (‘wrack’ และ ‘rack’)
แต่มันมีอย่างน้อยก็คำหนึ่งหละ ที่แดนและเรารู้ว่าจะให้คำพ้องเสียงสองคำออกมา ไม่ว่าจะเอาอักษรตัวแรก หรือตัวที่สองออก คำถามคือคำนั้นคือคำว่าอะไร
เราสามารถใช้ดิกชันนารีจากแบบฝึกหัด 1 เพื่อตรวจสอบว่าสายอักขระอยู่ในลิสต์ของคำหรือไม่ เพื่อตรวจสอบว่าคำสองคำเป็นคำพ้องเสียงหรือไม่ เราสามารถใช้พจนานุกรมการออกเสียงซีเอ็มยู (CMU Pronouncing Dictionary) ซึ่งสามารถดาวน์โหลดได้จาก http://www.speech.cs.cmu.edu/cgi-bin/cmudict หรือจาก http://thinkpython2.com/code/c06d และยังสามารถดาวน์โหลด http://thinkpython2.com/code/pronounce.py ที่เตรียมฟังก์ชัน read_dictionary
ที่ใช้อ่านพจนานุกรมการออกเสียง แล้วส่งไพธอนดิกชันนารีที่เชื่อมคำกับการออกเสียงออกมาให้
เขียนโปรแกรมที่หาคำทั้งหมดที่ตอบปริศนานี้ออกมา เฉลย: http://thinkpython2.com/code/homophone.py
https://greenteapress.com/thinkpython2/html/thinkpython2012.html