บทที่ 1
การเขียนโปรแกรม และการคิดเชิงคำนวณ#

จุดมุ่งหมายของบทนี้

  • เรียนรู้วิธีการติดตั้งและใช้ซอฟต์แวร์ในการเขียนโปรแกรมภาษาไพทอน

  • แบ่งปัญหาออกเป็นส่วนย่อย ที่ต้องแก้ออกเป็นส่วนเล็ก ๆ

  • การเขียนฟังก์ชันที่นำกลับมาใช้อีกได้

  • การใช้วงวน for while แบบไม่มีตัวแปร

  • การกำหนดเงื่อนไข if และ else ประกอบกับบูลีน และตัวดำเนินการบูลีน and or not

  • การใช้คำสั่ง break

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

การติดตั้งไพทอน#

ติดตั้งโปรแกรม Anaconda#

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

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

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

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

  • สิ่งแวดล้อมสำหรับการพัฒนาแบบเบ็ดเสร็จ (Interactive Development Environment หรือ IDE) เป็นเครื่องมือที่ไว้สำหรับอำนวยความสะดวกในการเขียนโค้ด ส่วนหลักคือหน้าต่างที่ไว้สำหรับพิมพ์โค้ด และบันทึกโค้ดเก็บลงไฟล์ รวมถึงฟีเจอร์อื่น ๆ ที่ทำให้เขียนโค้ดได้อย่างมีประสิทธิภาพมากขึ้น เช่น ปรับเปลี่ยนสีตัวอักษรต่าง ๆ เพื่อให้ดูง่ายขึ้น และสามารถปรับหน้าต่างเพื่อแสดงโค้ดหลาย ๆ ไฟล์ หรือหลาย ๆ หน้า ได้ตามความพอใจ

../../_images/jupyternb.png

ภาพที่ 1 ภาพตัวอย่างหน้าต่าง Jupyter notebook#

  • Jupyter notebook เป็น IDE ที่เรียบง่ายที่สุด และเป็นที่นิยมเป็นอย่างมากในหมู่นักวิเคราะห์ข้อมูล เพราะถูกออกแบบมาเพื่อการวิเคราะห์ข้อมูลโดยเฉพาะ เนื่องจาก Jupyter notebook รองรับการรันโค้ดทีละก้อนเล็ก ๆ และแสดงผลการคำนวณออกมาให้เห็นชัดเจน IDE นี้จึงถูกเรียกว่า notebook เพราะเปรียบเสมือนกับนักวิทยาศาสตร์ข้อมูลทำการวิเคราะห์ข้อมูลและจดบันทึกกระบวนการวิเคราะห์และผลวิเคราะห์ไว้ในสมุด เพื่อให้ทราบที่มาที่ไปของผลสรุปต่าง ๆ ที่ได้จากการวิเคราะห์ ไฟล์ที่จะสามารถเปิดด้วย Jupyter notebook ได้นั้นจะต้องมีนามสกุลไฟล์ (file extension) เป็น .ipynb

../../_images/jupyterlab.png

ภาพที่ 2 ภาพตัวอย่างหน้าต่าง JupyterLab#

  • Jupyterlab เป็น IDE ที่เบาและ feature น้อยกว่า VSCode ทำให้ใช้ได้ไม่ยากนักสำหรับมือใหม่ ถูกออกแบบมาเพื่อการประมวลผลและวิเคราะห์ข้อมูลเป็นจุดประสงค์หลัก เหมาะกับการจัดการกับงานที่ต้องอาจจะต้องใช้ไฟล์ข้อมูล หลายไฟล์ และไฟล์ jupyter notebook (.ipynb) หลาย ๆ ไฟล์ และใช้ Terminal ควบคู่ไปกับการทำงานด้วย

Visual Studio Code (VSCode)#

นอกจากนั้นให้ลงโปรแกรม IDE ที่ชื่อว่า Visual Studio Code (VSCode)

../../_images/vscode.png

ภาพที่ 3 ภาพตัวอย่างหน้าต่าง Visual Studio Code#

Visual Studio Code (VSCode) เป็น IDE ที่มีฟีเจอร์หลากหลายชนิด สามารถปรับรูปลักษณ์ตามความชอบได้หลายแบบ รวมถึงรองรับภาษาโปรแกรมหลายภาษา เหมาะกับการเขียนโปรแกรมที่ประกอบด้วยหลาย ๆ ไฟล์ และต้องมีการตรวจสอบโค้ดอย่างรัดกุม เป็น IDE ที่ดาวน์โหลดได้โดยไม่มีค่าใช้จ่าย และเป็นที่แพร่หลายมากในการเขียนโปรแกรมภาษาไพทอน VSCode สามารถเปิด Terminal ขึ้นมาใช้ในโปรแกรมได้อีกด้วย

