ตอนนี้เราได้รู้วิธีการสร้างชนิดข้อมูลใหม่ ขั้นต่อไป เป็นการเขียนฟังก์ชัน ที่รับออบเจ๊คต์ชนิดผู้เขียนโปรแกรม กำหนดขึ้นเป็นพารามิเตอร์ และให้ค่าออกมาเป็นออบเจ๊คต์เหล่านั้น ในบทนี้ได้นำเสนอ “การเขียนโปรแกรมเชิงฟังก์ชัน (functional programming style)” และ แผนการพัฒนาสองโปรแกรมใหม่
ตัวอย่างโปรแกรมของบทนี้สามารถดาวน์โหลดได้ที่ http://thinkpython2.com/code/Time1.py เฉลยสำหรับแบบฝึกหัดอยู่ที่ http://thinkpython2.com/code/Time1_soln.py
เช่นกันกับตัวอย่างอื่นของชนิดข้อมูลที่ผู้เขียนโปรแกรมกำหนดเอง เราจะประกาศคลาส Time
ซึ่งใช้บันทึกเวลาของวัน นิยามของคลาสมีลักษณะดังนี้
class Time: """Represents the time of day. attributes: hour, minute, second """
เราสามารถสร้างออบเจ๊คต์ใหม่ของ Time
และกำหนดค่าของแอตทริบิวต์ชั่วโมง (hour) นาที (minute) และ วินาที (second)
time = Time() time.hour = 11 time.minute = 59 time.second = 30
แผนภาพสถานะของออบเจ๊คต์ Time
มีลักษณะดังแสดงในรูปที่ 16.1
เพื่อเป็นการฝึก ให้เขียนฟังก์ชันชื่อ print_time
ซึ่งรับออบเจ๊คต์ Time และพิมพ์ค่าเวลาในรูปแบบ ชั่วโมง:นาที:วินาที
ข้อแนะนำคือ ใช้รูปแบบ '%.2d'
สำหรับพิมพ์ตัวเลขอย่างน้อยสองตำแหน่ง โดยจะเติมศูนย์ด้านหน้ากรณีค่าน้อยกว่าสิบ
เขียนฟังก์ชันบูลีนชื่อ is_after
ซึ่งรับออบเจ๊คต์ของ Time สองออบเจ๊คต์ t1
และ t2
แล้วให้ค่าออกมาเป็น True
ถ้าเวลา t1
ตามหลังเวลา t2
ตามลำดับเหตุการณ์ ไม่เช่นนั้นจะให้ค่าออกมาเป็น False
ท้าทาย: ลองเขียนฟังก์ชันโดยไม่ใช้คำสั่ง if
ในหัวข้อถัดๆ ไป เราจะเขียนสองฟังก์ชันสำหรับบวกเวลาซึ่งแสดงรูปแบบการเขียนฟังก์ชันไว้สองแบบคือ แบบฟังก์ชันบริสุทธิ์ (pure function) และ แบบตัวดัดแปลง (modifier) และได้แสดงแผนการพัฒนาที่เรียกว่า วิธี ต้นแบบและเติมแต่ง (prototype and patch) ซึ่งเป็นวิธีการแก้ปัญหาที่ซับซ้อนโดยเริ่มจากต้นแบบอย่างง่าย และเพิ่มเติมการจัดการกับความซับซ้อนอย่างค่อยเป็นค่อยไป
นี่เป็นต้นแบบอย่างง่ายของฟังก์ชัน add_time
def add_time(t1, t2): sum = Time() sum.hour = t1.hour + t2.hour sum.minute = t1.minute + t2.minute sum.second = t1.second + t2.second return sum
ภายในฟังก์ชันมีการสร้างออบเจ๊คต์ Time
ขึ้นใหม่ และกำหนดค่าตั้งต้นให้กับแอตทริบิวต์แล้วให้ค่าออกมาเป็นอ้างอิงไปยังออบเจ๊คต์ใหม่นั้น การทำงานเช่นนี้เรียกว่า ฟังก์ชันบริสุทธิ์ เนื่องจากไม่มีการแก้ไขค่าของออบเจ๊คต์ที่ถูกส่งเป็นอาร์กูเมนต์และไม่ได้รับผลกระทบใดเว้นแต่ค่าที่ให้ออกมา มีลักษณะเดียวกับฟังก์ชันสำหรับแสดงค่าหรือฟังก์ชันสำหรับรับอินพุตจากผู้ใช้
เพื่อการทดสอบฟังก์ชันจึงสร้างออบเจ๊คต์ของ Time ขึ้นมาสองออบเจ๊คต์ ออบเจ๊คต์ start
ใช้เก็บค่าเวลาเริ่มต้นของภาพยนต์ เช่นเรื่อง ไพธอนยักษ์กับจอกศักดิ์สิทธิ์ (Monty Python and the Holy Grail) และออบเจ๊คต์ duration
สำหรับเก็บค่าเวลาที่ใช้ในการฉายภาพยนต์ ซึ่งจะเป็น 1 ชั่วโมง 35 นาที
ฟังก์ชัน add_time
จะหาว่าภาพยนต์ถูกฉายจบตอนไหน
>>> start = Time() >>> start.hour = 9 >>> start.minute = 45 >>> start.second = 0 >>> duration = Time() >>> duration.hour = 1 >>> duration.minute = 35 >>> duration.second = 0 >>> done = add_time(start, duration) >>> print_time(done) 10:80:00
ผลลัพธ์คือ 10:80:00
อาจจะไม่ใช่อย่างที่เราหวังไว้ ปัญหาคือ ฟังก์ชันไม่ได้จัดการกับกรณีจำนวนเวลาวินาทีและนาทีที่มีผลรวมเกิน 60 เมื่อเกิดเหตุการณ์ดังกล่าวเราจะต้อง “ทด” เวลาส่วนเกินของวินาทีไปที่นาที และยกยอดนาทีส่วนเกินไปเพิ่มให้กับชั่วโมง
นี่คือฟังก์ชันรุ่นที่ได้รับการปรับปรุง
def add_time(t1, t2): sum = Time() sum.hour = t1.hour + t2.hour sum.minute = t1.minute + t2.minute sum.second = t1.second + t2.second if sum.second >= 60: sum.second -= 60 sum.minute += 1 if sum.minute >= 60: sum.minute -= 60 sum.hour += 1 return sum
แม้ว่าฟังก์ชันนี้จะถูกต้องแล้ว แต่ก็ค่อนข้างใหญ่ เราจะมาดูเวอร์ชันที่สั้นกว่านี้ในภายหลัง
ในบางครั้งก็ถือเป็นประโยชน์มากที่จะให้ฟังก์ชันแก้ไขค่าออบเจ๊คต์ที่รับมาเป็นพารามิเตอร์ ในกรณีนั้นการเปลี่ยนแปลงสามารถรับรู้ได้ถึงตำแหน่งที่ฟังก์ชันถูกเรียกใช้ ฟังก์ชันที่ทำงานลักษณะนี้เรียกว่า ตัวดัดแปลง (modifiers)
ฟังก์ชัน increment
ทำการบวกเพิ่มตัวเลขจำนวนวินาทีให้กับออบเจ๊คต์ Time
สามารถเขียนเป็นตัวดัดแปลงได้อย่างเป็นธรรมชาติ และนี่เป็นร่างคร่าวๆ
def increment(time, seconds): time.second += seconds if time.second >= 60: time.second -= 60 time.minute += 1 if time.minute >= 60: time.minute -= 60 time.hour += 1
บรรทัดแรกทำหน้าที่บวกเพิ่มวินาที ส่วนที่เหลือเป็นการจัดการกับกรณีพิเศษที่เราเจอก่อนหน้านี้
ฟังก์ชันนี้ทำงานได้ถูกต้องหรือไม่? อะไรจะเกิดขึ้นถ้าค่าของ วินาที
มีค่ามากกว่า 60 มากๆ?
ในกรณีนั้น การทดค่าเพียงแค่ครั้งเดียวจะยังไม่เพียงพอ เราจะต้องทำซ้ำจนกว่าค่าของ time.second
จะน้อยกว่า 60 แนวทางหนึ่งที่ทำได้คือ แทนที่คำสั่ง if
ด้วยคำสั่ง while
จะทำให้การทำงานของฟังก์ชันถูกต้องแต่ก็ไม่ค่อยมีประสิทธิภาพนัก ทำเป็นแบบฝึกหัด คือ เขียนฟังก์ชัน increment
เวอร์ชันที่ถูกต้องแต่ไม่มีการทำงานแบบวนซ้ำ
การทำงานทุกอย่างที่สามารถทำได้ด้วยตัวดัดแปลงจะสามารถทำด้วยฟังก์ชันบริสุทธิ์ได้เช่นกัน อันที่จริงภาษาเขียนโปรแกรมบางภาษาอนุญาตให้เขียนเฉพาะฟังก์ชันบริสุทธิ์ เนื่องจากมีความเชื่อว่า โปรแกรมที่ใช้เฉพาะฟังก์ชันบริสุทธิ์พัฒนาได้เร็วกว่าและมีข้อผิดพลาดน้อยกว่าโปรแกรที่ใช้ตัวดัดแปลง แต่หลายครั้งที่การใช้ตัวดัดแปลงมีความสะดวกสบายกว่า (และการใช้ฟังก์ชันมีแนวโน้มของประสิทธิภาพด้อยกว่า)
โดยทั่วไป ผมแนะนำให้คุณเขียนฟังก์ชันบริสุทธิ์เมื่อใดก็ตามที่สมเหตุสมผล และใช้ตัวดัดแปลงเฉพาะเมื่อมีข้อได้เปรียบที่น่าสนใจเท่านั้น แนวทางนี้อาจเรียกว่ารูปแบบ การเขียนโปรแกรมเชิงฟังก์ชัน (functional programming style)
เพื่อเป็นการฝึก ให้เขียนฟังก์ชัน increment
ที่เป็นรุ่น “บริสุทธิ์ (pure)” ซึ่งจะสร้างออบเจ๊คต์ Time ขึ้นมาใหม่และให้ค่าคืนกลับเป็นออบเจ๊คต์นั้นแทนการไก้ไขค่าของพารามิเตอร์
แผนการพัฒนาที่ผ่านมาถูกเรียกว่า “วิธีต้นแบบและเติมแต่ง” แต่ละฟังก์ชันถูกทำเป็นต้นแบบด้วยการทำงานพื้นฐานก่อน จากนั้นทดสอบและปรับแก้ข้อผิดพลาดที่เกิดขึ้นระหว่างใช้งาน
แนวทางนี้สามารถเป็นหนทางที่มีประสิทธิภาพ โดยเฉพาะในเวลาที่เรายังไม่มีความเข้าใจปัญหาอย่างลึกซึ้ง แต่การปรับเพิ่มการแก้ไขสามารถทำให้โค้ดมีความซับซ้อน เพราะต้องจัดการกับกรณีเฉพาะหลายกรณี และทำให้ไม่น่าเชื่อถือ เนื่องจากเป็นการยากที่จะรู้ได้ว่าเราเจอข้อผิดพลาดได้ครบทุกกรณี
อีกทางเลือกหนึ่งคือ การออกแบบแล้วพัฒนา (designed development) ซึ่งมีการเข้าใจปัญหาในระดับสูงช่วยทำให้การเขียนโปรแกรมง่ายขึ้นมาก ในกรณีนี้ความเข้าใจระดับสูงคือ ออบเจ๊คต์ Time เป็นเลขฐาน 60 จำนวน 3 ตำแหน่ง ! (ดูข้อมูลเพิ่มเติม http://en.wikipedia.org/wiki/Sexagesimal) โดยมีวินาที
เป็นหลักหน่วย มีนาที
เป็นหลัก 60 และชั่วโมง
เป็นหลัก 3600
เมื่อเราเขียนฟังก์ชัน add_time
และ increment
เราได้ทำการบวกในฐาน 60 และนี่คือเหตุผลว่าทำไมเราจึงทดจากหลักหนึ่งไปยังหลักถัดไป
จากข้อสังเกตนี้จึงได้แนวทางใหม่ที่สามารถจัดการปัญหาได้ทั้งหมด เนื่องจากเราสามารถแปลงเวลาให้เป็นจำนวนเต็มและใช้ความสามารถของคอมพิวเตอร์ช่วยบวกจำนวนเต็มให้
นี่คือฟังก์ชันที่แปลงเวลาเป็นจำนวนเต็ม
def time_to_int(time): minutes = time.hour * 60 + time.minute seconds = minutes * 60 + time.second return seconds
และนี่คือฟังก์ชันที่แปลงจำนวนเต็มเป็นเวลา (พึงระลึกว่าฟังก์ชัน divmod
หารอาร์กูเมนต์แรกด้วยอาร์กูเมนต์ที่สองแล้วให้ค่าออกมาเป็นผลหารและเศษในรูปของทูเพิล)
def int_to_time(seconds): time = Time() minutes, time.second = divmod(seconds, 60) time.hour, time.minute = divmod(minutes, 60) return time
อาจจะต้องใช้เวลาคิดสักหน่อยหรือรันทดสอบเพื่อให้มั่นใจว่าฟังก์ชันนี้ทำงานได้ถูกต้อง วิธีหนึ่งที่จะช่วยทดสอบได้คือ ตรวจสอบว่า time_to_int(int_to_time(x)) == x
สำหรับหลายๆ ค่าของ x
นี่เป็นตัวอย่างของการตรวจสอบความสอดคล้อง
และเมื่อมั่นใจแล้วว่าการทำงานนั้นถูกต้อง เราก็สามารถใช้คำสั่งเหล่านี้มาเขียนฟังก์ชัน add_time
ใหม่ได้ดังนี้
def add_time(t1, t2): seconds = time_to_int(t1) + time_to_int(t2) return int_to_time(seconds)
เวอร์ชันนี้สั้นกว่าเวอร์ชันก่อนหน้าและง่ายต่อการตรวจสอบ ให้ทำเป็นแบบฝึกหัด คือ เขียนฟังก์ชัน increment
ใหม่โดยใช้ฟังก์ชัน time_to_int
และ int_to_time
ช่วย
บางทีการแปลงค่าจากฐาน 60 เป็นฐาน 10 ไปและกลับก็ยากกว่าการจัดการกับเวลา การแปลงฐานเข้าใจได้ยากกว่า สัญชาตญาณของเราจึงบอกว่าการจัดการกับเวลาน่าจะเป็นวิธีที่ดีกว่า
แต่ถ้าเราเข้าใจได้อย่างถ่องแท้เรื่องการจัดการกับเลขฐาน 60 และได้ลงทุนเขียนฟังก์ชันการแปลงแล้ว (time_to_int
และ int_to_time
) เราจะได้โปรแกรมที่สั้นกว่า ง่ายต่อการทำความเข้าใจและแก้ไข แถมยังน่าเชื่อถือกว่า
มันยังจะเป็นการง่ายกว่าที่จะเพิ่มความสามารถอย่างอื่นเข้าไป เช่นการลบเวลาเพื่อหาช่วงระยะเวลาระหว่างสองเวลา วีธีการแบบธรรมดาคือ ลบแบบมีการยืม แต่การใช้ฟังก์ชันการแปลงช่วยจะง่ายกว่าและมีโอกาสถูกต้องสูงกว่า
พูดอย่างกระทบกระเทียบ บางครั้งทำปัญหาให้ยากกว่า (มีความกว้างขวางกว่า) ทำให้ทำได้ง่ายกว่า เพราะว่ามีกรณีเฉพาะน้อยกว่าและมีโอกาสผิดพลาดน้อยกว่า
ออบเจ๊คต์ Time เป็นรูปแบบที่ดีถ้าค่าของนาที
และวินาที
มีค่าเป็นจำนวนเต็มระหว่าง 0 ถึง 60 (รวม 0 แต่ไม่รวม 60) และค่าชั่วโมง
เป็นเลขบวก ชั่วโมง
และ นาที
ควรเป็นจำนวนเต็ม เราอาจจะอนุญาตให้วินาทีมีค่าเป็นทศนิยมได้
ข้อกำหนดเหล่านี้ถูกเรียกว่า ความคงที่ (invariants) เพราะจะต้องเป็นจริงเสมอ ถ้ามีอะไรที่ไม่สอดคล้อง คือ ไม่ถูกต้องตามข้อกำหนด จะทำให้เกิดข้อผิดพลาดบางประการขึ้น
การเขียนคำสั่งสำหรับตรวจสอบความคงที่ของข้อมูลสามารถช่วยตรวจสอบข้อผิดพลาดและช่วยหาสาเหตุของข้อผิดพลาดได้ ตัวอย่างเช่น เราอาจจะมีฟังก์ชัน valid_time
ที่รับออบเจ๊คต์ Time และให้ค่าออกมาเป็น False
ถ้ามีการละเมิดข้อกำหนด
def valid_time(time): if time.hour < 0 or time.minute < 0 or time.second < 0: return False if time.minute >= 60 or time.second >= 60: return False return True
ในช่วงเริ่มต้นของแต่ละฟังก์ชัน เราสามารถตรวจสอบอาร์กูเมนต์เพื่อให้มันใจว่ามีความถูกต้อง
def add_time(t1, t2): if not valid_time(t1) or not valid_time(t2): raise ValueError('invalid Time object in add_time') seconds = time_to_int(t1) + time_to_int(t2) return int_to_time(seconds)
หรือเราอาจใช้คำสั่ง assert ซึ่งจะตรวจสอบข้อกำหนดความคงที่และสร้างเอ็กเซ็ปชั่นถ้าไม่ผ่านข้อกำหนด
def add_time(t1, t2): assert valid_time(t1) and valid_time(t2) seconds = time_to_int(t1) + time_to_int(t2) return int_to_time(seconds)
คำสั่ง assert
เป็นคำสั่งที่มีประโยชน์ เนื่องจากสามารถแยกข้อแตกต่างของการตรวจสอบข้อผิดพลาดออกจากคำสั่งที่จัดการกับข้อมูลปกติได้
None
ตัวอย่างโปรแกรมของบทนี้สามารถดาวน์โหลดได้ที่ http://thinkpython2.com/code/Time1.py เฉลยสำหรับแบบฝึกหัดอยู่ที่ http://thinkpython2.com/code/Time1_soln.py
แบบฝึกหัด 1
เขียนฟังก์ชันชื่อ mul_time
ซึ่งรับออบเจ๊คต์ Time และตัวเลข แล้วให้ค่าออกมาเป็นออบเจ๊คต์ Time ใหม่ ที่บรรจุผลคูณของเวลาและตัวเลขนั้น
จากนั้นใช้ฟังก์ชัน mul_time
เพื่อเขียนฟังก์ชันซึ่งรับออบเจ๊คต์ Time ซึ่งเป็นเวลาที่ใช้เพื่อไปถึงเส้นชัยในการแข่งขันและรับตัวเลขที่เป็นระยะทาง จากนั้นให้ค่าออกมาเป็นออบเจ๊คต์ Time ที่แสดงค่าก้าวเฉลี่ย (average pace: เวลาต่อไมล์)
แบบฝึกหัด 2
โมดูล datetime
มีออบเจ๊คต์ time
ที่คล้ายกับออบเจ๊คต์ Time ในบทนี้ แต่มีเมธอดและตัวดำเนินการมากมาย ศึกษาเอกสารได้ที่ http://docs.python.org/3/library/datetime.html
datetime
เพื่อเขียนโปรแกรมสำหรับอ่านเวลาปัจจุบัน และพิมพ์ค่าวันของสัปดาห์เฉลย: http://thinkpython2.com/code/double.py
https://greenteapress.com/thinkpython2/html/thinkpython2017.html