บทนี้นำเสนอกรณีศึกษาที่สาธิตขั้นตอนการออกแบบฟังก์ชันต่างๆ ที่ทำงานร่วมกัน
บทนี้แนะนำมอดูล turtle
ที่ทำให้เราสามารถสร้างรูปภาพโดยใช้กราฟิกเต่า (turtle graphics) มอดูล turtle
มากับการติดตั้งภาษาไพธอนส่วนมากอยู่แล้ว แต่ถ้าเราใช้ไพธอนของ PythonAnywhere เราจะไม่สามารถรันตัวอย่างในมอดูล turtle ได้ (อย่างน้อยก็ยังไม่ได้ในตอนที่ผมเขียนหนังสือเล่มนี้)
ถ้าเราลงไพธอนบนคอมพิวเตอร์ของเราแล้ว เราจะสามารถรันตัวอย่างในมอดูล turtle ได้ ไม่เช่นนั้น ตอนนี้ก็เป็น เวลาที่ดีนะที่จะติดตั้งไพธอน ผมได้โพสต์ขั้นตอนการติดตั้งไว้ที่ http://tinyurl.com/thinkpython2e
ตัวอย่างของโค้ดที่ใช้ในบทนี้ อยู่ที่ http://thinkpython2.com/code/polygon.py
หมายเหตุผู้แปล: ในบทนี้จะใช้คำว่า turtle แทนคำว่า เต่า เพราะเป็นชื่อเฉพาะ
ในการตรวจสอบว่าเรามีมอดูล turtle
ในเครื่องหรือเปล่า ให้เปิดตัวแปลภาษาไพธอนขึ้นมาแล้วพิมพ์
>>> import turtle >>> bob = turtle.Turtle()
เมื่อเรารันโค้ดนี้แล้ว ไพธอนควรจะสร้างหน้าต่างใหม่ขึ้นมาพร้อมกับลูกศรเล็กๆ ที่เป็นตัวแทนของ turtle ถ้าได้แล้วก็ปิดหน้าต่างได้
ให้สร้างไฟล์ชื่อว่า mypolygon.py
และพิมพ์โค้ดต่อไปนี้:
import turtle bob = turtle.Turtle() print(bob) turtle.mainloop()
มอดูล turtle
(เขียนด้วย ’t’ ตัวเล็ก) ได้เตรียมฟังก์ชันชื่อว่า Turtle
(เขียนด้วย ’T’ ตัวใหญ่) ซึ่งสร้างวัตถุ Turtle ที่เรากำหนดให้กับตัวแปรที่ชื่อว่า bob
การพิมพ์ bob
ออกมาจะแสดงผลประมาณนี้:
<turtle.Turtle object at 0xb7bfbf4c>
นี่หมายความว่า bob
อ้างอิงถึงวัตถุชนิด Turtle
อย่างที่ถูกนิยามในมอดูล turtle
นั่นเอง
ฟังก์ชัน mainloop
สั่งให้หน้าต่างที่เปิดขึ้นมารอให้ผู้ใช้ทำอะไรสักอย่าง แม้ว่าในกรณีนี้ มันไม่มีอะไร ให้ผู้ใช้ทำมากนัก ยกเว้นการปิดหน้าต่าง
เมื่อเราสร้างวัตถุชนิด Turtle แล้ว เราสามารถเรียก เมธอด (method) เพื่อจะเลื่อนมันไปรอบๆ หน้าต่างได้ เมธอดเป็นเหมือนฟังก์ชัน แต่มันใช้กฎวากยสัมพันธ์ในการเขียนที่ต่างไปนิดหน่อย เช่น เมื่อเราต้องการจะเลื่อนเต่าไปข้างหน้า:
bob.fd(100)
เมธอด fd
มีความเชื่อมโยงกับวัตถุ turtle ที่เรียกว่า bob
การเรียกใช้งานเมธอดเหมือนกับ การขอร้อง: เราขอให้ bob
เคลื่อนที่ไปข้างหน้า
อาร์กิวเมนต์ของเมธอด fd
คือ ระยะทางในหน่วยพิกเซล (pixel) ดังนั้น ขนาดของการเคลื่อนที่จะขึ้น อยู่กับจอของแต่ละคน
เมธอดอื่นที่เราสามารถเรียกใช้กับ Turtle ได้ คือ bk
เพื่อที่จะเคลื่อนไปข้างหลัง, lt
เพื่อเลี้ยวซ้าย, และ rt
เพื่อเลี้ยวขวา อาร์กิวเมนต์ของ lt
และ rt
เป็นขนาดของมุมในหน่วยองศา (degree)
นอกจากนี้ Turtle แต่ละตัวจะถือปากกาของมันเอง ซึ่งจะจรดลง (down) หรือ ยกขึ้น (up); ถ้า Turtle จรดปากกาลงมันจะทิ้งรอยขีดไว้เมื่อมันเคลื่อนที่ เมธอด pu
และ pd
เป็นตัวย่อของ “pen up’ (ยกปากกาขึ้น) และ “pen down” (จรดปากกาลง)
เพื่อที่จะวาดมุมที่ถูกต้อง ให้เพิ่มบรรทัดเหล่านี้เข้าไปในโปรแกรม หลังจากสร้าง bob
แล้ว แต่ใส่ก่อนที่จะเรียกฟังก์ชัน mainloop
:
bob.fd(100) bob.lt(90) bob.fd(100)
เมื่อเรารันโปรแกรมนี้ เราควรจะเห็น bob
เคลื่อนที่ไปทางทิศตะวันออก และจากนั้นไปทางทิศเหนือ โดยที่ทิ้งเส้นไว้สองส่วน
คราวนี้ ให้แก้ไขโปรแกรมเพื่อจะวาดรูปสี่เหลี่ยมจตุรัส อย่าเพิ่งทำอย่างอื่นต่อจนกว่าจะวาดได้!
เราอาจจะเขียนโปรแกรมเมื่อกี้ออกมาประมาณนี้:
bob.fd(100) bob.lt(90) bob.fd(100) bob.lt(90) bob.fd(100) bob.lt(90) bob.fd(100)
เราสามารถทำสิ่งที่เหมือนกันแต่กระชับกว่า โดยการใช้คำสั่ง for
ให้เพิ่มตัวอย่างนี้เข้าไปในไฟล์ mypolygon.py
แล้วลองรันดูอีกที
for i in range(4): print('Hello!')
แล้วเราควรจะเห็นผลประมาณนี้:
Hello! Hello! Hello! Hello!
นี่เป็นการใช้งานที่ง่ายที่สุดของคำสั่ง for
; เราจะเห็นตัวอย่างอีกมากในภายหลัง แต่ตัวอย่างนี้ก็น่าจะเพียงพอสำหรับการให้เราเขียนโปรแกรมวาดรูปสี่เหลี่ยมใหม่ อย่าเพิ่งทำอย่างอื่นต่อจนกว่าจะทำได้นะ
นี่เป็นคำสั่ง for
ที่วาดรูปสี่เหลี่ยมจตุรัส:
for i in range(4): bob.fd(100) bob.lt(90)
กฎวากยสัมพันธ์ของคำสั่ง for
นั้นเหมือนกับของฟังก์ชัน มันจะมีส่วนหัวที่ลงท้ายด้วยทวิภาค (:) และส่วนตัวที่ต้องย่อหน้าเข้าไป ส่วนตัวของ for สามารถมีคำสั่งอยู่ภายในได้กี่คำสั่งก็ได้
คำสั่ง for
นี้สามารถเรียกว่า ลูป (loop) ได้ด้วย เนื่องจากกระแสการดำเนินการ (flow of execution) ทำงานผ่านไปตามส่วนตัวของคำสั่ง จากนั้นมันจะวนกลับไปทำคำสั่งบนสุดของคำสั่ง for ในกรณีนี้ โปรแกรมจะรันคำสั่งในส่วนตัวเป็นจำนวน 4 ครั้ง
โค้ดเวอร์ชันนี้แตกต่างจากโค้ดวาดรูปสี่เหลี่ยมจตุรัสอันที่แล้วนิดหน่อย เพราะว่ามันหันข้างหลังจาก วาดเส้นสุดท้ายของสี่เหลี่ยมจตุรัสแล้ว การหันข้างที่เพิ่มขึ้นมานี้ใช้เวลามากขึ้น แต่ว่ามันทำให้โค้ดง่ายขึ้นถ้าเราจะทำ สิ่งที่เหมือนๆ กันทุกครั้งที่ทำลูป โค้ดเวอร์ชันนี้ยังทำให้ turtle มันกลับไปที่จุดเริ่มต้น และหันหน้ากลับไปในทางเดียวกับตอนเริ่มต้นด้วย
ต่อไปนี้จะเป็นชุดแบบฝึกหัดที่ใช้ TurtleWorld มันน่าจะสนุกแต่ก็มีประเด็นความรู้ด้วย ในระหว่างที่เราทำแบบฝึกหัดนี้ ให้คิดว่าประเด็นความรู้ที่ได้คืออะไร
หัวข้อต่อไปนี้มีเฉลยของแบบฝึกหัดด้วย ดังนั้น ห้ามไปดูเฉลยจนกว่าเราจะทำเสร็จ (หรืออย่างน้อยก็พยายามทำก่อน)
square
ซึ่งรับพารามิเตอร์ชื่อว่า t
ที่เป็น turtle ฟังก์ชันนี้ควรจะใช้ turtle ในการวาดสี่เหลี่ยมจัตุรัส
ให้เขียนการเรียกฟังก์ชันที่ผ่านค่า bob
เข้าไปเป็นอาร์กิวเมนต์สำหรับฟังก์ชัน square
จากนั้นให้รันโปรแกรมอีกทีหนึ่ง
length
เข้าไปยัง square
ทำการแก้ไขส่วนตัวของฟังก์ชันโดยให้ความยาวของด้านคือ length
จากนั้นให้แก้ไขการเรียกฟังก์ชัน โดยให้ผ่านอาร์กิวเมนต์ตัวที่สองเข้าไปด้วย รันโปรแกรมอีกครั้งหนึ่ง ทดสอบโปรแกรมกับค่า length
ในช่วงค่าต่างๆsquare
แล้วเปลี่ยนชื่อเป็น polygon
เพิ่มพารามิเตอร์อีกตัวชื่อว่า n
ทำการแก้ไขส่วนตัวของฟังก์ชันให้วาดรูป n เหลี่ยมด้านเท่า (รูปหลายเหลี่ยมด้านเท่าที่มี n ด้าน) คำใบ้: มุมภายนอกของรูป n เหลี่ยมด้านเท่า คือ $360/n$ องศา
circle
ซึ่งรับ turtle t
, และรัศมี r
เป็นพารามิเตอร์ และวาดวงกลมกลายๆ โดยการเรียกฟังก์ชัน polygon
ด้วยความยาวและจำนวนด้านที่เหมาะสม ทดสอบฟังก์ชันของเราด้วยช่วงค่า r
ต่างๆ
คำใบ้: ลองหาเส้นรอบวง (circumference) ของวงกลม และทำให้ length * n = circumference
circle
ที่จะสามารถใช้ครอบคลุมกรณีอื่นๆ ได้ โดยให้ชื่อว่า arc
ซึ่งรับพารามิเตอร์อีกตัวหนึ่ง คือ angle
ซึ่งกำหนดสัดส่วนของวงกลมที่เราจะวาด angle
มีหน่วยเป็นองศา (degree) ดังนั้น เมื่อ angle=360
ฟังก์ชัน arc
ควรจะวาดวงกลมที่สมบูรณ์แบบฝึกหัดแรกให้เรานำโค้ดวาดรูปสี่เหลี่ยมจัตุรัสมาใส่ในนิยามฟังก์ชัน และจากนั้นเรียกฟังก์ชัน โดยผ่าน turtle เป็นพารามิเตอร์ นี่คือเฉลย:
def square(t): for i in range(4): t.fd(100) t.lt(90) square(bob)
คำสั่งชุดข้างในสุด (ย่อหน้าในสุด), fd
และ lt
, ถูกย่อหน้าเข้าไปสองรอบ เพื่อแสดงให้เห็นว่าพวกมันอยู่ใน for
ลูป ซึ่งอยู่ในนิยายของฟังก์ชันอีกที บรรทัดถัดมา, square(bob)
, ถูกล้างขอบซ้าย (เว้นบรรทัดและกลับเข้าไปชิดขอบซ้ายสุด) ซึ่งระบุถึงการสิ้นสุดของทั้งลูป for
และนิยามฟังก์ชัน
ในฟังก์ชันนี้ ตัวแปร t
อ้างอิงถึง turtle ตัวเดียวกับ bob
ดังนั้น คำสั่ง t.lt(90)
จะให้ผลเหมือนกับคำสั่ง bob.lt(90)
ในกรณีนี้ แล้วทำไมเราไม่เรียกพารามิเตอร์ว่า bob
ล่ะ? หลักการคือ t
สามารถเป็น turtle ตัวไหนก็ได้ ไม่เฉพาะแค่ bob
เท่านั้น ดังนั้น เราสามารถ สร้าง turtle ตัวที่สอง และผ่านมันเป็นอาร์กิวเมนต์เข้าไปยังฟังก์ชัน square
ได้:
alice = turtle.Turtle() square(alice)
การห่อชิ้นส่วนของโค้ดในฟังก์ชันนั้นเรียกว่า การห่อหุ้ม (encapsulation) ประโยชน์ของการห่อหุ้มอย่างหนึ่ง คือ มันจะผูกโค้ดเหล่านั้นไว้กับชื่อชื่อหนึ่ง ซึ่งทำหน้าที่คล้ายการบันทึกเอกสารด้วย ประโยชน์อีกอย่าง คือ การที่เราสามารถใช้โค้ดนี้ซ้ำได้ มันกระชับมากกว่าเมื่อเราเรียกฟังก์ชันซ้ำสองครั้ง แทนที่จะ คัดลอกโค้ดและเอามาวาง (copy and paste)
ขั้นตอนถัดไป คือ การเพิ่มพารามิเตอร์ length
ให้กับฟังก์ชัน square
นี่คือเฉลย:
def square(t, length): for i in range(4): t.fd(length) t.lt(90) square(bob, 100)
การเพิ่มพารามิเตอร์ให้ฟังก์ชันนั้นเรียกว่า การทำให้ครอบคลุม (generalization) เพราะว่ามันทำให้ฟังก์ชันนั้นทำงานแบบครอบคลุมกรณีทั่วไปมากยิ่งขึ้น: ในเวอร์ชันก่อนหน้า สี่เหลี่ยมจตุรัสนั้นมีขนาดเดิมเสมอ; แต่ในเวอร์ชันนี้ มันสามารถเป็นขนาดอะไรก็ได้
ขั้นตอนถัดไปก็เป็นการทำให้ครอบคลุมเช่นกัน แทนที่เราจะวาดสี่เหลี่ยมจตุรัส ฟังก์ชัน polygon
วาดรูปหลายเหลี่ยมด้านเท่า โดยมีจำนวนด้านเท่าไหร่ก็ได้ นี่คือเฉลย:
def polygon(t, n, length): angle = 360 / n for i in range(n): t.fd(length) t.lt(angle) polygon(bob, 7, 70)
ตัวอย่างนี้วาดรูป 7 เหลี่ยมที่มีด้านยาว 70 (หมายเหตุผู้แปล: หน่วยเป็นพิกเซล)
ถ้าเราใช้ไพธอน 2 ค่าของ angle
อาจจะเพี้ยนๆ หน่อย เพราะเป็นการหารของจำนวนเต็ม ทางแก้ง่ายๆ คือการคำนวณเป็น angle = 360.0 / n
เพราะว่าตัวเศษเป็นเลขจุดลอย ค่าผลลัพธ์ที่ได้ก็จะเป็นค่าจุดลอย
(หมายเหตุผู้แปล: ในการทำ integer division นั้นจำนวนเต็มหารจำนวนเต็มจะได้ค่าผลลัพธ์เป็นจำนวนเต็ม)
เมื่อฟังก์ชันมีจำนวนอาร์กิวเมนต์มากกว่า 2-3 ตัว มันง่ายที่จะลืมว่าแต่ละตัวคืออะไร หรือลำดับในการวางควรเป็นอย่างไร ในกรณีดังกล่าว มันเป็นความคิดที่ดีที่จะใส่ชื่อ ของพารามิเตอร์ในลิสต์ของอาร์กิวเมนต์:
polygon(bob, n=7, length=70)
อาร์กิวเมนต์แบบนี้เรียกว่า อาร์กิวเมนต์คำสำคัญ (keyword arguments) เพราะว่ามันใส่ชื่อ ของพารามิเตอร์ลงไปเป็น “คำสำคัญ (keywords)” (อย่าสับสนกับคำสำคัญของไพธอน เช่น while
และ def
)
กฎวากยสัมพันธ์นี้ทำให้เราสามารถอ่านโปรแกรมได้ง่ายขึ้น มันยังเตือนให้เรารู้ว่าอาร์กิวเมนต์และ พารามิเตอร์ทำงานอย่างไร: เมื่อเราเรียกฟังก์ชัน อาร์กิวเมนต์จะถูกกำหนดค่าให้กับพารามิเตอร์
ขั้นต่อไปคือการเขียนฟังก์ชัน circle
ซึ่งรับรัศมี (radius) r
เป็นพารามิเตอร์ นี่คือเฉลยง่ายๆ ที่ใช้ฟังก์ชัน polygon
ในการวาดรูป 50 เหลี่ยมด้านเท่า:
import math def circle(t, r): circumference = 2 * math.pi * r n = 50 length = circumference / n polygon(t, n, length)
บรรทัดแรกคำนวณเส้นรอบวงของวงกลมที่มีรัศมี r
โดยใช้สูตร $2 \pi r$ เนื่องจากเราใช้ math.pi
เราต้องนำเข้ามอดูล math
ด้วย โดยปกติแล้ว คำสั่ง import
จะอยู่ในตอนเริ่มต้นของไฟล์สคริปต์
n
คือ จำนวนเส้นในการวาดวงกลมโดยประมาณของเรา และ length
คือ ความยาวของแต่ละเส้นนั้น ดังนั้น ฟังก์ชัน polygon
วาดรูปหลายเหลี่ยมจำนวน 50 เหลี่ยม เพื่อจะประมาณการเป็นวงกลมที่มีรัศมี r
ข้อจำกัดของเฉลยแบบนี้คือ n
เป็นค่าคงที่ หมายถึงว่าสำหรับวงกลมอันใหญ่ๆ แล้ว เส้นที่วาดนี้มันจะยาวเกินไป และสำหรับวงกลมเล็กๆ นี้ เราจะเสียเวลาวาดเส้นน้อยๆ หลายๆ เส้น ทางแก้ทางหนึ่งคือการทำฟังก์ชันให้ครอบคลุม (generalize) โดยการเอา n
มาเป็นพารามิเตอร์ มันจะทำให้ผู้ใช้ (หรือใครก็ตามที่เรียกฟังก์ชัน circle
) สามารถควบคุม การใช้งานได้ดีขึ้น แต่ส่วนต่อประสานงาน (interface) จะเรียบร้อยน้อยลง
ส่วนต่อประสานงาน (interface) ของฟังก์ชัน คือ การสรุปว่าฟังก์ชันมันใช้งานอย่างไร: พารามิเตอร์คืออะไร? ฟังก์ชันทำงานอะไรบ้าง? และค่าที่ถูกส่งคืนกลับไปคืออะไร? ส่วนต่อประสานงานจะดู “เรียบร้อย” ถ้ามันให้ผู้เรียกสามารถใช้งานที่ต้องการได้โดยไม่ต้องมายุ่งกับรายละเอียดที่ไม่จำเป็น
ในตัวอย่างนี้ r
เป็นส่วนหนึ่งของส่วนต่อประสานงาน เพราะมันระบุคุณลักษณะของวงกลมที่จะต้องวาด ส่วน n
ไม่ค่อยเหมาะเท่าไหร่เพราะมันเกี่ยวกับรายละเอียดของการที่จะแสดงผลว่าวงกลมจะเป็น อย่างไร
แทนที่จะทำให้ส่วนต่อประสานงานดูรก มันดีกว่าจะเลือกค่าที่เหมาะสมของ n
โดยขึ้นอยู่กับค่าของ circumference
def circle(t, r): circumference = 2 * math.pi * r n = int(circumference / 3) + 3 length = circumference / n polygon(t, n, length)
คราวนี้ จำนวนของเส้น (segment) คือ จำนวนเต็มที่ใกล้เคียงกับ circumference/3
ทำให้ความยาวของเส้นมีค่าประมาณ 3 ซึ่งก็เล็กเพียงพอที่จะให้วงกลมนั้นดูดี แต่ใหญ่พอที่จะมีประสิทธิภาพ และเป็นที่ยอมรับได้สำหรับวงกลมขนาดใดๆ
การเพิ่ม 3 ให้ n
เป็นการรับรองว่ารูปหลายเหลี่ยมนี้จะมีอย่างน้อย 3 ด้าน
ตอนที่ผมเขียนฟังก์ชัน circle
ผมสามารถนำฟังก์ชัน polygon
กลับมาใช้ได้ เนื่องจากรูปหลายเหลี่ยมนั้นเป็นการร่างรูปวงกลมได้ดี แต่ถ้านำมาใช้ในฟังก์ชัน arc
มันไม่ค่อย ให้ความร่วมมือเท่าไร; เราไม่สามารถใช้ polygon
หรือ circle
มาวาดเส้นโค้งได้
ทางเลือกหนึ่งคือเริ่มด้วยสำเนาของฟังก์ชัน polygon
และแปลงร่างให้มันเป็นฟังก์ชัน arc
ผลที่ได้จะหน้าตาเป็นประมาณนี้:
def arc(t, r, angle): arc_length = 2 * math.pi * r * angle / 360 n = int(arc_length / 3) + 1 step_length = arc_length / n step_angle = angle / n for i in range(n): t.fd(step_length) t.lt(step_angle)
ครึ่งที่สองของฟังก์ชันนี้ดูเหมือนกับฟังก์ชัน polygon
แต่เราไม่สามารถนำ polygon
มาใช้ได้โดยไม่เปลี่ยนส่วนต่อประสานงาน เราอาจจะทำ polygon
ให้ครอบคลุม โดยการรับขนาด ของมุมมาเป็นอาร์กิวเมนต์ตัวที่สาม แต่ polygon
มันจะไม่ใช่ชื่อที่เหมาะสมแล้ว! ถ้าอย่างนั้น เราลองเรียกมันแบบกว้างๆ ว่าเป็นฟังก์ชัน polyline
ก็แล้วกัน:
def polyline(t, n, length, angle): for i in range(n): t.fd(length) t.lt(angle)
คราวนี้ เราสามารถเขียนฟังก์ชัน polygon
และ ฟังก์ชัน arc
ใหม่โดยใช้ polyline
:
def polygon(t, n, length): angle = 360.0 / n polyline(t, n, length, angle) def arc(t, r, angle): arc_length = 2 * math.pi * r * angle / 360 n = int(arc_length / 3) + 1 step_length = arc_length / n step_angle = float(angle) / n polyline(t, n, step_length, step_angle)
ในที่สุด เราก็สามารถเขียน circle
ใหม่โดยใช้ arc
:
def circle(t, r): arc(t, r, 360)
กระบวนการเช่นนี้—การเรียงโปรแกรมใหม่เพื่อพัฒนาส่วนต่อประสานงานและอำนวยความสะดวกให้โค้ด ถูกใช้งานซ้ำๆ ได้ เรียกว่า การปรับโครงสร้าง (refactoring) ในกรณีนี้ เราสังเกตว่า มีโค้ดที่เหมือนกันในฟังก์ชัน arc
และ polygon
ดังนั้นเราจึง “แยกส่วน” มันออกมา ใส่ในฟังก์ชัน polyline
ถ้าเราวางแผนล่วงหน้าสักหน่อย เราอาจจะเขียน polyline
ก่อน และหลีกเลี่ยงการปรับโครงสร้าง แต่บ่อยครั้งที่เราก็ไม่มีข้อมูลเพียงพอในตอนเริ่มต้นของโปรเจ็กต์ ที่จะออกแบบส่วนต่อประสานงานทุกอย่าง เมื่อเราเริ่มต้นเขียนโค้ดแล้ว เราจะเข้าใจปัญหาต่างๆ ดีขึ้น ในบางครั้งการปรับโครงสร้างเป็นสัญญาณบอกว่า เราก็ได้เรียนรู้อะไรบ้างล่ะนะ
แผนการพัฒนา (development plan) เป็นกระบวนการเขียนโปรแกรมอย่างหนึ่ง กระบวนการที่เราใช้ใน กรณีศึกษานี้ คือ “การห่อหุ้มและการทำให้ครอบคลุม (encapsulation and generalization)” ขั้นตอนของกระบวนการนี้คือ:
กระบวนการข้างต้นนี้มีข้อเสียบางอย่าง—เราจะเห็นทางเลือกอื่นทีหลัง—แต่มันก็เป็นประโยชน์ถ้าเราไม่รู้ล่วงหน้า ว่าจะแบ่งโปรแกรมเป็นฟังก์ชันต่างๆ ได้อย่างไร กระบวนการแบบนี้ทำให้เราออกแบบโปรแกรมควบคู่ไปกับการเขียนได้
A ด็อกสตริง (docstring) คือ สายอักขระที่อยู่ตอนเริ่มต้นของฟังก์ชัน ซึ่งอธิบายส่วนต่อประสานงานของฟังก์ชัน (“doc” เป็นคำเรียกสั้นๆ ของ “documentation” หรือเอกสาร) นี่คือตัวอย่าง:
def polyline(t, n, length, angle): """Draws n line segments with the given length and angle (in degrees) between them. t is a turtle. """ for i in range(n): t.fd(length) t.lt(angle)
โดยปกตินิยมแล้ว ด็อกสตริงทุกอันจะเป็นสายอักขระที่อยู่ในอัญประกาศสามอัน (triple quotes) ซึ่งรู้จักกันในนาม สายอักขระหลายบรรทัด เพราะว่าอัญประกาศสามอันจะทำให้สายอักขระสามารถขยายความยาวได้มากกว่าหนึ่งบรรทัด
(หมายเหตุผู้แปล: string แปลเป็นภาษาไทยว่า สายอักขระ แต่เพื่อไม่ให้สับสนมาก เราคิดว่ามันเป็นข้อมูลที่เป็น ข้อความที่ประกอบด้วยคำอาจจะหลายคำก็พอได้)
ด็อกสตริงเป็นสิ่งที่สั้น แต่มันก็มีข้อมูลที่สำคัญที่บางคนจำเป็นต้องรู้ในการใช้ฟังก์ชัน มันอธิบายแบบกระชับ ว่าฟังก์ชันทำอะไร (โดยที่เราไม่ต้องรู้รายละเอียดว่ามันทำอย่างไร) มันยังอธิบายว่าพารามิเตอร์แต่ละตัวมีผล กับฟังก์ชันอย่างไร และชนิดของพารามิเตอร์แต่ละตัวควรจะเป็นชนิดอะไร (ถ้ามันไม่ชัดเจนอยู่แล้ว)
การเขียนเอกสารแบบนี้เป็นส่วนสำคัญของการออกแบบส่วนต่อประสานงาน ส่วนต่อประสานงานที่ถูกออกแบบมาอย่างดี ควรจะง่ายต่อการอธิบาย; ถ้าเรามีความลำบากในการอธิบายฟังก์ชันหนึ่งๆ ของเราแล้ว บางทีส่วนต่อประสานงานนั้น อาจจะต้องได้รับการปรับปรุง
ส่วนต่อประสานงานเหมือนกับสัญญาระหว่างฟังก์ชันกับตัวที่เรียกฟังก์ชัน ตัวเรียกฟังก์ชันตกลงที่จะผ่านค่าชนิดที่ ตกลงกันไว้อย่างแน่นอนเข้ามาเป็นพารามิเตอร์ และฟังก์ชันก็ตกลงที่จะทำงานบางอย่างที่ตกลงกันไว้อย่างแน่นอนเช่นกัน
ตัวอย่างเช่น ฟังก์ชัน polyline
ต้องการใช้อาร์กิวเมนต์ 4 ตัว: t
จะต้องเป็นชนิด Turtle; n
จะต้องเป็นจำนวนเต็ม; length
จะต้องเป็นค่าบวก; และ angle
จะต้องเป็นเลขใดๆ ที่หน่วยเป็นองศา (degree)
สิ่งที่ฟังก์ชันต้องการเหล่านี้เรียกว่า เงื่อนไขเบื้องต้น (preconditions) เพราะว่าพวกมันจะต้องเป็นจริงก่อนที่ ฟังก์ชันจะเริ่มทำงาน ในทางกลับกัน เงื่อนไขต่างๆ ในตอนจบของฟังก์ชันจะต้องเป็น เงื่อนไขลงท้าย (postconditions) เงื่อนไขลงท้ายรวมไปถึง ผลการทำงานของฟังก์ชัน (เช่น การวาดรูปเส้นต่างๆ) และผลข้างเคียงต่างๆ (เช่น การเคลื่อนที่ของ Turtle และการเปลี่ยนแปลงค่าอย่างอื่น)
เงื่อนไขเบื้องต้น (preconditions) อยู่ในความรับผิดชอบของตัวเรียก (caller) ถ้าตัวเรียกไม่สามารถทำตามเงื่อนไขเบื้องต้น (ที่ถูกอธิบายไว้อย่างเหมาะสม!) ได้ และทำให้ฟังก์ชันทำงานไม่ได้อย่างถูกต้อง ความผิดพลาดของโปรแกรมจะอยู่ที่ตัวเรียก ไม่ใช่ที่ฟังก์ชัน
ถ้าเงื่อนไขเบื้องต้นนั้นถูกต้องแล้ว และเงื่อนไขลงท้ายไม่ถูกต้อง ความผิดพลาดนั้นจะอยู่ในฟังก์ชัน ถ้าเงื่อนไขทั้งเบื้องต้นและลงท้ายนั้นชัดเจน มันจะช่วยในการดีบักโปรแกรม
แบบฝึกหัด 1
ดาวน์โหลดโค้ดที่ใช้ในบทนี้ได้จาก http://thinkpython2.com/code/polygon.py
circle(bob, radius)
เราสามารถคำนวณโดยมือ หรือเพิ่มคำสั่ง print
ในโค้ดก็ได้arc
ในหัวข้อที่ 4.7 ไม่ค่อยถูกเท่าไหร่ เพราะการประมาณการ เชิงเส้นของวงกลมนั้นจะทำให้ประมาณไปเกินขอบนอกของวงกลมจริงๆ เสมอ นั่นส่งผลให้ Turtle ไปจบที่ 2-3 พิกเซล เลยไปจากจุดหมายปลายทางที่ถูกต้อง เฉลยของผมได้แสดงให้เห็นถึงวิธีลดผลกระทบของความผิดพลาดนี้ ให้อ่านโค้ด และดูว่ามันดูเข้าท่าหรือไม่ ถ้าเราเขียนแผนภาพดู เราอาจจะเห็นว่ามันทำงานอย่างไร
แบบฝึกหัด 2
ให้เขียนชุดของฟังก์ชันต่างๆ ให้ครอบคลุมการวาดดอกไม้ในรูปที่ 4.1
เฉลย: http://thinkpython2.com/code/flower.py, ไฟล์ที่จำเป็นต้องใช้: http://thinkpython2.com/code/polygon.py
แบบฝึกหัด 3
ให้เขียนชุดของฟังก์ชันให้ครอบคลุมการวาดรูปทรงในรูปที่ 4.2
เฉลย: http://thinkpython2.com/code/pie.py.
แบบฝึกหัด 4
ตัวอักษรของพยัญชนะสามารถสร้างได้จากส่วนประกอบเล็กๆ เช่น เส้นตั้ง เส้นนอน และเส้นโค้งจำนวนไม่กี่เส้น ให้ออกแบบพยัญชนะที่สามารถวาดจากส่วนประกอบเล็กๆ เหล่านี้ในจำนวนที่น้อยที่สุด จากนั้นให้เขียนฟังก์ชันที่วาดอักษรเหล่านี้
เราควรจะเขียนหนึ่งฟังก์ชันสำหรับอักษรแต่ละตัว แล้วตั้งชื่อ เช่น draw_a
, draw_b
, และอื่นๆ และให้ใส่ฟังก์ชันเหล่านี้ลงในไฟล์ชื่อ letters.py
เราสามารถดาวน์โหลด “โปรแกรมพิมพ์ดีด turtle (turtle typewriter)” จาก http://thinkpython2.com/code/typewriter.py เพื่อที่จะทดสอบ โค้ดของเรา
เราสามารถดูเฉลยได้จาก http://thinkpython2.com/code/letters.py; ต้องการไฟล์ http://thinkpython2.com/code/polygon.py ในการรัน
แบบฝึกหัด 5
อ่านเกี่ยวกับเกลียว (spiral) ได้ที่ http://en.wikipedia.org/wiki/Spiral
จากนั้นเขียนโปรแกรมที่วาดรูปเกลียว Archimedian (หรือเกลียวแบบอื่นๆ)
เฉลยอยู่ที่ http://thinkpython2.com/code/spiral.py
https://greenteapress.com/thinkpython2/html/thinkpython2005.html