หลังจากติดตั้งเรียบร้อยแล้ว ให้ไปที่ View > Extension จากนั้นพิมพ์ python และติดตั้ง python extension (อย่าลืมเช็คว่าเป็นของ Microsoft)

เขียนโปรแกรมแบบตอบโต้ (Interactive)#

หลังจากที่ลงโปรแกรม Anaconda เรียบร้อยแล้ว เราสามารถเริ่มเขียนโปรแกรมเป็นภาษาไพทอนได้ทันทีเพื่อทดสอบว่าลงโปรแกรมได้โดยสมบูรณ์แล้ว ให้เริ่มจากการเปิด Anaconda Prompt หรือ Anaconda Powershell ก็ได้ซึ่งเป็นเทอร์มินัลสำหรับผู้ที่ใช้ Windows หรือเปิด Terminal App ซึ่งเป็นเทอร์มินัลสำหรับผู้ที่ใช้ Mac จากนั้นให้พิมพ์คำว่า ipython ลงไปเพื่อเปิด Interactive Python Interpreter (ipython) และลองพิมพ์

 print('Hello, world!')

และกด Enter เพื่อทำการส่งคำสั่งที่เขียนเป็นภาษาไพทอนเพื่อให้ Python interpreter แปลเป็นภาษาเครื่องและรันคำสั่ง จากนั้นหน้าจอจะแสดงผลดังนี้

Python 3.7.6 (default, Jan  8 2020, 13:42:34)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.29.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: print('Hello, world!')
Hello, world!

In [2]:

โปรแกรม ipython เป็นโปรแกรมที่ทำให้เราสามารถพิมพ์คำสั่งภาษาไพทอนและกดรันเพื่อเห็นผลลัพธ์ได้ทันที ทำให้เราสามารถตอบโต้กับเครื่องไปมาเป็นภาษาไพทอนได้

เขียนโปรแกรมใส่ไฟล์ และสั่งรันทั้งไฟล์#

ในบางครั้งเราต้องการเขียนโปรแกรมที่ค่อนข้างยาว และต้องการรันโปรแกรมทั้งหมดที่อยู่ในไฟล์ เราสามารถเขียนโปรแกรมโดยการเขียนคำสั่งทั้งหมดใส่ไว้ในไฟล์ก่อน ซึ่งจะต้องมีสกุลไฟล์ .py จากนั้นค่อยสั่งให้ Python interpreter แปลและรันคำสั่งทั้งหมดในครั้งเดียว ไล่ไปทีละบรรทัด

รันด้วย JupyterLab#

วิธีการเขียนโปรแกรมใส่ไฟล์และรันโปรแกรมที่อยู่ในไฟล์นั้นผ่าน JupyterLab มีดังนี้

  1. เปิด Anaconda navigator

  2. กด Install (ถ้ายังไม่ได้ install) JupyterLab

  3. กด Launch JupyterLab

  4. กด File -> New -> Python file และตั้งชื่อว่า myfirstprogram.py ไฟล์จะถูกเปิดขึ้นบนหน้าต่างใหม่

  5. พิมพ์คำว่า print('Hello, world!') ใส่ในไฟล์และกด Save

  6. (Mac) กด File -> New -> Terminal เพื่อเปิด Terminal เป็นหน้าต่างใหม่บน JupyterLab (Windows) เปิด Anaconda Powershell

  7. พิมพ์คำว่า python myfirstprogram.py ลงไปบนเทอร์มินัล จากนั้นให้กด enter

หน้าต่างเทอร์มินัลจะแสดงคำว่า Hello, world! แต่ว่าเทอร์มินัลของแต่ละเครื่องก็จะต่างกันไป

$ python myfirstprogram.py
Hello, world!

รันด้วย VS Code#

วิธีการเขียนโปรแกรมใส่ไฟล์และรันโปรแกรมที่อยู่ในไฟล์นั้นผ่าน VS Code มีดังนี้

  1. เปิด VS Code

  2. กด File -> New File และตั้งชื่อว่า myfirstprogram.py ไฟล์จะถูกเปิดขึ้นบนหน้าต่างใหม่

  3. พิมพ์คำว่า print('Hello, world!') ใส่ในไฟล์และกด Save

  4. กด Terminal -> New Terminal เพื่อเปิด Terminal เป็นหน้าต่างใหม่บน VS Code

  5. พิมพ์คำว่า python myfirstprogram.py ลงไปบนเทอร์มินัล จากนั้นให้กด enter

