Table of Contents

5. เงื่อนไขและการเรียกซ้ำ

หัวข้อหลักของบทนี้ คือ คำสั่ง if ซึ่งดำเนินการกับโค้ดต่างกันขึ้นอยู่กับสถานะของโปรแกรม แต่ก่อนอื่น ผมอยากจะแนะนำตัวดำเนินการใหม่สองตัว: การหารปัดเศษลง (floor division) และโมดูลัส (modulus)

5.1 การหารปัดเศษลง และโมดูลัส

เครื่องหมาย การหารปัดเศษลง (floor division), //, หารเลขสองตัวแล้วปัดเศษลงให้เป็นเลขจำนวนเต็ม ตัวอย่างเช่น สมมติว่าเวลาฉายหนัง คือ 105 นาที เราอาจจะอยากทราบว่ามันนานกี่ชั่วโมง การหารแบบปกตินิยมจะให้ค่าเป็นเลขจุดลอย:

>>> minutes = 105
>>> minutes / 60
1.75

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

>>> minutes = 105
>>> hours = minutes // 60
>>> hours
1

ในการหาเศษของชั่วโมง เราสามารถลบจำนวนนาทีในหนึ่งชั่วโมงออก:

>>> remainder = minutes - hours * 60
>>> remainder
45

อีกทางหนึ่ง คือ การใช้ ตัวดำเนินการมอดูลัส (modulus operator), %, ซึ่งจะหารเลขสองตัว และให้ค่าเป็นเศษของการหาร

>>> remainder = minutes % 60
>>> remainder
45

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

นอกจากนี้ เราสามารถดึงตัวเลขหลักทางขวาสุดออกมาได้ด้วย (หลักเดียวหรือหลายหลัก) เช่น x % 10 จะได้เลขตัวขวาสุด (หลักหน่วย) ของ x (ในฐาน 10) เช่นเดียวกันกับ x % 100 จะให้เลขสองหลัก สุดท้ายออกมา

แต่ถ้าเราใช้ไพธอน 2 การหารเลขจะต่างออกไป ตัวดำเนินการหาร, /, จะทำการหารแบบปัดเศษถ้าเลขทั้งสอง เป็นจำนวนเต็ม และจะทำการหารแบบจุดลอยหากเลขตัวใดตัวหนึ่งเป็น float

5.2 นิพจน์บูลีน

นิพจน์บูลีน (boolean expression) คือนิพจน์ที่มีค่าความจริงเป็น จริง หรือ เท็จ ตัวอย่างต่อไปนี้ใช้ตัวดำเนินการ == ซึ่งเปรียบเทียบตัวถูกดำเนินการสองตัว และให้ค่าเป็น จริง (True) ถ้าสองค่านั้นเท่ากัน และให้ค่าเป็น เท็จ (False) หากไม่เท่ากัน:

>>> 5 == 5
True
>>> 5 == 6
False

True และ False เป็นค่าพิเศษที่มีชนิดเป็น บูล (bool); มันไม่ใช่สายอักขระ:

>>> type(True)
<class 'bool'>
>>> type(False)
<class 'bool'>

ตัวดำเนินการ == เป็นหนึ่งใน ตัวดำเนินการเชิงสัมพันธ์ (relational operators); ตัวอื่นๆ คือ:

      x != y               # x is not equal to y
      x > y                # x is greater than y
      x < y                # x is less than y
      x >= y               # x is greater than or equal to y
      x <= y               # x is less than or equal to y

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

5.3 ตัวดำเนินการทางตรรกะ

ตัวดำเนินการทางตรรกะ (logical operators) มี 3 ตัว: and, or, และ not. ความหมายของตัวดำเนินการเหล่านี้ตรงกับความหมายในภาษาอังกฤษเลย เช่น x > 0 and x < 10 จะเป็นจริงถ้า x มากกว่า 0 และ น้อยกว่า 10 เท่านั้น

