เนื้อหาในบทนี้เกี่ยวกับการวนซ้ำ ซึ่งเป็นความสามารถในการรันก้อนชุดคำสั่งซ้ำไปมา เราได้เห็น การวนซ้ำแบบหนึ่งไปแล้วโดยการใช้การเรียกซ้ำ ในหัวข้อที่ 5.8 เราได้เห็นอีกแบบ โดยใช้ลูป for
ในหัวข้อที่ 4.2 ในบทนี้ เราจะเห็นอีกแบบ โดยการใช้คำสั่ง while
แต่ก่อนอื่น ผมอยากจะพูดอะไรนิดหน่อยเกี่ยวกับการกำหนดค่าให้ตัวแปร
เราอาจจะค้นพบแล้วว่า เราสามารถกำหนดค่าให้ตัวแปรตัวเดิมได้มากกว่าหนึ่งครั้ง การกำหนดค่าครั้งใหม่ ทำให้ตัวแปรนั้นอ้างอิงไปที่ค่าใหม่ (และหยุดอ้างอิงไปที่ค่าเก่า)
>>> x = 5 >>> x 5 >>> x = 7 >>> x 7
ครั้งแรกที่เราแสดงค่า x
มันเป็น 5; พอครั้งที่สอง มันเป็น 7
รูปที่ 7.1 แสดงให้เห็นว่าการกำหนดค่าให้ใหม่เป็นอย่างไรในแผนภาพสถานะ
ถึงตรงนี้ ผมอยากจะพูดถึงจุดที่สับสนกันมาก เพราะว่าไพธอนใช้เครื่องหมายเท่ากับ (=
) เพื่อการกำหนดค่า มันเลยทำให้เราอยากจะแปล คำสั่งเช่น a = b
ว่าเป็นการแสดงการเท่ากันทางตรรกะ; นั่นคือ การที่บอกว่า a
และ b
นั้นเท่ากัน แต่การแปลความแบบนี้มันผิด
อย่างแรก การเท่ากันเป็นความสัมพันธ์แบบสมมาตร แต่การกำหนดค่าไม่ใช่ เช่น ในคณิตศาสตร์ ถ้า $a=7$ แล้ว $7=a$ แต่ในไพธอน คำสั่ง a = 7
นั้นทำได้ และ 7 = a
นั้นทำไม่ได้
นอกจากนี้ ในคณิตศาสตร์ การเท่ากันทางตรรกะมีค่าเป็น จริง หรือ เท็จ ตลอดเวลา ถ้า $a=b$ ในตอนนี้ แล้ว $a$ จะเท่ากับ $b$ เสมอ ในไพธอน การกำหนดค่าอาจจะทำให้ตัวแปรสองตัวมีค่าเท่ากัน แต่มันจะไม่เป็นแบบนั้นตลอดไป:
>>> a = 5 >>> b = a # a and b are now equal >>> a = 3 # a and b are no longer equal >>> b 5
บรรทัดที่สามเปลี่ยนค่าของ a
แต่ไม่เปลี่ยนค่าของ b
ดังนั้น มันจึงไม่เท่ากันแล้ว
การกำหนดค่าให้ใหม่นั้นบ่อยครั้งมีประโยชน์ แต่เราควรจะต้องใช้มันอย่างระมัดระวัง ถ้า ค่าของตัวแปรนั้นเปลี่ยนบ่อยๆ มันอาจจะทำให้อ่านโค้ดและดีบักได้ยากขึ้น
การกำหนดค่าให้ใหม่ที่ทำเป็นปกติ คือ การปรับค่าตัวแปร (update) โดยที่ ค่าใหม่จะขึ้นอยู่กับค่าเก่า
>>> x = x + 1
มันหมายความว่า “เอาค่าปัจจุบันของ x
, บวกหนึ่ง, แล้วปรับค่า x
ให้เป็นค่าใหม่”
ถ้าเราพยายามที่จะปรับค่าตัวแปรที่ไม่มีอยู่จริง เราจะได้ข้อผิดพลาด เพราะว่าไพธอนจะประเมินค่าทางขวา ก่อนที่จะกำหนดค่าให้ตัวแปร x
:
>>> x = x + 1 NameError: name 'x' is not defined
ก่อนที่เราจะสามารถปรับค่าตัวแปรได้ เราจะต้อง ให้ค่าเริ่มต้น (initialize) กับมันก่อน โดยปกติแล้ว จะใช้การกำหนดค่าแบบง่ายๆ:
>>> x = 0 >>> x = x + 1
การปรับค่าตัวแปรโดยการบวก 1 เข้าไปเรียกว่า การเพิ่มค่า (increment); ส่วนการลบออก 1 เรียกว่า การลดค่า (decrement)
บ่อยครั้งที่คอมพิวเตอร์เคยชินกับการทำงานซ้ำๆ แบบอัตโนมัติ การทำงานอะไรที่เหมือนๆ กันโดยไม่มีข้อผิดพลาด คือ สิ่งที่คอมพิวเตอร์สามารถทำงานได้ดี และคนทำได้ไม่ดีนัก ในโปรแกรมคอมพิวเตอร์ การทำงานซ้ำๆ เรียกอีกอย่างหนึ่งว่า การวนซ้ำ (iteration)
เราได้เห็นฟังก์ชันไปแล้วสองฟังก์ชัน, countdown
และ print_n
, ที่วนซ้ำโดยการใช้การเรียกซ้ำ เพราะว่าการวนซ้ำเป็นอะไรที่ปกติธรรมดามาก ไพธอนจึงเตรียมคุณลักษณะของภาษาที่ทำให้มันง่ายขึ้น หนึ่งคือคำสั่ง for
ที่เราเห็นในหัวข้อที่ 4.2 เราจะกลับไปพูดเรื่องนั้นทีหลัง
อีกคำสั่งหนึ่ง คือ คำสั่ง while
นี่คือเวอร์ชันของฟังก์ชัน countdown
ที่ใช้คำสั่ง while
:
def countdown(n): while n > 0: print(n) n = n - 1 print('Blastoff!')
เราสามารถอ่านคำสั่ง while
เหมือนกับว่าเป็นภาษาอังกฤษทั่วไปเลย มันหมายความว่า “ในขณะที่ n
มีค่ามากกว่า 0, ให้แสดงค่าของ n
และจากนั้นให้ลดค่า n
ลงหนึ่ง เมื่อเราได้ค่าเป็น 0 แล้ว ให้แสดงคำว่า Blastoff!
”
นี่คือกระแสการดำเนินการสำหรับคำสั่ง while
อย่างเป็นทางการ:
while
และทำคำสั่งถัดไปเลยกระแสการทำงานแบบนี้เรียกว่า ลูป (loop) เพราะว่าขั้นตอนที่ 3 จะทำการวนกลับ (ลูปกลับ) ย้อนไปข้างบน
ส่วนที่เป็นตัวของลูปควรจะเปลี่ยนค่าของตัวแปรอย่างน้อยหนึ่งตัว เพื่อที่จะทำให้เงื่อนไขกลายเป็น เท็จ (False) ในที่สุด และทำให้ลูปหยุดทำงาน ไม่เช่นนั้น ลูปจะทำงานไปเรื่อยๆ ชั่วนิรันดร์ ซึ่งเรียกว่า ลูปไม่รู้จบ (infinite loop) แหล่งของความบันเทิงแบบไม่สิ้นสุดของนักวิทยาการคอมพิวเตอร์ คือ การสังเกตพบว่า วิธีใช้งานของยาสระผม คือ “สระให้เกิดฟอง, ล้างออก, ทำซ้ำอีกรอบ” นั้นเป็นลูปไม่รู้จบ
ในกรณีของฟังก์ชัน countdown
เราสามารถพิสูจน์ได้ว่า ลูปจะหยุดทำงาน: ถ้า n
เป็นศูนย์หรือลบ ลูปจะไม่ทำงานเลย ไม่เช่นนั้น ค่า n
จะน้อยลงในแต่ละรอบของการลูป ทำให้ในที่สุด เราก็จะต้องได้ 0
สำหรับลูปอื่นๆ บางลูป มันไม่ง่ายนักที่จะพิสูจน์แบบนั้น เช่น:
def sequence(n): while n != 1: print(n) if n % 2 == 0: # n is even เลขคู่ n = n / 2 else: # n is odd เลขคี่ n = n*3 + 1
เงื่อนไขสำหรับลูปนี้ คือ n != 1
ดังนั้น ลูปจะวนไปเรื่อยๆ จนกว่า n
เป็น 1
ซึ่งทำให้เงื่อนไขนั้นเป็นเท็จ
แต่ละครั้งที่ทำลูป โปรแกรมจะเอ้าต์พุตค่าของ n
และจากนั้นจะตรวจสอบว่ามันเป็นจำนวนคู่หรือจำนวนคี่ ถ้าเป็นจำนวนคู่ n
จะถูกหารด้วย 2 แต่ถ้าเป็นจำนวนคี่ ค่าของ n
จะถูกแทนด้วย n*3 + 1
เช่น ถ้าอาร์กิวเมนต์ที่ผ่านเข้าไปใน sequence
คือ 3 ผลของ n
จะเป็น 3, 10, 5, 16, 8, 4, 2, 1
เนื่องจากบางครั้ง n
มีค่าเพิ่มขึ้นและบางครั้งมีค่าลดลง เลยพิสูจน์ไม่ได้อย่างชัดเจนว่า n
จะมีค่าถึง 1 หรือไม่ หรือโปรแกรมจะหยุดทำงานหรือไม่ สำหรับเฉพาะบางค่าของ n
เราสามารถ พิสูจน์ได้ว่าโปรแกรมจะหยุดทำงาน เช่น ถ้าค่าเริ่มต้นเป็นค่าที่ยกกำลังสอง n
จะเป็นจำนวนคู่ในทุกๆ รอบของลูปจนกระทั่งถึงค่า 1 ตัวอย่างที่แล้วจบด้วยลำดับอย่างว่าโดยเริ่มที่ค่า 16
คำถามที่ยาก คือ เราสามารถพิสูจน์ได้หรือไม่ว่าโปรแกรมจะจบสำหรับค่า n
ที่เป็นบวก ทุกค่า จนถึงตอนนี้ ไม่มีใครสามารถพิสูจน์ว่ามันได้ หรือ พิสูจน์ว่ามันไม่ได้! (ให้ดู http://en.wikipedia.org/wiki/Collatz_conjecture.)
เพื่อเป็นการฝึกทำ ให้เขียนฟังก์ชัน print_n
ใหม่จากหัวข้อที่ 5.8 โดยใช้การวนซ้ำ (iteration) แทนการใช้การเรียกซ้ำ (recursion)
บางครั้ง เราไม่รู้ว่าเมื่อไหร่จะต้องหยุดลูป จนกระทั่งเรามาถึงครึ่งทางของลูป ในกรณีนั้น เราสามารถใช้คำสั่ง break
ในการกระโดดออกจากลูป
ตัวอย่างเช่น สมมติว่าเราต้องเอาอินพุตมาจากผู้ใช้จนกระทั่งเขาพิมพ์เข้ามาว่า done
เราสามารถเขียนว่า:
while True: line = input('> ') if line == 'done': break print(line) print('Done!')
เงื่อนไขของลูปนี้ คือ True
ซึ่งจะเป็นจริงเสมอ ดังนั้น ลูปจะรันจนกว่าจะเจอ คำสั่ง break
ในแต่ละรอบของลูป โปรแกรมจะขอข้อมูลจากผู้ใช้โดยใช้เครื่องหมายวงเล็บสามเหลี่ยมแบบเปิด (<) ถ้าผู้ใช้พิมพ์เข้ามาว่า done
คำสั่ง break
จะทำให้ออกจากลูป ไม่เช่นนั้น โปรแกรมจะสะท้อนสิ่งที่ผู้ใช้พิมพ์เข้ามา แล้วก็กลับไปยังข้างบนสุดของลูป นี่คือตัวอย่างการทำงาน:
> not done not done > done Done!
การเขียนลูป while
แบบนี้เป็นเรื่องปกติ เพราะว่าเราสามารถตรวจสอบเงื่อนไข ที่ไหนก็ได้ในลูป (ไม่เพียงแค่ในส่วนบนสุดของลูป) และเราสามารถยืนยันเงื่อนไขการหยุดได้ (“หยุดเมื่อสิ่งนี้เกิดขึ้น”) แทนที่จะบอกในทางลบ (“ทำงานไปเรื่อยๆ จนกว่าสิ่งนั้นจะเกิด”)
ลูปมักถูกใช้ในโปรแกรมที่คำนวณผลลัพธ์เชิงตัวเลข โดยการเริ่มด้วยค่าประมาณ และวนซ้ำเพื่อทำคำตอบให้ดีขึ้น
ตัวอย่างเช่น วิธีหนึ่งของการคำนวณรากที่สอง คือ วิธีของนิวตัน (Newton’s method) สมมติว่าเราต้องการจะหารากที่สองของ $a$ ถ้าเราเริ่มด้วยค่าประมาณสักอย่าง, $x$, เรา สามารถคำนวณค่าประมาณที่ดีกว่าเดิมโดยใช้สูตรต่อไปนี้:
$$y = \frac{x + a/x}{2}$$ ตัวอย่างเช่น ถ้า $a$ คือ 4 และ $x$ คือ 3
>>> a = 4 >>> x = 3 >>> y = (x + a/x) / 2 >>> y 2.16666666667
ผลลัพธ์ที่ได้จะใกล้เคียงกับคำตอบที่ถูกมากขึ้น ($\sqrt{4} = 2$) ถ้าเราทำกระบวนการนี้ซ้ำ ด้วยการประมาณค่าใหม่ มันก็จะใกล้เข้าไปอีก:
>>> x = y >>> y = (x + a/x) / 2 >>> y 2.00641025641
หลังจากการปรับค่าอีกสองสามครั้ง ค่าประมาณก็เกือบจะเป๊ะ:
>>> x = y >>> y = (x + a/x) / 2 >>> y 2.00001024003 >>> x = y >>> y = (x + a/x) / 2 >>> y 2.00000000003
โดยทั่วไปแล้ว เราไม่รู้ก่อนว่าจะต้องทำกี่ขั้นตอนจนกว่าจะได้คำตอบที่ถูกต้อง แต่เรารู้ เมื่อเราได้ค่านั้นมาแล้วเพราะว่าค่าประมาณนั้นไม่เปลี่ยนแล้ว:
>>> x = y >>> y = (x + a/x) / 2 >>> y 2.0 >>> x = y >>> y = (x + a/x) / 2 >>> y 2.0
เมื่อ y == x
เราสามารถหยุดได้ นี่คือลูปที่เริ่มด้วยค่าประมาณการเริ่มต้น, x
, และปรับค่าให้ดีขึ้นเรื่อยๆ จนกระทั่งมันหยุดเปลี่ยน:
while True: print(x) y = (x + a/x) / 2 if y == x: break x = y
สำหรับค่า a
ส่วนมาก วิธีนี้ทำงานได้ดี แต่โดยทั่วไปแล้วมันอันตรายถ้าจะทดสอบ การเท่ากันของค่าจุดลอย float
ค่าจุดลอยเป็นเพียงแค่ค่าประมาณ: จำนวนตรรกยะส่วนมาก เช่น $1/3$ และอตรรกยะ เช่น $\sqrt{2}$ ไม่สามารถแทนให้ตรงเป๊ะด้วยข้อมูลชนิด float
แทนที่จะตรวจสอบว่า x
และ y
เท่ากันเป๊ะ มันปลอดภัยกว่าที่จะใช้ ฟังก์ชันที่มีอยู่ในตัว abs
เพื่อคำนวณหาค่าสัมบูรณ์, หรือขนาด, ของความต่างระหว่าง สองค่านี้:
if abs(y-x) < epsilon: break
โดยที่ epsilon
มีค่าเช่น 0.0000001
ซึ่งกำหนดว่าใกล้เท่าไหร่ จึงจะเพียงพอ
วิธีของนิวตัน (Newton’s method) เป็นตัวอย่างของ อัลกอริทึม (algorithm): มันคือกระบวนการทางกลไกสำหรับแก้ปัญหาของหมวดหมู่หนึ่งๆ (ในกรณีนี้ การคำนวณรากที่สอง)
เพื่อที่จะเข้าใจว่า อัลกอริทึม คืออะไร มันอาจจะช่วยโดยการเริ่มด้วยบางสิ่งที่ไม่ใช่อัลกอริทึม เมื่อเราเรียนรู้ที่จะคูณเลขหลักเดียว เราอาจจะจำสูตรคูณ มันทำให้เราต้องท่องจำคำตอบ 100 อย่างโดยเฉพาะ ความรู้แบบนี้ไม่ใช่อัลกอริทึม
แต่ถ้าเรา “ขี้เกียจ’ เราอาจจะเรียนรู้เทคนิคพิเศษสองสามอย่าง เช่น ในการที่จะหาผลคูณ ของ $n$ และ 9 เราสามารถเขียน $n-1$ เป็นเลขหลักแรก และ $10-n$ เป็นเลขหลักที่สอง เทคนิคนี้เป็นการแก้ปัญหาแบบครอบคลุมสำหรับการคูณเลขหลักเดียวตัวไหนก็ได้กับ 9 นี่คือ อัลกอริทึม!
ในทำนองเดียวกัน เทคนิคที่เราเรียนรู้สำหรับการบวกและลบแบบทดยืม และการหารยาวล้วนเป็น อัลกอริทึม ลักษณะหนึ่งของอัลกอริทึม คือ มันไม่ต้องการความฉลาดในการทำงาน มันเป็น กลไกที่แต่ละขั้นตอนนั้นทำต่อๆ กันไปโดยอ้างอิงจากกฎเกณฑ์ง่ายๆ ชุดหนึ่ง
การทำงานตามอัลกอริทึมมันน่าเบื่อ แต่การออกแบบนั้นน่าสนใจ ประเทืองปัญญา และเป็น ศูนย์กลางของวิทยาการคอมพิวเตอร์
บางสิ่งที่ผู้คนทำตามธรรมชาติโดยไม่ยากหรือไม่ต้องคิด เป็นสิ่งที่ยากที่สุดในการเขียนออกมาเป็น อัลกอริทึม การทำความเข้าใจภาษาธรรมชาติ (natural language) เป็นตัวอย่างที่ดี เราทุกคนเข้าใจมัน แต่จนกระทั่งบัดนี้ ไม่มีใครสามารถจะอธิบายได้ว่าเราทำมันได้ อย่างไร อย่างน้อยก็ไม่ใช่ในรูปแบบของอัลกอริทึม
เมื่อเราเริ่มเขียนโปรแกรมที่ใหญ่ขึ้น เราอาจจะใช้เวลาในการดีบักมากขึ้น โค้ดที่มากขึ้น หมายความว่า โอกาสที่มากขึ้นที่จะเกิดข้อผิดพลาด และมีหลายตำแหน่งมากขึ้นที่บัก ซ่อนอยู่
ทางหนึ่งที่จะลดเวลาการดีบัก คือ “การดีบักโดยการตัดครึ่งส่วน” เช่น ถ้าในโปรแกรมมี 100 บรรทัด แล้วเราตรวจสอบไปทีละบรรทัด มันก็จะใช้ 100 ขั้นตอน
แทนที่จะทำแบบนั้น ให้แบ่งปัญหาลงครึ่งหนึ่ง ให้ดูแถวๆ ตรงกลางของโปรแกรม หาค่าระหว่างทางที่เราสามารถตรวจสอบได้ เพิ่มคำสั่ง print
(หรืออะไรสักอย่าง ที่สามารถทำให้เราตรวจสอบความถูกต้องได้) และรันโปรแกรม
ถ้าตรงกลางโปรแกรมมันผิด จะต้องมีปัญหาที่ครึ่งแรกของโปรแกรมแน่ๆ แต่ถ้ามันถูก ปัญหา จะอยู่ที่ครึ่งหลังของโปรแกรม
ทุกครั้งที่เราทำการตรวจสอบแบบนี้ เราจะแบ่งครึ่งจำนวนบรรทัดที่เราต้องค้นหา หลังจากผ่านไป ุหกขั้นตอนแล้ว (ซึ่งน้อยกว่า 100 ขั้นตอน) เราจะเหลือแค่หนึ่งหรือสองบรรทัดเท่านั้น อย่างน้อย ก็ตามทฤษฎี
ในทางปฏิบัติ มันไม่ค่อยชัดเท่าไหร่ว่า “กึ่งกลางโปรแกรม” คืออะไร และอาจจะไม่ สามารถตรวจสอบมันได้ มันไม่ค่อยเข้าท่าเท่าไหร่ที่จะนับบรรทัดและหาจุดกึ่งกลางเป๊ะ แทนที่จะทำแบบนั้น ให้คิดว่าตำแหน่งใดในโปรแกรมที่อาจจะมีข้อผิดพลาด และตรงไหน ที่มันตรวจสอบได้ง่าย จากนั้นให้เลือกมาจุดหนึ่งซึ่งเราคิดว่ามีโอกาสเท่าๆ กันที่จะเกิดบัก ก่อนหน้าและหลังจากจุดนั้น
แบบฝึกหัด 1
คัดลอกลูปจากหัวข้อที่ 7.5 และหุ้มมันด้วยฟังก์ชันที่ชื่อว่า mysqrt
ซึ่งรับ a
มาเป็นพารามิเตอร์ และคืนค่าประมาณของรากที่สองของ a
เพื่อทดสอบโค้ด ให้เขียนฟังก์ชันชื่อว่า test_square_root
ซึ่งพิมพ์ ตารางหน้าตาแบบนี้:
a mysqrt(a) math.sqrt(a) diff - --------- ------------ ---- 1.0 1.0 1.0 0.0 2.0 1.41421356237 1.41421356237 2.22044604925e-16 3.0 1.73205080757 1.73205080757 0.0 4.0 2.0 2.0 0.0 5.0 2.2360679775 2.2360679775 0.0 6.0 2.44948974278 2.44948974278 0.0 7.0 2.64575131106 2.64575131106 0.0 8.0 2.82842712475 2.82842712475 4.4408920985e-16 9.0 3.0 3.0 0.0
คอลัมน์แรก เป็นเลข, $a$; คอลัมน์ที่สอง เป็นรากที่สองของ $a$ ที่คำนวณจากฟังก์ชัน mysqrt
; คอลัมน์ที่สาม เป็นรากที่สองที่คำนวณจาก math.sqrt
; คอลัมน์ ที่สี่ เป็นค่าสัมบูรณ์ของผลต่างระหว่างค่าประมาณทั้งสองค่า
แบบฝึกหัด 2
ฟังก์ชันที่มีอยู่ในตัว eval
รับสายอักขระเข้ามา และประเมินค่าโดยใช้ตัวแปลภาษาไพธอน เช่น:
>>> eval('1 + 2 * 3') 7 >>> import math >>> eval('math.sqrt(5)') 2.2360679774997898 >>> eval('type(math.pi)') <class 'float'>
ให้เขียนฟังก์ชันที่ชื่อว่า eval_loop
ที่วนรับค่าจากผู้ใช้ และประเมินค่าที่รับเข้ามา โดยใช้ฟังก์ชัน eval
และพิมพ์ผลลัพธ์ออกมา
มันควรจะทำงานไปเรื่อยๆ จนกว่าผู้ใช้จะใส่ค่า 'done'
เข้ามา และจากนั้น ให้คืนค่ากลับไปเป็นค่าของนิพจน์สุดท้ายที่ฟังก์ชันนี้ประเมิน
แบบฝึกหัด 3
นักคณิตศาสตร์ที่ชื่อว่า ศรีนิวาสา รามานุจัน (Srinivasa Ramanujan) พบอนุกรมไม่รู้จบ ซึ่งสามารถใช้สร้างค่าประมาณของ $1 / \pi$:
$$\frac{1}{\pi} = \frac{2\sqrt{2}}{9801} \sum^\infty_{k=0} \frac{(4k)!(1103+26390k)}{(k!)^4 396^{4k}}$$
ให้เขียนฟังก์ชันที่ชื่อว่า estimate_pi
ซึ่งใช้สูตรนี้ในการคำนวณ และคืนค่า ประมาณของ $\pi$ มันควรจะใช้ลูป while
ในการคำนวณผลรวมของพจน์ต่างๆ จนกว่าพจน์สุดท้ายจะมีค่าน้อยกว่า 1e-15
(ซึ่งเป็นสัญลักษณ์ที่ไพธอนใช้แทนค่า $10^{-15}$) เราสามารถตรวจสอบผลลัพธ์โดยการเปรียบเทียบค่าที่ได้กับค่า math.pi
เฉลย: http://thinkpython2.com/code/pi.py
https://greenteapress.com/thinkpython2/html/thinkpython2008.html