หน้าต่างเทอร์มินัลจะแสดงคำว่า Hello, world! แต่ว่าเทอร์มินัลของแต่ละเครื่องก็จะต่างกันไป

การคิดเชิงคำนวณ#

หลักสำคัญของการเขียนโปรแกรมมิได้อยู่ที่การจดจำคำสั่งทั้งหมด แต่เน้นที่การนำคำสั่งมาประยุกต์ใช้และประกอบกันอย่างเหมาะสมเพื่อให้ระบบสามารถปฏิบัติงานตามที่ผู้พัฒนาต้องการได้อย่างมีประสิทธิภาพ เมื่อได้รับโจทย์ เช่น การดึงข้อมูลความคิดเห็นจากโซเชียลมีเดียเพื่อนำมาวิเคราะห์ว่าผู้สมัครรับเลือกตั้งรายใดได้รับความสนใจจากสาธารณชนมากที่สุด หรือเขียนแอปพลิเคชันในการวิเคราะห์การใช้ภาษาของข้อความที่กำหนดให้ จะพบว่าโจทย์ดังกล่าวมีขอบเขตที่กว้างและซับซ้อน จนอาจไม่รู้ว่าจะเริ่มจากจุดไหนก่อน การแก้ปัญหาเชิงคำนวณ (computational thinking) เป็นหลักการสำคัญที่จะช่วยในการแก้โจทย์ลักษณะนี้ โดยมีองค์ประกอบหลัก 4 ประการ

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

  2. การสังเกตหาแบบแผน (Pattern Recognition) หมายถึง การพิจารณาและค้นหาแบบแผนหรือรูปแบบที่เกิดขึ้นซ้ำ ๆ ในปัญหาย่อย ๆ ซึ่งจะช่วยให้สามารถแก้ไขปัญหาได้อย่างมีประสิทธิภาพ เช่น ในกระบวนการประกอบลิ้นชัก แม้ว่าแต่ละลิ้นชักจะมีขนาดแตกต่างกัน แต่ขั้นตอนการประกอบ เช่น การขันน็อตทั้งสี่ด้านและการติดตั้งมือจับ ยังคงมีรูปแบบที่เหมือนกัน

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

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

1. ไล่ประกอบขาตู้ แล้วเอาพักไว้ก่อน
2. ประกอบโครงตู้ แล้วเอาพักไว้ก่อน
3. ประกอบผลลัพธ์จาก 1 และ 2
4. ถ้าตู้มี k ลิ้นชัก ไล่ประกอบตัวลิ้นชักจนครบ k อัน
5. สอดลิ้นชักทั้ง k อันเข้าไปในโครงตู้

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

หุ่นยนต์คาเรล#

ทักษะการคิดเชิงคำนวณตามตัวอย่างที่กล่าวมาข้างต้น ถือเป็นทักษะที่สำคัญยิ่งสำหรับการเขียนโปรแกรม ซึ่งในบทนี้เราจะฝึกฝนผ่านการใช้โปรแกรมที่ชื่อว่า คาเรล (Karel) [Pattis, 1994] คาเรลเป็นหุ่นยนต์จำลองขนาดเล็กที่สามารถเคลื่อนที่ไปมาภายในโลกสี่เหลี่ยมที่ถูกกำหนดไว้ โดยผู้ใช้สามารถเขียนโปรแกรมด้วยภาษาไพทอนเพื่อสั่งการคาเรลให้ปฏิบัติตามคำสั่งต่าง ๆ ที่ตั้งไว้สำหรับการแก้โจทย์ในสถานการณ์ที่กำหนดได้ ในขั้นตอนนี้ เราจะใช้คาเรลเพื่อฝึกฝนทักษะการคิดเชิงคำนวณ ก่อนที่จะเข้าสู่การเขียนโปรแกรมภาษาไพทอนในระดับที่ซับซ้อนขึ้น ซึ่งจำเป็นต้องเรียนรู้คำสั่งเพิ่มเติมอีกมากมาย วัตถุประสงค์ของการฝึกฝนผ่านคาเรล คือ การพัฒนาทักษะการคิดเชิงคำนวณของนักเขียนโปรแกรม โดยไม่ต้องจำคำสั่งที่ซับซ้อนจำนวนมาก

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