n%2 == 0 or n%3 == 0 เป็นจริงถ้า ตัวใดตัวหนึ่ง หรือ ทั้งสองตัว ของเงื่อนไขเป็นจริง นั่นคือ ถ้าเลขนั้นสามารถหารด้วย 2 หรือ 3 ลงตัว

สุดท้ายนี้ ตัวดำเนินการ not จะทำนิพจน์บูลีนให้เป็นนิเสธ (การกลับค่าความจริง) ดังนั้น not (x > y) จะเป็นจริง ถ้า x > y เป็นเท็จ นั่นคือ ถ้า x น้อยกว่าหรือเท่ากับ y

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

>>> 42 and True
True

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

5.4 การดำเนินการตามเงื่อนไข

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

if x > 0:
    print('x is positive')

นิพจน์บูลีนที่อยู่หลังจาก if เรียกว่า เงื่อนไข (condition) ถ้าเงื่อนไขเป็นจริง คำสั่งที่ย่อหน้าเข้าไปนั้นจะถูกรัน ไม่เช่นนั้น ก็ไม่มีอะไรเกิดขึ้น

คำสั่ง if มีโครงสร้างเหมือนนิยามของฟังก์ชัน: มีส่วนหัว ตามด้วยส่วนตัวที่ถูกย่อหน้าเข้าไป คำสั่งแบบนี้เรียกว่า คำสั่งประกอบ (compound statements)

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

if x < 0:
    pass          # TODO: need to handle negative values!

5.5 การดำเนินการทางเลือก

รูปแบบที่สองของคำสั่ง if คือ “การดำเนินการทางเลือก (alternative execution)” ซึ่งมีทางเลือกทำสองทางและมีเงื่อนไขที่จะกำหนดว่าคำสั่งชุดไหนจะถูกรัน กฎวากยสัมพันธ์ของคำสั่งเป็นแบบนี้:

if x % 2 == 0:
    print('x is even')
else:
    print('x is odd')

ถ้าเศษของการหารเมื่อเราหาร x ด้วย 2 มีค่าเป็น 0 แล้ว เรารู้ว่า x เป็นจำนวนคู่ และโปรแกรมจะแสดงข้อความตามนั้น แต่ถ้าเงื่อนไขเป็นเท็จ คำสั่งชุดที่สองจะทำงาน เนื่องจากเงื่อนไขจะต้องเป็นจริงหรือเท็จเท่านั้น จึงมีแค่ทางเลือกหนึ่งอย่างเท่านั้นที่ทำงาน ทางเลือกเหล่านี้เรียกว่า (แขนง) branches เพราะว่ามันเป็นกิ่งที่แตกแยกออกไป ของกระแสการดำเนินการ (flow of execution)

5.6 เงื่อนไขลูกโซ่

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

if x < y:
    print('x is less than y')
elif x > y:
    print('x is greater than y')
else:
    print('x and y are equal')

elif เป็นคำย่อของ “else if” (ไม่เช่นนั้น ถ้า) ทบทวนอีกทีว่าแขนงแค่หนึ่งอันเท่านั้นที่จะทำงาน มันไม่มีการจำกัดจำนวนของคำสั่ง elif ถ้าจะมีข้อย่อย (clause) else ด้วย มันจะต้องอยู่ท้ายสุด แต่มันไม่จำเป็นต้องมี

if choice == 'a':
    draw_a()
elif choice == 'b':
    draw_b()
elif choice == 'c':
    draw_c()

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

5.7 เงื่อนไขซ้อนใน

เงื่อนไขหนึ่งๆ สามารถซ้อนในเงื่อนไขอื่นได้ เราสามารถเขียนตัวอย่างในหัวข้อที่แล้วให้เป็นแบบนี้ได้:

if x == y:
    print('x and y are equal')
else:
    if x < y:
        print('x is less than y')
    else:
        print('x is greater than y')

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

