มาถึงจุดนี้คุณได้รู้จักวิธีการใช้งานฟังก์ชันเพื่อจัดระเบียบโค้ดและใช้ชนิดข้อมูลในตัวเพื่อจัดระเบียบข้อมูล ขั้นต่อไปเป็นการเรียนรู้ “การเขียนโปรแกรมเชิงวัตถุ” ซึ่งจะใช้ชนิดข้อมูลที่ผู้เขียนโปรแกรมกำหนดเองเพื่อจัดระเบียบได้ทั้งโค้ดและข้อมูล การเขียนโปรแกรมเชิงวัตถุเป็นหัวข้อใหญ่ เพื่อให้เข้าใจจะต้องศึกษาเพิ่มอีกสองถึงสามบท
ตัวอย่างโปรแกรมของบทนี้สามารถดาวน์โหลดได้ที่ http://thinkpython2.com/code/Point1.py เฉลยสำหรับแบบฝึกหัดอยู่ที่ http://thinkpython2.com/code/Point1_soln.py
เราได้ใช้ชนิดข้อมูลภายในของไพธอนมาหลายชนิดแล้ว คราวนี้เรามาลองสร้างชนิดข้อมูลใหม่ ตัวอย่างเช่น สร้างชนิดข้อมูลชื่อว่า Point
ซึ่งใช้สำหรับแทนจุดในระนาบสองมิติ
สัญลักษณ์สำหรับแทนจุดในทางคณิตศาสตร์มักเขียนคู่ลำดับไว้ในวงเล็บและคั่นตัวเลขด้วยจุลภาค ตัวอย่างเช่น $(0,0)$ แทนตำแหน่งต้นกำเนิด และ $(x,y)$ ใช้แทนจุดที่ห่างไปทางขวา $x$ หน่วย และสูงขึ้นไป $y$ หน่วยจากจุดต้นกำเนิด
มีหลากหลายแนวทางที่อาจใช้แทนจุดในภาษาไพธอน
x
และ y
การสร้างชนิดข้อมูลใหม่เป็นวิธีการที่ซับซ้อนกว่าวิธีการอื่น แต่ก็มีข้อดีอีกหลายประการดังจะได้เห็นต่อไป
ชนิดข้อมูลที่ผู้เขียนโปรแกรมกำหนดขึ้นนี้มีชื่อเรียกอีกอย่างว่า คลาส (class) การนิยามคลาสมีลักษณะดังนี้
class Point: """Represents a point in 2-D space."""
ส่วนหัวบ่งชี้ว่าคลาสใหม่นี้มีชื่อว่า Point
ส่วนเนื้อหาเป็นข้อความบรรยายที่ใช้อธิบายว่าคลาสนี้สร้างขึ้นเพื่ออะไร เราสามารถนิยามตัวแปรและเมธอดภายในส่วนนิยามคลาส แต่เราจะกลับมากล่าวถึงในภายหลัง
การนิยามคลาสชื่อ Point
จะสร้าง คลาสออบเจ๊คต์ (class object) ขึ้น
>>> Point <class '__main__.Point'>
เนื่องจาก Point
ถูกนิยามในระดับบนสุด ดังนั้นจึงมี “ชื่อเต็ม” ว่า __main__.Point
คลาสออบเจ๊คต์เป็นเหมือนโรงงานสำหรับสร้างออบเจ๊คต์ เราเรียกใช้ Point
เหมือนกับการเรียกฟังก์ชันเพื่อสร้างออบเจ๊คต์ Point
>>> blank = Point() >>> blank <__main__.Point object at 0xb7e9d3ac>
ค่าที่ถูกส่งออกมาเป็นอ้างอิงไปยังออบเจ๊คต์ของ Point ซึ่งเรากำหนดค่าให้กับ blank
การสร้างออบเจ๊คต์ใหม่ขึ้นมานี้จะเรียกว่า การสร้างอินสแตนซ์ (instantiation) และออบเจ๊คต์ที่ได้คือหนึ่ง อินสแตนซ์ (instance) ของคลาสนั้น
เมื่อคุณพิมพ์อินสแตนซ์ ไพธอนจะบอกว่ามันเป็นอินสแตนซ์ของคลาสใดและบอกว่าถูกเก็บไว้ที่ใดในหน่วยความจำ (ส่วนหน้า 0x
บอกให้รู้ว่า ตัวเลขที่ตามมานั้นเป็นเลขฐานสิบหก)
ทุกๆ ออบเจ๊คต์เป็นหนึ่งอินสแตนซ์ของบางคลาส ดังนั้นการใช้ “ออบเจ๊คต์” และ “อินสแตนซ์” สามารถใช้แทนกันได้ แต่ในบทนี้จะใช้ “อินสแตนซ์” เพื่อชี้ชัดว่ากำลังกล่าวถึงชนิดข้อมูลที่ผู้เขียนโปรแกรมกำหนดขึ้น
เราสามารถกำหนดค่าให้กับอินสแตนซ์โดยการใช้สัญกรณ์จุด
>>> blank.x = 3.0 >>> blank.y = 4.0
ไวยากรณ์เช่นนี้เหมือนกับไวยากรณ์ที่ใช้ในการเลือกตัวแปรจากโมดูล ตัวอย่างเช่น math.pi
หรือ string.whitespace
ในกรณีนี้เราได้กำหนดค่าให้กับสมาชิกของออบเจ๊คต์ แต่อย่างไรก็ตามสมาชิกเหล่านี้ถูกเรียกว่า แอตทริบิวต์ (attributes)
ออกเสียงเหมือนคำนาม “AT-trib-ute” เน้นเสียงของพยางค์แรก ตรงกันข้ามกับคำกริยา “a-TRIB-ute” ซึ่งจะเน้นเสียงของพยางค์ที่สอง
แผนภาพต่อไปนี้แสดงผลลัพธ์ของการกำหนดค่าเหล่านี้ แผนภาพสถานะที่แสดงออบเจ๊คต์และแอตทริบิวต์ของออบเจ๊คต์จะถูกเรียกว่า แผนภาพออบเจ๊คต์ (object diagram) ดูรูปที่ 15.1
ตัวแปร blank
อ้างถึงออบเจ๊คต์ Point ซึ่งบรรจุสองแอตทริบิวต์ แต่ละแอตทริบิวต์อ้างอิงถึงตัวเลขทศนิยมอย่างละตัว
เราสามารถอ่านค่าของแอตทริบิวต์แต่ละตัวด้วยวิธีการเดียวกัน
>>> blank.y 4.0 >>> x = blank.x >>> x 3.0
นิพจน์ blank.x
หมายถึง “ไปยังตำแหน่งที่อ้างถึงโดยออบเจ๊คต์ blank
และอ่านค่าของแอตทริบิวต์ x
” ในตัวอย่างจะเห็นว่า มีการกำหนดค่าให้กับตัวแปร x
ซึ่งไม่ทำให้เกิดการขัดแย้งระหว่างตัวแปร x
และแอตทริบิวต์ x
แต่อย่างใด
คุณสามารถใช้สัญกรณ์จุดเป็นส่วนหนึ่งของนิพจน์ใดๆ ได้ ตัวอย่างเช่น
>>> '(%g, %g)' % (blank.x, blank.y) '(3.0, 4.0)' >>> distance = math.sqrt(blank.x**2 + blank.y**2) >>> distance 5.0
เราสามารถส่งผ่านอินสแตนซ์เป็นอาร์กูเมนต์ได้ตามปกติ ตัวอย่างเช่น
def print_point(p): print('(%g, %g)' % (p.x, p.y))
print_point
รับจุดหนึ่งจุดเข้ามาเป็นอาร์กูเมนต์แล้วแสดงค่าแบบสัญลักษณ์ทางคณิตศาสตร์ จึงสามารถเรียกใช้โดยส่งผ่าน blank
เป็นอาร์กูเมนต์ได้
>>> print_point(blank) (3.0, 4.0)
ภายในฟังก์ชัน p
เป็นสมนามของ blank
ดังนั้นถ้าฟังก์ชันมีการแก้ไขค่าของ p
ก็จะส่งผลต่อ blank
เช่นกัน
เพื่อเป็นการฝึก ให้เขียนฟังก์ชันชื่อ distance_between_points
ซึ่งรับจุดสองจุดเป็นอาร์กูเมนต์และให้ค่าออกมาเป็นระยะห่างระหว่างสองจุดนั้น
บางครั้งก็มีความชัดเจนว่าอะไรควรเป็นแอตทริบิวต์ของออบเจ๊คต์ แต่ในบางคราวเราจะต้องตัดสินใจ ตัวอย่างเช่น ลองจินตนาการว่าเรากำลังออกแบบคลาสสำหรับรูปสี่เหลี่ยม อะไรเป็นแอตทริบิวต์ที่เราจะใช้เพื่อระบุตำแหน่งที่ตั้งและขนาดของรูปสี่เหลี่ยม? เราสามารถที่จะละเลยเรื่องของมุมเพื่อให้ง่ายขึ้นโดยสมมติว่ามีเฉพาะสี่เหลี่ยมในแนวตั้งและแนวนอน
มีอย่างน้อยสองแนวทางที่เป็นไปได้
ณ จุดนี้ ยังเป็นการยากที่จะบอกว่าวิธีการไหนจะเป็นวิธีการที่ดีกว่าวิธีอื่น ดังนั้นเราจะลองทำตามวิธีแรกเพื่อเป็นตัวอย่าง
นี่เป็นนิยามของคลาสดังกล่าว
class Rectangle: """Represents a rectangle. attributes: width, height, corner. """
ด็อกสตริงได้ระบุรายการแอตทริบิวต์ไว้ด้วยคือ width
และ height
เป็นตัวเลข ส่วน corner
เป็นออบเจ๊คต์ Point ที่ใช้ระบุตำแหน่งของมุมซ้ายล่าง
เพื่อให้ได้ตัวแทนของรูปสี่เหลี่ยมหนึ่งรูป เราจะต้องสร้างอินสแตนซ์ของคลาส Rectangle และกำหนดค่าต่างๆ ให้กับแอตทริบิวต์ เช่น
box = Rectangle() box.width = 100.0 box.height = 200.0 box.corner = Point() box.corner.x = 0.0 box.corner.y = 0.0
นิพจน์ box.corner.x
หมายถึง “ไปยังตำแหน่งที่ออบเจ๊คต์ box
อ้างถึงและเลือกแอตทริบิวต์ชื่อว่า corner
ซึ่งจะเป็นการไปยังออบเจ๊คต์นั้นและเลือกแอตทริบิวต์ชื่อ x
”
รูปที่ 16.2 แสดงสถานะของออบเจ๊คต์ที่ได้ ออบเจ๊คต์ที่ถูกกำหนดเป็นแอตทริบิวต์ของออบเจ๊คต์อื่นจะ ฝังตัว อยู่ภายในอีกที
ฟังก์ชันสามารถส่งค่าออกมาเป็นอินสแตนซ์ได้ ตัวอย่างเช่น ฟังก์ชัน find_center
รับอาร์กูเมนต์เป็นออบเจ๊คต์ Rectangle
แล้วให้ค่าออกมาเป็นอินสแตนซ์ของ Point
ที่มีพิกัดตำแหน่งกึ่งกลางของรูปสี่เหลี่ยม
def find_center(rect): p = Point() p.x = rect.corner.x + rect.width/2 p.y = rect.corner.y + rect.height/2 return p
นี่เป็นตัวอย่างการส่ง box
เป็นอาร์กูเมนต์และกำหนดค่าผลลัพธ์ซึ่งเป็นอินสแตนซ์ Point ให้กับ center
>>> center = find_center(box) >>> print_point(center) (50, 100)
เราสามารถเปลี่ยนแปลงสถานะของออบเจ๊คต์ได้ด้วยการกำหนดค่าใหม่ให้กับแอตทริบิวต์ของออบเจ๊คต์ ตัวอย่างเช่น การเปลี่ยนขนาดของรูปสี่เหลี่ยมโดยไม่ย้ายตำแหน่ง สามารถทำได้โดยการแก้ไขค่าของ width
และ height
box.width = box.width + 50 box.height = box.height + 100
เราสามารถเขียนฟังก์ชันเพื่อแก้ไขออบเจ๊คต์ ตัวอย่างเช่น ฟังก์ชัน grow_rectangle
ซึ่งรับออบเจ๊คต์ Rectangle และตัวเลขอีกสองตัวคือ dwidth
และ dheight
สำหรับใช้บวกเพิ่มให้กับความกว้างและความสูงของรูปสี่เหลี่ยม
def grow_rectangle(rect, dwidth, dheight): rect.width += dwidth rect.height += dheight
นี่คือตัวอย่างที่แสดงให้เห็นถึงผลที่ได้
>>> box.width, box.height (150.0, 300.0) >>> grow_rectangle(box, 50, 100) >>> box.width, box.height (200.0, 400.0)
rect
ที่อยู่ภายในฟังก์ชันเป็นสมนามของ box
ดังนั้นเมื่อใดที่ฟังก์ชันมีการแก้ไขค่าของ rect
ค่าของ box
ก็จะเปลี่ยนตาม
เพื่อเป็นการฝึก ให้เขียนฟังก์ชันชื่อว่า move_rectangle
ซึ่งรับอาร์กูเมนต์เป็นออบเจ๊คต์ Rectangle และตัวเลขอีกสองจำนวนคือ dx
และ dy
ให้ฟังก์ชันทำการย้ายตำแหน่งของสี่เหลี่ยมด้วยบวกเพิ่ม dx
ให้กับพิกัด x
ของ corner
และบวกเพิ่ม dy
ให้กับพิกัด y
ของ corner
การมีสมนามทำให้การทำความเข้าใจโปรแกรมทำได้ยาก เนื่องจากการเปลี่ยนแปลงค่าในที่หนึ่งอาจส่งผลกระทบอย่างคาดไม่ถึงกับอีกที่หนึ่ง ซึ่งจะเป็นการยากที่จะติดตามค่าของทุกตัวแปรที่มีการอ้างถึงออบเจ๊คต์ที่กำหนด
การทำสำเนาออบเจ๊คต์เป็นอีกทางเลือกแทนการทำสมนาม โมดูล copy
มีฟังก์ชันชื่อว่า copy
ซึ่งสามารถสร้างสำเนาของออบเจ๊คต์ใดก็ได้
>>> p1 = Point() >>> p1.x = 3.0 >>> p1.y = 4.0 >>> import copy >>> p2 = copy.copy(p1)
p1
และ p2
มีข้อมูลบรรจุภายในเหมือนกัน แต่เป็นคนละออบเจ๊คต์
>>> print_point(p1) (3, 4) >>> print_point(p2) (3, 4) >>> p1 is p2 False >>> p1 == p2 False
ตัวดำเนินการ is
แสดงให้เห็นชัดว่า p1
และ p2
ไม่ใช่ออบเจ๊คต์เดียวกันเป็นไปตามที่เราคาดหวัง แต่เราอาจจะคาดหวังว่าตัวดำเนินการ ==
ให้ผลลัพธ์เป็น True
เนื่องจากทั้งสองจุดนี้มีข้อมูลที่เหมือนกัน ในกรณีนี้คุณอาจจะผิดหวังที่ได้รู้ว่า สำหรับอินสแตนซ์แล้วพฤติกรรมเริ่มต้นของตัวดำเนินการ ==
ให้ผลเหมือนกับตัวดำเนินการ is
ซึ่งจะตรวจสอบว่าเป็นออบเจ๊คต์อันเดียวกันหรือไม่ ไม่ใช่ตรวจสอบว่ามีความเท่าเทียมกันหรือไม่ ทั้งนี้เป็นเพราะว่าสำหรับชนิดข้อมูลที่ผู้เขียนโปรแกรมกำหนดขึ้นเองแล้วไพธอนไม่รู้ว่าควรจะตรวจสอบความเท่ากันอย่างไร อย่างน้อยก็ยัง
ถ้าเราใช้ copy.copy
เพื่อสำเนาออบเจ๊คต์ Rectangle เราจะพบว่ามีการสำเนาเฉพาะ Rectangle แต่จะไม่สำเนาออบเจ๊คต์ฝังตัว Point
>>> box2 = copy.copy(box) >>> box2 is box False >>> box2.corner is box.corner True
รูปที่ 16.3 แสดงแผนภาพออบเจ๊คต์ให้เห็นว่ามีลักษณะอย่างไร การทำงานเช่นนี้เรียกได้ว่าเป็น การสำเนาตื้น (shallow copy) เนื่องจากทำการสำเนาเฉพาะตัวออบเจ๊คต์และทุกการอ้างอิง แต่ไม่สำเนาออบเจ๊คต์ที่ฝังอยู่ในออบเจ๊คต์นั้น
สำหรับแอปพลิเคชันส่วนใหญ่ไม่ได้ต้องการผลลัพธ์เช่นนี้ ในตัวอย่างนี้การเรียกใช้ grow_rectangle
กับออบเจ๊คต์ Rectangle ใดจะไม่สร้างผลกระทบต่อออบเจ๊คต์อื่น แต่ถ้าเรียกใช้ move_rectangle
จะส่งผลกระทบทั้งคู่ พฤติกรรมเช่นนี้สร้างความสับสนและมีข้อผิดพลาดง่าย
โชคดีที่โมดูล copy
ได้เตรียมเมธอดชื่อ deepcopy
ซึ่งคัดลอกไม่เฉพาะออบเจ๊คต์ที่ระบุแต่ยังคัดลอกรวมไปถึงออบเจ๊คต์ที่ถูกอ้างถึงและออบเจ๊คต์อื่นที่ถูกอ้างถึงด้วย ออบเจ๊คต์เหล่านั้น ไปเรื่อยๆ จึงไม่น่าแปลกใจที่กระบวนการนี้เรียกว่า การสำเนาลึก (deep copy)
>>> box3 = copy.deepcopy(box) >>> box3 is box False >>> box3.corner is box.corner False
box3
และ box
เป็นคนละออบเจ๊คต์ที่แยกจากกันอย่างสมบูรณ์
เพื่อเป็นการฝึก ให้เขียนฟังก์ชัน move_rectangle
อีกเวอร์ชันที่สร้างออบเจ๊คต์ Rectangle และให้ค่าออกมาเป็นออบเจ๊คต์ใหม่ แทนการแก้ไขออบเจ๊คต์เดิม
เมื่อเราเริ่มทำงานกับออบเจ๊คต์ เรามีแนวโน้มที่จะพบเอ็กเซ็ปชั่นใหม่ๆ ถ้าเราพยายามเข้าถึงแอตทริบิวต์ที่ไม่มีอยู่จริง เราจะพบเอ็กเซ็ปชั่น AttributeError
>>> p = Point() >>> p.x = 3 >>> p.y = 4 >>> p.z AttributeError: Point instance has no attribute 'z'
ถ้าเราไม่แน่ใจว่าออบเจ๊คต์นั้นเป็นชนิดใด เราสามารถสอบถาม
>>> type(p) <class '__main__.Point'>
เรายังสามารถใช้ isinstance
เพื่อตรวจสอบว่าออบเจ๊คต์นั้นเป็นอินสแตนซ์ของคลาสใดคลาสหนึ่งหรือไม่
>>> isinstance(p, Point) True
ถ้าเราไม่แน่ใจว่าออบเจ๊คต์หนึ่งมีแอตทริบิวต์ที่สนใจหรือไม่ เราสามารถใช้ฟังก์ชัน hasattr
ตรวจสอบได้
>>> hasattr(p, 'x') True >>> hasattr(p, 'z') False
อาร์กูเมนต์แรกคือออบเจ๊คต์ใดๆ อาร์กูเมนต์ที่สองเป็น สตริง ที่มีชื่อของแอตทริบิวต์ที่ต้องการทราบ
นอกจากนี้เรายังสามารถใช้คำสั่ง try
เพื่อหาดูว่าออบเจ๊คต์นั้นมีแอตทริบิวต์ที่เราต้องการหรือไม่
try: x = p.x except AttributeError: x = 0
วิธีนี้สามารถช่วยทำให้การเขียนฟังก์ชันเพื่อทำงานกับหลากหลายชนิดข้อมูลได้ ดูรายละเอียดเพิ่มเติมในหัวข้อ 17.9
copy
ที่อยู่ในโมดูล copy
deepcopy
ที่อยู่ในโมดูล copy
แบบฝึกหัด 1
เขียนนิยามของคลาสชื่อ Circle
ที่มีแอตทริบิวต์ center
และ radius
กำหนดให้ center
เป็นออบเจ๊คต์ของ Point และให้ radius เป็นตัวเลข
สร้างอินสแตนซ์ของ Circle ซึ่งเป็นตัวแทนของวงกลมซึ่งมีจุดศูนย์กลางอยู่ที่ (150, 100) รัศมี 75 หน่วย
เขียนฟังก์ชันชื่อ point_in_circle
ซึ่งรับออบเจ๊คต์ Circle และ Point แล้วให้ค่าคืนกลับเป็น True ถ้าจุดนั้นอยู่ภายในหรือบนขอบเขตของวงกลม
เขียนฟังก์ชันชื่อ rect_in_circle
ซึ่งรับออบเจ๊คต์ Circle และ Rectangle แล้วให้ค่าคืนกลับเป็น True ถ้ารูปสี่เหลี่ยมนั้นอยู่ภายในหรือบนขอบเขตของวงกลม
เขียนฟังก์ชันชื่อ rect_circle_overlap
ซึ่งรับออบเจ๊คต์ Circle และ Rectangle แล้วให้ค่าคืนกลับเป็น True ถ้ามุมใดมุมหนึ่งของสี่เหลี่ยมอยู่ภายในวงกลม หรือเขียนรุ่นที่ท้าทายมากขึ้นซึ่งจะให้ค่าออกมาเป็น True ถ้าส่วนใดส่วนหนึ่งของรูปสี่เหลี่ยมอยู่ภายในวงกลม
เฉลย: http://thinkpython2.com/code/Circle.py
แบบฝึกหัด 2
เขียนฟังก์ชันชื่อ draw_rect
ซึ่งรับออบเจ๊คต์ Turtle และ Rectangle แล้วใช้ออบเจ๊คต์ Turtle ในการวาดรูปสี่เหลี่ยมโดยดูตัวอย่างการใช้งาน Turtle ในบทที่ 4
เขียนฟังก์ชันชื่อ draw_circle
ซึ่งรับออบเจ๊คต์ Turtle และ Circle แล้ววาดรูปของวงกลมนั้น
เฉลย: http://thinkpython2.com/code/draw.py
https://greenteapress.com/thinkpython2/html/thinkpython2016.html