คำสั่ง

คำอธิบาย

move()

สั่งให้คาเรลเดินไปข้างหน้า ถ้าข้างหน้ามีกําแพงมันจะชนและเกิดข้อผิดพลาด

turn_left()

สั่งให้คาเรลเลี้ยวซ้าย

put_beeper()

วางกระดิ่งหนึ่งอันไว้บนจุดที่ยืนอยู่

pick_beeper()

เก็บกระดิ่งขึ้นมาหนึ่งอันจากจุดที่ยืนอยู่ (ถ้าไม่มีกระดิ่งที่จุดนั้น จะเกิดข้อผิดพลาด)

ตัวอย่าง#

หากคาเรลอยู่ในโลกขนาด 2x2 และจุดเริ่มต้นอยู่ที่มุมซ้ายล่างของโลกและหันหน้าไปทางทิศตะวันออก และจุดหมายคือให้คาเรลเก็บกระดิ่งที่มุมขวาล่าง และไปเดินทางไปยังมุมซ้ายบน ดังภาพข้างล่าง

../../_images/karel-ex1.png

ภาพที่ 4 โลกของคาเรลในโจทย์ จุดเริ่มต้นของโจทย์แสดงอยู่ในภาพด้านซ้าย จุดมุ่งหมายของโจทย์แสดงอยู่ในภาพด้านขวา#

โจทย์นี้อาศัยนำคำสั่งหลาย ๆ คำสั่งมาต่อกันในลำดับที่เหมาะสม ดังนี้

move()
pick_beeper()
turn_left()
move()
turn_left()
move()
put_beeper()

ถ้าหากโปรแกรมสั่งให้คาเรลชนกำแพง ดังโค้ดข้างล่างนี้

move()
move()

โปรแกรมจะเกิดข้อผิดพลาด และจะหยุดทำงานทันที

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

move()
turn_left()
move()
pick_beeper()

โปรแกรมจะเกิดข้อผิดพลาด และจะหยุดทำงานทันทีเช่นกัน

โปรแกรมและฟังก์ชัน#

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

../../_images/karel-ex2.png

ภาพที่ 5 โลกของคาเรลในโจทย์ จุดเริ่มต้นของโจทย์แสดงอยู่ในภาพด้านซ้าย จุดมุ่งหมายของโจทย์แสดงอยู่ในภาพด้านขวา#

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

# โปรแกรมสำหรับแก้โจทย์แรกในบทนี้
from stanfordkarel import *

def main():
    move()
    pick_beeper()
    move()
    turn_left()
    move()
    turn_left()
    turn_left()
    turn_left()
    move()
    move()
    put_beeper()
    move()

if __name__ == "__main__":
    run_karel_program()

ส่วนประกอบของโปรแกรมในไฟล์นี้มีดังนี้

คอมเมนต์#

# โปรแกรมสำหรับแก้โจทย์แรกในบทนี้

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

การนำเข้าไลบรารี#

from stanfordkarel import *

ไลบรารี (Library) คือ คลังของโค้ดหรือฟังก์ชันที่ถูกเขียนขึ้นล่วงหน้าและรวบรวมไว้เพื่อให้นักพัฒนาสามารถนำไปใช้งานได้โดยไม่ต้องเขียนโค้ดใหม่ตั้งแต่ต้น ไลบรารีมักจะประกอบไปด้วยฟังก์ชัน และเครื่องมือที่ช่วยในการทำงานเฉพาะด้าน ในบทนี้ไลบรารีที่ต้องนำเข้ามาคือ stanfordkarel ซึ่งมีฟังก์ชันในการสั่งการคาเรลให้ปฏิบัติงานต่าง ๆ ได้ เช่น move() และ put_beeper()

การประกาศฟังก์ชัน#

def main():
    move()
    pick_beeper()
    move()

ส่วนนี้เรียกว่า ฟังก์ชัน โดยฟังก์ชันมีลักษณะสำคัญดังนี้:

  • ส่วนหัวของฟังก์ชัน (function header) ประกอบด้วย

    • คำสำคัญ (keyword) def ซึ่งใช้สำหรับประกาศฟังก์ชันใหม่

    • ชื่อของฟังก์ชันซึ่งตามหลังคำว่า def และตามด้วยวงเล็บเปิด-ปิด และโคลอน ():

  • ส่วนเนื้อหาของฟังก์ชัน (function body) คือ กลุ่มของคำสั่งที่ประกอบอยู่ภายในฟังก์ชัน โดยคำสั่งเหล่านี้จะต้องมีการย่อหน้า (indentation) ในทุกบรรทัด เพื่อบ่งชี้ว่าคำสั่งเหล่านี้เป็นส่วนหนึ่งของฟังก์ชันนั้น