แม้ว่าการย่อหน้าของคำสั่งต่างๆ ทำให้โปรแกรมมีโครงสร้างที่ชัดเจน แต่ เงื่อนไขซ้อนใน (nested conditionals) มันทำให้อ่านยากเวลาอ่านเร็วๆ จึงเป็นความคิดที่ดีที่จะหลีกเลี่ยงถ้าทำได้

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

if 0 < x:
    if x < 10:
        print('x is a positive single-digit number.')

คำสั่ง print จะทำงานถ้าเราผ่านเงื่อนไขทั้งสองนี้ได้ ดังนั้น เราสามารถทำให้เกิดผลอย่างเดียวกัน โดยการใช้ตัวดำเนินการ and:

if 0 < x and x < 10:
    print('x is a positive single-digit number.')

สำหรับเงื่อนไขประเภทนี้ ไพธอนมีทางเลือกให้ทำแบบกระชับ:

if 0 < x < 10:
    print('x is a positive single-digit number.')

5.8 การเรียกซ้ำ

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

def countdown(n):
    if n <= 0:
        print('Blastoff!')
    else:
        print(n)
        countdown(n-1)

ถ้า n เป็น 0 หรือมีค่าลบ มันจะแสดงคำว่า “Blastoff!” ออกมา ไม่เช่นนั้น มันจะแสดงค่า n ออกมาและเรียกฟังก์ชันที่ชื่อว่า countdown—ตัวมันเอง—โดยผ่านค่า n-1 เป็นอาร์กิวเมนต์

จะเกิดอะไรขึ้นถ้าเราเรียกฟังก์ชันนี้ในลักษณะนี้?

>>> countdown(3)

การดำเนินการของฟังก์ชัน countdown เริ่มด้วย n=3 และเนื่องจาก n มีค่ามากกว่า 0 ฟังก์ชันจะแสดงค่า 3 ออกมาและจากนั้นจึงเรียกตัวมันเอง…

การทำงานของฟังก์ชัน countdown เริ่มด้วย n=2 และเนื่องจาก n มีค่ามากกว่า 0 ฟังก์ชันจะแสดงค่า 2 ออกมาและจากนั้นจึงเรียกตัวมันเอง…
การทำงานของฟังก์ชัน countdown เริ่มด้วย n=1 และเนื่องจาก n มีค่ามากกว่า 0 ฟังก์ชันจะแสดงค่า 1 ออกมาและจากนั้นจึงเรียกตัวมันเอง…
การทำงานของฟังก์ชัน countdown เริ่มด้วย n=0 และเนื่องจาก n มีค่าไม่มากกว่า 0 ฟังก์ชันจะแสดงคำว่า “Blastoff!” ออกมา จบ และกลับออกไป

ฟังก์ชัน countdown ที่ได้รับค่า n=1 มาก็จบ และกลับออกไป

ฟังก์ชัน countdown ที่ได้รับค่า n=2 มาก็จบ และกลับออกไป

ฟังก์ชัน countdown ที่ได้รับค่า n=3 มาก็จบ และกลับออกไป

และจากนั้นเราก็กลับมายัง __main__ ดังนั้น เอ้าต์พุตทั้งหมดจะหน้าตาเป็นแบบนี้:

3
2
1
Blastoff!

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

อีกตัวอย่างหนึ่ง เราสามารถเขียนฟังก์ชันที่พิมพ์สายอักขระจำนวน n ครั้ง

def print_n(s, n):
    if n <= 0:
        return
    print(s)
    print_n(s, n-1)

ถ้า n <= 0 คำสั่ง return จะทำให้จบฟังก์ชัน กระแสการดำเนินการจะกลับไปยังตัวเรียก (caller) ทันที และบรรทัดที่เหลือในฟังก์ชันนั้นจะไม่ถูกรัน

ส่วนที่เหลือของฟังก์ชันนั้นเหมือนกับฟังก์ชัน countdown: มันแสดง s และจากนั้นเรียกตัวเองเพื่อแสดง s ไปอีก $n-1$ ครั้ง ดังนั้น จำนวนบรรทัดของเอ้าต์พุตจะเป็น 1 + (n - 1) ซึ่งรวมกันแล้วได้ n บรรทัด