กำหนดคำสั่งการรันไฟล์ไพทอน#

if __name__ == "__main__":
    run_karel_program()

โปรแกรมตัวอย่างนี้ถูกจัดเก็บในไฟล์ที่มีนามสกุล .py ซึ่งเมื่อไฟล์ถูกเรียกใช้ โปรแกรมจะทำงานทั้งไฟล์ โค้ดที่แสดงข้างต้นเป็นการกำหนดลำดับการทำงานของโปรแกรม โดยใช้โครงสร้าง if __name__ == "__main__": เพื่อระบุว่า หากโปรแกรมถูกรันโดยตรง ฟังก์ชัน run_karel_program() จะถูกเรียกใช้งานก่อน ซึ่งฟังก์ชันนี้มีหน้าที่ในการโหลดหน้าต่างที่แสดงผลหุ่นยนต์คาเรล จากนั้นโปรแกรมจะไปเรียกฟังก์ชัน main() ซึ่งได้ถูกประกาศไว้ในไฟล์เดียวกันนี้

การแบ่งปัญหาออกเป็นส่วนย่อย#

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

turn_left()
turn_left()
turn_left()

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

def main():
    move()
    pick_beeper()
    move()
    turn_left()
    move()
    turn_right() #หันขวา ฟังก์ชันใหม่
    move()
    move()
    put_beeper()
    move()

def turn_right():
    turn_left()
    turn_left()
    turn_left()

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

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

สรุปส่วนต่าง ๆ ของฟังก์ชันได้ดังนี้

../../_images/function.png

ข้อควรระวัง#

การสร้างฟังก์ชันในภาษาไพทอนจำเป็นต้องปฏิบัติตามรูปแบบที่ถูกต้องตามกฎไวยากรณ์ (syntax) เนื่องจากภาษาไพทอน เป็นภาษาการเขียนโปรแกรมที่มีโครงสร้างชัดเจนและยึดกับไวยากรณ์อย่างเคร่งครัด การเขียนโค้ดผิดจากไวยากรณ์แม้แต่เพียงเล็กน้อยจะส่งผลให้โปรแกรมไม่สามารถทำงานได้ตามที่คาดหมาย ตัวแปลภาษาไพทอนจะตรวจสอบโค้ดเมื่อมีการรันโปรแกรม และหากพบความผิดพลาด โปรแกรมจะหยุดทำงานทันทีพร้อมแสดงข้อความแจ้งข้อผิดพลาด (error message) ซึ่งศัพท์เทคนิคในการเขียนโปรแกรมเรียกว่าโยน (throw) ข้อผิดพลาด เพื่อช่วยให้ผู้เขียนโปรแกรมสามารถตรวจสอบและแก้ไขโค้ดได้อย่างถูกต้อง ข้อผิดพลาดมีหลายประเภท ข้อผิดพลาดที่พบบ่อยประเภทหนึ่งคือ ข้อผิดพลาดทางไวยากรณ์ (syntax error) เช่น การลืมวงเล็บ การพิมพ์ชื่อฟังก์ชันผิด หรือการจัดวางบรรทัดโค้ดที่ไม่ถูกต้อง ตัวแปลภาษาไพทอนก็จะทำการโยน SyntaxError ดังตัวอย่างดังนี้

  File "my_first_karel.py", line 4
    def main()
             ^
SyntaxError: invalid syntax

ตัวแปลภาษาไพทอนโยน SyntaxError มา เพราะว่าเราลืม : ท้ายหัวฟังก์ชัน ในบรรทัดที่ 4 เพื่อแจ้งให้ทราบว่าเราเขียนโค้ดผิดไวยากรณ์ หรือถ้าเราลืม () ก็จะได้ SyntaxError เช่นกัน

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

  File "my_first_karel.py", line 4
    def main:
            ^
SyntaxError: invalid syntax

ข้อผิดพลาดอีกประเภทที่พบบ่อยสำหรับผู้ที่กำลังเริ่มต้นเขียนโปรแกรมคือ ข้อผิดพลาดในการจัดย่อหน้า (indentation) ตัวแปลภาษาไพทอนจะโยนข้อผิดพลาด IndentationError มาให้ ดังตัวอย่างดังนี้

  File "my_first_karel.py", line 5
   move()
      ^