สำหรับตัวอย่างง่ายๆ แบบนี้ มันอาจจะง่ายกว่าที่จะใช้ลูป for แต่เราจะเห็นตัวอย่างอีกมากในภายหลังที่ยากที่จะเขียนด้วย ลูป for และง่ายที่จะเขียนด้วยการเรียกซ้ำ (recursion) ดังนั้น จึงเป็นเรื่องที่ดีที่จะเริ่มเข้าใจหัวข้อนี้ก่อน

5.9 แผนภาพแบบกองซ้อนสำหรับฟังก์ชันเรียกซ้ำ

ในหัวข้อที่ 3.9 เราใช้แผนภาพแบบกองซ้อนในการแสดงสถานะของโปรแกรม ในขณะที่มีการเรียกฟังก์ชัน แผนภาพชนิดเดียวกันนี้สามารถช่วยให้เข้าใจฟังก์ชันเรียกซ้ำ (recursive function) ได้ด้วย

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

รูปที่ 5.1 แสดงแผนภาพแบบกองซ้อนสำหรับฟังก์ชัน countdown ที่ถูกเรียกด้วยค่า n = 3.

 แผนภาพแบบกองซ้อน (Stack diagram)
รูปที่ 5.1 แผนภาพแบบกองซ้อน (Stack diagram)

เหมือนทั่วไป บนสุดของกองคือกรอบของ __main__ มันว่างเปล่าเพราะว่าเราไม่ได้สร้างตัวแปรใดๆ ใน __main__ หรือไม่ได้ผ่านอาร์กิวเมนต์ใดๆ เข้าไป

กรอบ countdown ทั้ง 4 กรอบ มีค่าพารามิเตอร์ n ที่ต่างกัน ล่างสุดของกองซึ่ง n=0 เรียกว่า กรณีฐาน (base case) มันไม่ได้เรียกตัวเองซ้ำ ดังนั้น จึงไม่มีกรอบเพิ่มไปอีก

เพื่อเป็นการฝึกทำ ให้วาดแผนภาพแบบกองซ้อนสำหรับฟังก์ชัน print_n ที่ถูกเรียกด้วยค่า s = 'Hello' และ n=2 จากนั้นให้เขียนฟังก์ชันชื่อว่า do_n ที่รับวัตถุฟังก์ชันและจำนวน n เข้าเป็นอาร์กิวเมนต์ และมันจะเรียกฟังก์ชันที่กำหนดให้เป็นจำนวน n ครั้ง

5.10 การเรียกซ้ำไม่รู้จบ

ถ้าการเรียกซ้ำไม่ไปถึงกรณีฐาน (base case) เสียที มันจะทำให้เกิดการเรียกซ้ำไปตลอดกาล และโปรแกรมก็จะไม่จบ นี่เรียกว่า การเรียกซ้ำไม่รู้จบ (infinite recursion) และมันก็ไม่ได้เป็นความคิดที่ดีเท่าไรนัก นี่คือ โปรแกรมแบบสั้นที่สุดที่จะทำให้เกิดการเรียกซ้ำแบบไม่สิ้นสุด:

def recurse():
    recurse()

ในสภาพแวดล้อมของการเขียนโปรแกรมส่วนใหญ่ โปรแกรมจะไม่ทำงานไปตลอดกาลแบบนั้น ไพธอนจะรายงานข้อความความผิดพลาดเมื่อมีการเรียกซ้ำจนถึงความลึกที่มากที่สุด (ที่อนุญาตให้รันได้):

  File "<stdin>", line 2, in recurse
  File "<stdin>", line 2, in recurse
  File "<stdin>", line 2, in recurse
                  .   
                  .
                  .
  File "<stdin>", line 2, in recurse
RuntimeError: Maximum recursion depth exceeded

การย้อนรอยแบบนี้มันจะเยอะกว่าที่เราเคยทำในบทก่อนหน้านี้นิดหน่อย เมื่อเกิดข้อผิดพลาดในตัวอย่างนี้ขึ้นมา มันมี 1000 recurse frames (กรอบการเรียกซ้ำ) บนกองนี้!

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

5.11 การนำเข้าข้อมูลผ่านคีย์บอร์ด

โปรแกรมที่เราเขียนมาถึงตอนนี้ไม่ได้รับอินพุต (input) มาจากผู้ใช้ มันแค่ทำอะไรซ้ำๆ ตลอดเวลา

ไพธอนได้เตรียมฟังก์ชันที่มีอยู่ในตัวเรียกว่า input ที่หยุดโปรแกรมและรอให้ผู้ใช้พิมพ์อะไรสักอย่างเข้ามา เมื่อผู้ใช้กดปุ่ม Return หรือ Enter โปรแกรมจะทำงานต่อ และฟังก์ชัน input จะคืนค่า สิ่งที่ผู้ใช้พิมพ์เข้ามาเป็นชนิดสายอักขระ ในไพธอน 2 ฟังก์ชันที่ทำงานแบบเดียวกันนี้เรียกว่า raw_input

>>> text = input()
What are you waiting for?
>>> text
'What are you waiting for?'

ก่อนที่จะรับอินพุตมาจากผู้ใช้ มันเป็นความคิดที่ดีที่จะพิมพ์ข้อความไปบอกผู้ใช้ว่าให้พิมพ์อะไรเข้ามา ฟังก์ชัน input สามารถรับข้อความพร้อมรับ (prompt) เป็นอาร์กิวเมนต์ได้:

>>> name = input('What...is your name?\n')
What...is your name?
Arthur, King of the Britons!
>>> name
'Arthur, King of the Britons!'

ลำดับอักขระ \n ในตอนท้ายของข้อความพร้อมรับเป็นตัวแทนของ บรรทัดใหม่ (newline) ซึ่งเป็นอักขระพิเศษที่ทำให้เกิดการขึ้นบรรทัดใหม่ นั่นคือเหตุผลที่อินพุตของผู้ใช้ปรากฏอยู่ข้างใต้ข้อความพร้อมรับ

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

>>> prompt = 'What...is the airspeed velocity of an unladen swallow?\n'
>>> speed = input(prompt)
What...is the airspeed velocity of an unladen swallow?
42
>>> int(speed)
42

แต่ถ้าผู้ใช้พิมพ์อย่างอื่นที่ไม่ใช่สายอักขระของตัวเลขแล้วล่ะก็ เราก็จะได้ข้อผิดพลาด:

(หมายเหตุผู้แปล: ได้ข้อผิดพลาดตอนที่พยายามแปลงให้เป็น int)

>>> speed = input(prompt)
What...is the airspeed velocity of an unladen swallow?
What do you mean, an African or a European swallow?
>>> int(speed)
ValueError: invalid literal for int() with base 10

เราจะรู้ว่าจะจัดการกับข้อผิดพลาดประเภทนี้อย่างไรในภายหลัง

5.12 การดีบัก

เมื่อเกิดข้อผิดพลาดเชิงวากยสัมพันธ์ (syntax error) หรือข้อผิดพลาดตอนดำเนินการ (runtime error) ข้อความแจ้งข้อผิดพลาด (error message) มีข้อมูลจำนวนมากให้เรา แต่มันจะทำให้เรารู้สึกท้วมท้นมาก ส่วนที่เป็นประโยชน์โดยปกติแล้วจะเป็น:

ข้อผิดพลาดเชิงวากยสัมพันธ์นั้นง่ายที่จะหา แต่มันก็มีข้อต้องระวังนิดหน่อย ข้อผิดพลาดที่เกี่ยวกับการเว้นวรรค (whitespace error) อาจจะทำให้เราปวดหัวได้ เพราะว่าช่องว่างและย่อหน้านั้นมันมองไม่เห็น และเราก็ชินกับการไม่สนใจมัน