IndentationError: expected an indented block

ตัวแปลภาษาไพทอนโยน IndentationError มาให้ถ้าเราลืมกั้นย่อหน้าให้คำสั่ง move() ในบรรทัดที่ 5 พร้อมแนะนำวิธีการแก้ว่าอาจจะต้องเพิ่มย่อหน้าให้เป็นบล็อกใหม่ (expected an indented block)

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

การคิดเชิงคำนวณเพื่อในการเขียนโปรแกรม#

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

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

../../_images/repair-road.png

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

def main():
    move()
    turn_right()
    move()
    put_beeper()
    turn_right()
    turn_right()
    move()
    turn_right()
    move()
    move()
    move()
    # ไม่สมบูรณ์ยังมีต่ออีก...    

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

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

  1. การซ่อมหลุมแรก

  2. การเดินไปยังหลุมที่สอง

  3. การซ่อมหลุมที่สอง

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

def fill_hole1():
    pass

def move_to_next_hole()
    move()
    move()

def fill_hole2():
    pass

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

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

../../_images/deposed-repair-road.png

ภาพที่ 6 กรอบสีแดงแสดงให้ว่าสองปัญหาย่อยเป็นแบบแผนของปัญหาที่เหมือนกัน#

เพราะฉะนั้นโค้ดในการแก้ปัญหานี้จะมีการเรียกใช้ฟังก์ชันซ้ำ เพื่อแก้ปัญหาย่อยที่มีแบบแผนเดียวกัน ฟังก์ชัน fill_hole1 และ fill_hole2 จึงถูกแทนที่ด้วยการเรียกฟังก์ชัน fill_hole สองครั้งในตำแหน่งที่เหมาะสมดังนี้

def main():
    fill_hole()
    move_to_next_hole()
    fill_hole()
    
def fill_hole():
    pass

def move_to_next_hole()
    move()
    move()

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

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

  1. กระโดดลงไปในหลุม

  2. วางกระดิ่ง

  3. กระโดดออกมานอกหลุม

หลังจากที่เราสามารถเริ่มลงมือเขียนโค้ด (อิมพลิเมนต์) อัลกอริทึมในการ fill_hole ได้ดังนี้

def fill_hole():
    jump_in()
    put_beeper()
    jump_out()

def jump_in():
    move()
    turn_right()
    move()

def jump_out():
    turn_around()
    move()
    turn_right()
    move()

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

def fill_hole():
    move_L()
    put_beeper()
    jump_out()

def move_L():
    move()
    turn_right()
    move()

def jump_out():
    turn_around()
    moveL()

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

วงวน for#

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

วงวน for เป็นการควบคุมการทำงานของโปรแกรมให้วนซ้ำ ๆ ตามจำนวนครั้งที่กำหนดไว้ล่วงหน้า ตัวอย่างเช่น ถ้าเราต้องการให้คาเรลวางกระดิ่งลงบนถนนที่มีหลุมบ่อ 5 หลุม โดยที่คาเรลต้องวางกระดิ่งลงในทุก ๆ หลุม โดยเริ่มจากหลุมที่ 1 จนถึงหลุมที่ 5 โค้ดที่เขียนด้วยวงวน for จะมีลักษณะดังนี้

def main():
    move()
    for i in range(5):
        put_beeper()
        move()
    turn_around()

ซึ่งหมายความว่า เราจะทำการประมวลผลกลุ่มคำสั่ง (code block) ที่อยู่ภายใต้คำสั่ง for ทั้งหมด 5 ครั้ง (นักพัฒนาโปรแกรมมักเรียกกระบวนการนี้อย่างไม่เป็นทางการว่า ฟอร์ลูปไป 5 รอบ) หรือเท่ากับว่าทำการประมวลผลโค้ดดังนี้

def main():
    move()
    put_beeper()
    move()
    put_beeper()
    move()
    put_beeper()
    move()
    put_beeper()
    move()
    put_beeper()
    move()
    turn_around()

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

for i in range(จำนวนครั้งที่จะวนซ้ำ):
    คำสั่งที่ต้องการวนซ้ำ
    คำสั่งที่ต้องการวนซ้ำ

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

วงวน while#

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

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

../../_images/while-ex1.png

ภาพที่ 7 จุดเริ่มต้นของโจทย์#

../../_images/while-ex2.png

ภาพที่ 8 จุดมุ่งหมายของโจทย์#

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

def main():
    move()
    for i in range(6):
        put_beeper()
        move()

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

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

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

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

def main():
    move()
    while front_is_clear():
        put_beeper()
        move()

เราเปลี่ยน for i in range(6): เป็น while front_is_clear(): ซึ่งหมายความว่า ก่อนที่โปรแกรมจะทำการประมวลผลในแต่ละรอบของการวนซ้ำ จะมีการตรวจสอบเงื่อนไขว่าด้านหน้าของคาเรลว่างอยู่หรือไม่ (front_is_clear()) หากด้านหน้ายังว่างอยู่ โปรแกรมจะทำการประมวลผลคำสั่งที่อยู่ภายในโครงสร้างการวนซ้ำ เมื่อประมวลผลคำสั่งในแต่ละรอบเสร็จสิ้น โปรแกรมจะตรวจสอบเงื่อนไขอีกครั้ง หากเงื่อนไขยังคงเป็นจริง (ด้านหน้ายังว่าง) โปรแกรมจะวนซ้ำและทำงานตามคำสั่งในโครงสร้างการวนซ้ำอีกหนึ่งครั้ง กระบวนการนี้จะดำเนินไปเรื่อย ๆ จนกว่าเงื่อนไขจะไม่เป็นจริงอีกต่อไป กล่าวคือ คาเรลจะหยุดเมื่อด้านหน้ามีกำแพง

../../_images/while-flow-chart.png

ภาพที่ 9 แผนภูมิการทำงาน (flowchart) ที่แสดงลำดับขั้นตอนการทำงานของโปรแกรม#

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

คำสั่งตรวจสอบ

คำสั่งที่ตรงข้าม

ตรวจสอบอะไร

front_is_clear()

front_is_blocked()

มีกำแพงอยู่ตรงหน้าคาเรลหรือไม่

beepers_present()

no_beepers_present()

มี กระดิ่งตรงที่คาเรลยืนอยู่หรือไม่

left_is_clear()

left_is_blocked()

มีกำแพงอยู่ทางซ้ายของคาเรลหรือไม่

right_is_clear()

right_is_blocked()

มีกำแพงอยู่ทางขวาของคาเรลหรือไม่

beepers_in_bag()

no_beepers_in_bag()

คาเรลมีกระดิ่งในกระเป๋าหรือไม่

facing_north()

not_facing_north()

คาเรลหันหน้าทางเหนือหรือไม่

facing_south()

not_facing_south()

คาเรลหันหน้าทางใต้หรือไม่

facing_east()

not_facing_east()

คาเรลหันหน้าทางตะวันออกหรือไม่

facing_west()

not_facing_west()

คาเรลหันหน้าทางตะวันตกหรือไม่

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

โดยสรุป โครงสร้างในการใช้คำสั่ง while มีดังนี้

while เงื่อนไขบูลีน:
    คำสั่งที่ต้องการให้วนซ้ำ
    คำสั่งที่ต้องการให้วนซ้ำ

การตั้งเงื่อนไขด้วย if else elif และตัวปฏิบัติการบูลีน#

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

ตัวอย่างเช่น หากเราต้องการให้คาเรลเดินไปข้างหน้า ถ้าด้านหน้าไม่มีสิ่งกีดขวาง เราสามารถเขียนฟังก์ชัน safe_move() เพื่อควบคุมการเดินของคาเรลได้ดังนี้

def safe_move():
    if front_is_clear():
        move()

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

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

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

../../_images/safe-move-diagram.png

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

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

../../_images/invert-beeper-world.png

ภาพที่ 10 (ซ้าย) จุดเริ่มต้นของคาเรล (ขวา) ผลลัพธ์ที่คาดหวังหลังจากรันโปรแกรมแล้ว#

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

def invert():
   if beepers_present(): # ถ้ามีกระดิ่ง
      pick_beeper()      # ให้เก็บกระดิ่ง
   else:                 # มิฉะนั้นแล้ว
      put_beeper()       # ให้วางกระดิ่งลงในช่องนั้น     

ในฟังก์ชันนี้ เราใช้โครงสร้าง if-else ซึ่งทำงานโดยการตรวจสอบเงื่อนไข if ก่อน หากเงื่อนไขเป็นจริง (มีกระดิ่งอยู่ในช่อง) โปรแกรมจะทำการเก็บกระดิ่งด้วยคำสั่ง pick_beeper() แต่หากเงื่อนไขไม่เป็นจริง (ไม่มีกระดิ่ง) โปรแกรมจะดำเนินการตามคำสั่งในส่วน else ซึ่งจะทำการวางกระดิ่งในช่องนั้นด้วยคำสั่ง put_beeper()

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