>>> x = 5
>>>  y = 6
  File "<stdin>", line 1
    y = 6
    ^
IndentationError: unexpected indent

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

เช่นเดียวกันกับข้อผิดพลาดตอนดำเนินการ สมมติว่าเราพยายามที่จะคำนวณอัตราส่วนระหว่าง สัญญาณและสัญญาณรบกวน (signal-to-noise ratio) ในหน่วยเดซิเบล สูตรคือ $SNR_{db} = 10 \log_{10} (P_{signal} / P_{noise})$ ในไพธอน เราน่าจะเขียนประมาณนี้:

import math
signal_power = 9
noise_power = 10
ratio = signal_power // noise_power
decibels = 10 * math.log10(ratio)
print(decibels)

เมื่อเรารันโปรแกรม เราจะได้ข้อยกเว้น:

Traceback (most recent call last):
  File "snr.py", line 5, in ?
    decibels = 10 * math.log10(ratio)
ValueError: math domain error

ข้อความแจ้งข้อผิดพลาดระบุว่ามีปัญหาที่บรรทัดที่ 5 แต่ก็ไม่เห็นมีอะไรผิดนี่นา เพื่อที่จะหาข้อผิดพลาดที่แท้จริง มันอาจจะเป็นประโยชน์ที่จะพิมพ์ค่าของ ratio ออกมาดู ซึ่งมีค่าเป็น 0 ปัญหาจึงอยู่ที่บรรทัดที่ 4 ที่ใช้การหารแบบปัดเศษลง แทนที่จะใช้การหารแบบจุดลอย

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

5.13 อภิธานศัพท์

5.14 แบบฝึกหัด

แบบฝึกหัด 1
มอดูล time มีฟังก์ชันที่ชื่อว่า time ให้ใช้ ซึ่งคืนค่าเป็นเวลามาตรฐานโลกตามนาฬิกาที่กรีนิช (Greenwich Mean Time: GMT) ณ ปัจจุบัน อ้างอิงจาก “the epoch” ซึ่งเป็นเวลามาตรฐานที่ใช้อ้างอิง บนระบบยูนิกซ์ epoch คือ วันที่ 1 มกราคม ค.ศ. 1970

(หมายเหตุผู้แปล: epoch อ่านว่า เอพ’เพิค คือเวลา 00:00:00 UTC ของวันที่ 1 มกราคม ค.ศ. 1970)

>>> import time
>>> time.time()
1437746094.5735958

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

แบบฝึกหัด 2
ทฤษฎีบทสุดท้ายของแฟร์มา (Fermat’s Last Theorem) ระบุว่า ไม่มีจำนวนเต็มบวก $a$, $b$, และ $c$ ใดๆ ที่ทำให้:

$$a^n + b^n = c^n$$ สำหรับค่าใดๆ ของ $n$ ที่มากกว่า 2

โปรแกรมควรจะพิมพ์ว่า “Holy smokes, Fermat was wrong!” ไม่เช่นนั้น โปรแกรมควรจะพิมพ์ว่า “No, that doesn’t work.”

แบบฝึกหัด 3
ถ้าเราได้กิ่งไม้มาสามชิ้น เราอาจจะหรืออาจจะไม่สามารถที่จะเรียงมันให้เป็น สามเหลี่ยมได้ เช่น ถ้าไม้อันหนึ่งยาว 12 นิ้ว และอีกสองอันยาวอันละ 1 นิ้ว เราจะไม่ สามารถทำให้ไม้แท่งสั้นประกบกันได้ สำหรับความยาวของไม้ทั้งสามแท่ง มันมีการทดสอบแบบ ง่ายๆ เพื่อที่จะดูว่ามันสามารถที่จะประกอบกันเป็นสามเหลี่ยมได้หรือไม่:

ถ้าความยาวของด้านใดด้านหนึ่งนั้นยาวมากกว่าผลรวมของอีกสองด้านที่เหลือ เราจะไม่สามารถสร้างสามเหลี่ยมได้ นอกจากนี้ เราจะสามารถทำได้ (ถ้าผล รวมของสองด้าน เท่ากับด้านที่สาม มันจะประกอบกันเป็นสิ่งที่เรียกว่า สามเหลี่ยมลดรูป หรือ “degenerate” triangle)

(หมายเหตุผู้แปล: สามเหลี่ยมลดรูป หรือ degenerate triangle คือ สามเหลี่ยมที่ไม่เห็น เป็นรูปสามเหลี่ยมทั่วไป แต่เป็นคล้ายๆ เส้นตรงแทน)

  1. ให้เขียนฟังก์ชันชื่อว่า is_triangle ซึ่งรับจำนวนเต็มมาเป็นอาร์กิวเมนต์ และพิมพ์ “Yes” หรือ “No” ขึ้นอยู่กับว่าเราสามารถจะประกอบสาม เหลี่ยมแท่งไม้จากความยาวที่กำหนดให้ได้หรือไม่
  2. ให้เขียนฟ้งก์ชันที่รับอินพุตมาจากผู้ใช้เป็นความยาวของไม้ 3 แท่ง แปลงให้เป็น จำนวนเต็มและใช้ฟังก์ชัน is_triangle ตรวจสอบว่าแท่งไม้ที่มีความยาวที่ใส่ เข้ามา จะสามารถประกอบกันเป็นสามเหลี่ยมได้หรือไม่

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

def recurse(n, s):
    if n == 0:
        print(s)
    else:
        recurse(n-1, n+s)
 
recurse(3, 0)
  1. จะเกิดอะไรขึ้นถ้าเราเรียกฟังก์ชันแบบนี้ีี: recurse(-1, 0)?
  2. ให้เขียนด็อกสตริง (docstring) ที่อธิบายทุกอย่างที่บางคนต้องรู้ เพื่อที่จะใช้ฟังก์ชันนี้ (ไม่ต้องเขียนอย่างอื่นมา)

แบบฝึกหัดต่อไปนี้ใช้มอดูล turtle ใน บทที่ 4:

แบบฝึกหัด 5
ให้อ่านฟังก์ชันต่อไปนี้ และดูว่าเรารู้ไหมว่ามันทำอะไร (ดูตัวอย่าง ใน บทที่ 4) จากนั้นให้รันฟังก์ชันและดูว่าเราคิดถูกไหม

def draw(t, length, n):
    if n == 0:
        return
    angle = 50
    t.fd(length*n)
    t.lt(angle)
    draw(t, length, n-1)
    t.rt(2*angle)
    draw(t, length, n-1)
    t.lt(angle)
    t.bk(length*n)

 เส้นโค้งค็อค (Koch curve)
รูปที่ 5.2 เส้นโค้งค็อค (Koch curve)

แบบฝึกหัด 6
เส้นโค้งค็อค (Koch curve) เป็น แฟร็กทัล (fractal) แบบรูปที่ 5.2 ในการจะวาดเส้นโค้งค็อคที่ ยาว $x$ ทั้งหมดที่เราจะต้องทำคือ

  1. วาดเส้นโค้งค็อคที่ยาว $x/3$
  2. หันซ้าย 60 องศา
  3. วาดเส้นโค้งค็อคที่ยาว $x/3$
  4. หันขวา 120 องศา
  5. วาดเส้นโค้งค็อคที่ยาว $x/3$
  6. หันซ้าย 60 องศา
  7. วาดเส้นโค้งค็อคที่ยาว $x/3$

ข้อยกเว้นคือ ถ้า $x$ น้อยกว่า 3: เราจะแค่วาดเส้นตรงที่ยาว $x$ ได้เท่านั้น

https://greenteapress.com/thinkpython2/html/thinkpython2006.html