ขั้นตอนการทำงานของฟังก์ชัน invert() สามารถแสดงเป็นแผนภูมิการทำงานได้ดังนี้

../../_images/invert-beeper-flow-chart.png

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

def main():
    invert()

ตอนนี้เราแก้โจทย์นี้บนช่องแรกได้แล้ว จากนั้นหากเราเห็นว่าเราต้องรัน invert() ซ้ำ ๆ ในทุกช่องจนกว่าเราหน้าติดกำแพง ซึ่งแปลว่าเราต้องกำหนดเงื่อนไขที่ทำให้คาเรลรันโค้ดซ้ำ ๆ โดยใช้ while loop ดังนี้

def main():
    invert()
    while front_is_clear():
        move()
        invert()

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

ข้อแตกต่างระหว่าง if กับ while#

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

เราสามารถสรุปวิธีการใช้ if ได้ดังนี้

def main
    คำสั่ง 1
    if เงื่อนไข
        คำสั่ง 2
        คำสั่ง 3
    คำสั่ง 4

../../_images/if-vs-while-flow-chart.png

เราสามารถสรุปวิธีการใช้ while ได้ดังนี้ เพื่อเปรียบเทียบกันกับ if ซึ่งรันคำสั่งเพียงแค่ครั้งเดียว

def main
    คำสั่ง 1
    while เงื่อนไข
        คำสั่ง 2
        คำสั่ง 3
    คำสั่ง 4
../../_images/while-vs-if-flow-chart.png

บูลีน#

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

สมมติว่าเราต้องการให้คาเรลเอากระดิ่งที่อยู่รอบรั้วออกไปให้หมด ดังตัวอย่าง

../../_images/boolean-remove-fence.png

ภาพที่ 11 ซ้าย: จุดเริ่มต้น ขวา: จุดมุ่งหมาย#

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

def main():
    for i in range(4):
        remove_one_side()

จากนั้นเราสามารถคิดแบบนามธรรมไปได้ว่า รั้วจะยาวแค่ไหนก็ไม่สำคัญ สำคัญว่าเราเห็นว่ามีเงื่อนไขในการตรวจได้ว่าคาเรลยังไม่สุดรั้ว ซึ่งการตรวจหารั้วคือ การตรวจสอบว่าฝั่งซ้ายมีกำแพงหรือไม่โดยคำสั่ง left_is_blocked() หรือ อีกเงื่อนไขหนึ่งคือ ตรงที่ยืนอยู่มีกระดิ่งหรือไม่ด้วยโดยคำสั่ง beepers_present() ซึ่งการเชื่อมบูลีนสองตัวด้วย หรือ สามารถใช้ตัวปฏิบัติการ or ลงไปตรง ๆ แบบภาษาอังกฤษได้เลยดังนี้

def remove_one_side():
    while left_is_blocked() or beepers_present():
        if beepers_present():
            pick_beeper()
        move()

เพื่อทดสอบว่าตรรกะตามฟังก์ชันนั้นถูกต้องหรือไม่ เราอาจจะเขียนตารางออกมาเพื่อตรวจสอบอีกครั้ง

../../_images/or-operation.png

ภาพที่ 12 ตัวอย่างการใช้ตัวปฏิบัติการ or#

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

นอกจากนั้นแล้วภาษาไพทอนยังมีตัวปฏิบัติการ and ซึ่งมีการตีค่าบูลีนเหมือนกันกับ และ ในตรรกศาสตร์ ดังนี้

../../_images/and-operation.png

ภาพที่ 13 ตัวอย่างการใช้ and operator#

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

def main():
    for i in range(4):
        remove_one_side()
        turn_left()
        move()

def remove_one_side():
    while left_is_blocked() or beepers_present():
        if beepers_present():
            pick_beeper()
        move()

สรุป#

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

  1. การแบ่งปัญหาออกเป็นส่วนย่อย

  2. การสังเกตหาแบบแผน

  3. การคิดแบบนามธรรม

  4. การออกแบบอัลกอริทึม ซึ่งเป็นทักษะที่สำคัญในการเขียนโปรแกรมที่ดี

อ้างอิง#

[1]

Richard E Pattis. Karel the robot: a gentle introduction to the art of programming. John Wiley & Sons, 1994.