Python: Object-Oriented #1: Monkey Patch

เกริ่นนำ

ภาษา Python นั้นรองรับกระบวนทัศน์ในการเขียนโปรแกรม (paradigm) ได้หลายแบบ ขึ้นอยู่กับว่าผู้เขียนโปรแกรมจะเลือกใช้งานอย่างไร ซึ่งแน่นอนครับ ในยุคที่หายใจเข้าออกเป็น object อย่างทุกวันนี้ ภาษา Python ก็สามารถรองรับกระบวนทัศน์ Object-Oriented ได้ ไม่ใช่แค่ได้นะครับ ทำได้ดีด้วย ซึ่งผมจะค่อยๆ เล่าให้ท่านฟัง

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

We’re all consenting adults here

ขอเริ่มที่ประเด็นเล็กๆ ให้เห็นภาพกันก่อน คือเรื่อง fields ที่อยู่ใน object แน่นอนครับการเปลี่ยนแปลง fields จากภายนอก object ได้โดยตรงนั้น อาจจะให้ object นั้นทำงานผิดพลาดได้ แม้ว่าควบคุมให้เข้าถึงได้เฉพาะ object จาก class ที่ inherit มา ก็เสี่ยงอยู่ดี อาจจะเกิดสิ่งที่เรียกว่า “fragile base class” ได้  ทำให้ภาษาในกระแสหลักนั้น  เขาจึงออกแบบ public/protected/private เอาไว้ให้ใช้ แล้วให้ผู้เขียนโปรแกรมใช้วิจารณญาณเลือกใช้ให้เหมาะสมเอง แต่พอมาถึง Python กลับ ไม่มี! Python เปิดโล่งเตียนเป็น public  ตลอด ใครใคร่ใช้ ใช้  (ECMAScript ก็เป็นเช่นนั้น)

เมื่อสิบกว่าปีที่แล้วมีคนเขียนอีเมลใน LISTSERV Python ไปถาม Guido van Rossum ผู้สร้าง Python ว่าทำไมถึงไม่มี private เหมือนดังเช่นในภาษาอื่นๆ ซึ่ง Rossum ก็ตอบเป็นประโยคอมตะว่า “We’re all consenting adults here” หรือแปลแบบเอาความว่า “ที่นี่เรา (เหล่าโปรแกรมเมอร์) ล้วนแล้วแต่โตๆ กันแล้ว” หรือถ้าจะแปลเอาความหมายจริงๆ ก็น่าจะแปลได้ว่า “โปรแกรมเมอร์ไม่โง่นะ เรารู้ดีนะว่ากำลังทำอะไรอยู่”

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

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

design patterns ที่ออกแบบกันมาเพื่อลดความซับซ้อนนั้น แต่ตัวมันเองก็คือความซับซ้อน จริงหรือไม่ ลองคิดดู

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

เมื่อก่อนผมก็มอง object-oriented design เป็นกฏที่ต้องเทิดไว้ ทำตามกติกาอย่างเคร่งครัด โปรแกรมเราเมื่อใหญ่ขึ้นจะได้ไม่มีปัญหา แต่พอมาใช้ Python และ ECMAScript ที่มีความยืดหยุ่นกว่ามาก แรกๆ ก็รับไม่ค่อยได้หรอกครับ แต่พอใช้ๆ ไป กลับเคยชิน ปัญหาที่คิดว่าจะเกิดถ้าเราไม่ควบคุมให้ดี กลับแทบไม่เคยเกิด (ที่ใช้คำว่าแทบ เพราะจำไม่ได้จริงๆ ว่ามันเคยเกิดหรือไม่) ผมเคยคิดกระทั่งว่าเราโดนฝรั่งต้มเหมือนปัญหา Y2K ที่ผ่านมาหรือเปล่า

เท่าที่ผมค้นหาข้อมูลในอินเตอร์เน็ต ก็แทบไม่เคยพบว่าใครมีปัญหากับโครงสร้างภาษาของ Python (แทบ – ในบริบทเดียวกับย่อหน้าข้างบนครับ) เมื่อผมพิจารณาอีกมิติหนึ่ง ลองดูสิว่า Python เขาเอาไปทำงานใหญ่ๆ กับบ้างไหน ก็พบ wikipedia หน้านี้ครับ https://en.wikipedia.org/wiki/List_of_Python_software  หลายตัวในรายการนี้ก็เป็นโปรแกรมระดับโลกที่รู้จักกันดี ดังนั้นที่ว่าการไม่มี private จะทำงานใหญ่ไม่ได้ ข้อนี้ก็ล้มล้างไปครับ

นอกจากการไม่มีการจำกัดขอบเขตของตัวแปรแล้ว Python ยังมีความยืดหยุ่นอีกบางประการกว่าภาษาทาง OO กระแสหลัก ใครใช้ภาษาทางกระแสหลักมาอาจจจะส่ายหน้า ซึ่งความยืดหยุ่นนี้ก็มีใน ECMAScript เช่นกัน ซึ่งผมเคยขอยกยอดเอาไว้ มาเขียนในบทความนี้ ท่านอาจเปลี่ยนใจไม่ส่ายหน้าก็ได้ หลังจากที่ผมนำเสนอให้ท่านอ่านกันในสองบทความนี้ครับ

Everything is an object

ผมขอเริ่มเนื้อหาของ OO ของ Python ตรงนี้ครับ “ทุกสิ่งทุกอย่างเป็น object” ภาษาที่ใช้กระบวนทัศน์แบบ OO นั้นส่วนมากก็จะมีคำกล่าวอ้างดังนี้ครับ แต่เอาเข้าจริงๆ ก็มีข้อยกเว้น จำพวกที่เรียกว่าชนิดตัวแปร value type หรือ primitive type เหล่านี้ไม่ใช่ object นะครับ แต่อาจจะมีกลยุทธ์ทำให้ดูเหมือนเป็น object โดยใช้การ boxing

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

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

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

มาลองดูโค้ดแรกของ Python กัน มาดูกันว่า Python นั้นจัดการ object กันเป็นอย่างไร

ท่านคิดว่าในหน่วยความจำจะเป็นอย่างไรครับ ทายถูกกันไหม

python_oo_1

ภาพนี้บอกอะไรเราหลายอย่าง อย่างแรกก็คือ ตัวแปรทุกตัวที่สร้างขึ้นมาใน Python เป็นตัวแปรตัวชี้แบบ reference ทั้งหมด ไม่มีข้อยกเว้น

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

sys.getrefcount() เป็นการนับว่า object ที่ส่งไปเป็นพารามิเตอร์นั้นมี reference ชี้อยู่กี่ตัว ตัวเลขที่ท่านได้ อาจจะไม่เหมือนของผม แต่การเพิ่มลดจะต้องเป็นอัตราเดียวกัน ใน A: ของผมนั้นของเดิม มีตัวชี้มาก่อนหน้าแล้ว 2 ตัว จากนั้นผมให้ a, b, c ไปชี้ มันจึงเพิ่มอีก 3 กลายเป็น B:  5 และเมื่อเปลี่ยนใจให้ b ไปชี้ที่อื่น จึงลดลงหนึ่ง C: จึงเหลือเพียง 4 ง่ายๆ แค่นี้เองครับ

class และ type

จากตัวอย่างจะเห็นว่าตัวชี้ทุกตัวมีชนิดกำกับอยู่ตามสิ่งที่มันชี้อย่างเคร่งครัดและยืดหยุ่นในเวลาเดียวกันตามแนวคิดของตัวแปร reference นั่นเอง ดังนั้นเราจึงเรียกใช้ methods หรือ properties ต่างๆ ที่ class นั้นพึงมี มากระทำกับข้อมูลของมัน ลองดูตัวอย่างนี้ครับ

ผมพุ่งเป้าไปยังตัวเลขจำนวนเต็ม ซึ่งมี method หนึ่งชื่อว่า bit_length() เพื่อแสดงขนาดของหน่วยความจำที่ใช้เป็นจำนวนบิท จะเห็นว่าเราเรียกผ่านตัวชี้ก็ได้ หรือจะเรียกโดยตรงผ่านตัวเลขเลยก็ได้ แต่ก็ต้อง () คร่อมตัวเลขก่อน มิฉะนั้นมันจะดีความว่า . ที่ตามหลังตัวเลขเป็นการบอกทศนิยม แทนที่จะเป็นการเรียกใช้ method ไปเสีย

การเปรียบเทียบ

เฉกเช่นเดียวกันกับภาษาทาง OO อื่นๆ การเปรียบเทียบ มีสองวิธีคือการเปรียบเทียบแบบเร็ว และการเปรียบเทียบแบบช้า

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

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

การเปรียบเทียบแบบเร็วใช้ is ส่วนแบบช้าใช้ == ลองดูตัวอย่าง

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

แอบส่อง collection

== และ is ไม่ได้เท่ากันเสมอไปหรอกครับ ลองนึกถึงหลักความจริงดู ถ้าท่านสร้าง list ขนาด 100 ค่าขึ้นมา และก่อนที่จะกำหนดค่าตัวแปรใดๆ ให้เท่ากับ list 100 ค่านี้ มันต้องไปค้นหาและเปรียบเทียบกับ list อื่นว่าตรงกันทุกค่าหรือไม่ ถ้าทำแบบนั้นประสิทธิภาพจะหายหมดแน่นอนครับ ดังนั้นพวก collections หรือ class อื่นๆ (ที่ไม่ได้เป็นแบบ primitive แบบภาษา OO อื่น) นั้นเวลากำหนดค่า จะไม่มีการไปไล่หาเพื่อให้ชี้ที่เดียวกันนะครับ

แต่ก่อนที่เราไปดู collection กัน ผมขอให้เราดู string กันก่อนครับ บางภาษามอง string อยู่ในกลุ่มพื้นฐานเหมือนพวกตัวเลข แต่บางภาษาก็มอง string มันก็คือ collection ของ ตัวอักษร ดังนั้นจึงไปอยู่กลุ่มเดียวกับพวก array ถ้าว่ากันจริงๆ แล้วภาษาส่วนมากจะมอง string เป็น array ประเภทหนึ่งครับ แล้ว Python มองอย่างไร เรามาดูกัน

ชัดเจนนะครับว่า s1 และ s1 ชี้ที่เดียวกัน “Hello, World” จึงมีเพียงชุดเดียวในหน่วยความจำ คราวนี้มาลองดู list ดูบ้าง

จะเห็นได้ว่า Python จะไม่พยายามไปค้นหาว่ามีตรงกันหรือไม่ มันช้า อยากให้มีก็สร้างให้เลย หวังว่าเข้าใจกันได้ไม่ยากนะครับ

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

การคัดลอก object

มาลองดูตัวอย่างกันครับว่าเราเข้าใจ เรื่องของ collection ดีแล้วรึยัง

คำถามก็คือ ในบรรทัดที่ 7 นี้จะแสดงผลอะไร มีคำตอบที่เป็นไปได้อยู่เพียงสองแบบเท่านั้นนั่นคือ [1, 2, 3] และ [2, 4, 6] ท่านคิดว่าคำตอบไหน ลองคิดดูก่อนเฉลยนะครับ

ผมยังไม่ตอบก็แล้วกัน ขอไล่โค้ดให้ดู การทำงานเริ่มจากบรรทัดแรก คือกำหนดค่า b = [1, 2, 3] ดังนั้น b จึงชี้ไปยัง [1, 2, 3]   จากนั้นไปเรียกใช้ฟังก์ชัน doubleIt(b)

พารามิเตอร์ที่ส่งเข้าไปยังฟังก์ชันนั้น แปลกสักเล็กน้อย คือ “Object references are passed by value” หรืออาจจะเข้าใจง่ายขึ้นถ้าพูดว่า “pass by object reference”  การส่งนั้นจะเป็นการคัดลอก reference เสมอครับ ไม่มีการคัดลอกตัว object แต่อย่างใด

ดังนั้นเมื่อทำงานถึงบรรทัดแรกในฟังก์ชัน a และ b จะชี้ที่เดียวกัน คือ [1, 2, 3] อะไรที่ทำกับ a ก็หมายถึงว่ามีผลกระทบกับ b ด้วย ดังนั้นคำตอบจึงเป็น [2, 4, 6] นั่นเอง

เอาหละ ถ้าเกิดต้องการให้แก้ b แล้วไม่กระทบกับ a นั่นคือทำให้เป็นคนละตัวกัน ก็ทำได้ครับ ดังนี้

คงไม่ต้องอธิบายมากนะครับโค้ดนี้ แต่สิ่งที่อยากเน้นก็คือการคัดลอกนี้เป็นแบบตื้น (shallow copy) ถ้าต้องการแบบลึก (deep copy) ก็เปลี่ยนไปเรืยก copy.deepcopy(a) แทน ถ้าใครสนใจรายละเอียดเกี่ยวกับ shallow/deepcopy ผมเคยเขียนในบทความชุด ECMAScript ไปแล้ว ลองไปหาอ่านดูได้ครับ

การสร้าง class

Python ก็เหมือนภาษาทั่วไป คือสร้าง object จาก class แต่ท่านก็มีทางเลือกอีกทางก็คือโคลนนิ่งมันมาโดยใช้ copy ดังที่ผมอธิบายเอาไว้ข้างต้น มาหัวข้อนี้เรามาสร้าง class กันครับ แต่ผมจะเริ่มสร้างจากแบบเปล่าๆ เพื่อต่อเติมภายหลังได้อย่าง ECMAScript ทำได้ดังนี้

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

จากโค้ดที่เห็นว่าเปล่าๆ ไม่มีของนั้น จริงๆ แล้วมันก็มีของบ้างที่ inherit มาจาก class หลักคือ object ดังเช่นในภาษาอื่นทั่วๆ ไป เราอาจจะแอบดูได้โดย ใช้คำสั่ง  print( dir(c) )  จะได้ผลลัพธ์ดังนี้ครับ

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

จะได้ผลลัพธ์ดังนี้ครับ

เห็นทั้ง pi และ sum แล้วนะครับ

รู้จัก fields

ลองทำ โจทย์ซักข้อเล่นดูครับ

ลองตอบดูครับที่ผม ? ไว้ จะแสดงผลลัพธ์อะไร (ทำอย่างกะเล่น Gotcha! กันอีกแล้ว) ใครตอบถูกบ้าง ถ้าใครเคยอ่าน ECMAScript ที่ผมเขียน คงพอเดาลูกเล่นนี้ออกครับ คำตอบคือ 3.14 ทั้งสองค่า

การกำหนด pi = 3.14 นั้นกำหนดใน class การกำหนดลักษณะนี้เป็นการกำหนดตัวแปรให้ class ครับ ซึ่งมีเพียงชุดเดียว ไม่มีการเพิ่มจำนวนตัวแปรตามจำนวนของ object ที่เพิ่มขึ้นนะครับ ชุดเดียวตลอด จะว่าไปก็เหมือน static fields ในภาษากระแสหลัก

ดังนั้นเมื่อเราสร้าง object และเรียกใช้ property pi  มันจะหาในตัวของ object ก่อน ซึ่งไม่เจอ เมื่อไม่เจอก็จะไปหาใน class ต่อไปซึ่งเจอครับ นั่นคือ 3.14 นั่นเอง

หัวใจอยู่ที่บรรทัดที่ 9 นั่นคือ a.pi = 10   เป็นการกำหนดค่า pi ให้เป็น 10 แต่ไม่ได้แก้ค่า pi ของเดิมนะครับ แต่หากเป็นการสร้าง field ใหม่อยู่ใน object ของ a เอง ดังนั้นในบรรทัดถัดมา เมื่อขอดูค่า pi ของ b มันก็ย่อมไม่มี และไปดึงมาจาก class จึงได้ 3.14

และเมื่อเราลบค่า pi โดยใช้คำสั่ง del ตัวแปรใน object ก็จะถูกลบทิ้ง และในบรรทัดสุดท้าย เพื่อพยายามขอค่ามาแสดง ในเมื่อค่าของตัวเองถูกลบไปแล้ว ดังนั้นจึงต้องดึงมาจาก class ได้ 3.14

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

python_oo_2

ลืมเขียนไปครับ กล่องกรอบสีฟ้าคือ class C1 นะครับ  และเพื่อความกระชับ ผมเขียนรวบ ตัวชี้ a, b และ object ที่มันชี้ให้เป็นตัวเดียวกัน คงไม่งงกันนะครับ ถ้าท่านเข้าใจแล้ว โค้ดข้างล่างท่านก็น่าจะตอบได้สบายๆ ลองดูครับ

Methods

เมื่อเราเพิ่ม fields ได้ เราก็สามารถเพิ่ม methods ได้เช่นกัน ซึ่งแนวคิดนั้นล้อมาจาก fields เลยครับ ลองดูแบบปกติกันก่อน

จุดที่น่าสนใจคือ ใน method ที่สร้างขึ้นมานั้น จะมีพารามิเตอร์ตัวแรกเป็น self (จริงๆ แล้วชื่ออะไรก็ได้ แต่ใช้ชื่อนี้เป็นกันเป็นมาตรฐาน) สังเกตที่ prototype ของ myset() ให้ดีนะครับ เวลาสร้างมีพารามิเตอร์ 4 ตัว แต่เวลาเอาไปใช้งานจริงกลับมีเพียง 3 ตัว self นั้นหายไปเฉยๆ ใครเคยอ่าน ECMAScript ของที่ผมเขียน ก็จะรู้ว่ามันก็แนวเดียวกัน แต่ถ้าใครยังไม่คุ้นเคย ผมขออธิบายอย่างนี้ครับ สิ่งที่เราเขียนคือ

เวลามันทำงานจริง มันจะแอบเปลี่ยนโค้ดให้เป็น

คือตบเอา a เข้าไปเป็นพารามิเตอร์ตัวแรกซึ่งก็คือ self นั่นเอง ดังนั้นการแก้อะไรที่ self ใน method ก็ย่อมส่งผลไปยัง a ด้วยเช่นกัน เพราะ a และ self นั้นชี้ที่ object เดียวกันนั่นเอง ดังนั้น method ทุกตัวจะต้องมีพารามิเตอร์อย่างน้อยหนึ่งตัว นั่นคือ self นั่นเอง

แนวคิดของการทำ OO แบบเรียบง่ายตรงไปตรงมาแบบนี้มีหลายภาษานะครับ ที่ผมใช้บ่อยๆ ก็ Python, ECMAScript และ MATLAB ล้วนแล้วแต่ใช้แนวนี้ทั้งนั้น

ถ้าอยากเพิ่มเติมภายหลังเหมือนกับ fields ก็ย่อมทำได้เช่นกัน โดย

หัวใจอยู่ที่บรรทัด 12-13 เราเติม methods เข้าไปยัง C1 ผมตั้งใจให้ชื่อ method ต่างออกไป เพื่อให้เห็นเด่นชัดขึ้น แต่ถ้าจะใช้ชื่อเดียวกันก็ย่อมทำได้ ไม่มีปัญหาแต่อย่างใด

คราวนี้เรามาดูกันอีกแบบ ด้วยเหตุผลอะไรก็ตามที ตามข้างบนมี object a และ b เรียบร้อยแล้ว ผมอยากเพิ่ม method เฉพาะกิจให้ a เท่านั้น แต่ b ไม่เพิ่มให้ ถามว่าทำได้ไหม คิดดีๆ ก่อนค่อยตอบนะครับ

ถ้าท่านบอกว่าทำได้ ก็ทำอย่างนี้ไง

ใครตอบแบบนี้ ผิดนะครับ เพราะอะไรหรือ ก็ในบรรทัดสุดท้ายมันจะพยายามแปลงให้เป็น

แต่ only_a ไม่มีใน C1 นะครับ ดังนั้นมันจึงไม่ผ่าน มันจึงทำขั้นต่อไป คือไปหา only_a() ใน a ซึ่งก็เจอ แต่จะไม่มีการตบตัวแปรนะครับ method ใน object  ถือว่าเป็นฟังก์ชันธรรมดา ดังนั้นถ้าเขียนให้ทำงานได้ ก็ต้องเขียนแบบนี้ครับ

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

ทางแก้ก็มี ด้องเรียกตัวช่วยครับ ดังนี้

จะสังเกตได้ว่า only_a() นิยามเหมือนกับที่จะบรรจุใน class มี self เป็นพารามิเตอร์ตัวแรก  ส่วน MethodType() ที่มีพารามิเตอร์สองตัวนั้น ตัวแรกเป็นชื่อของฟังก์ชัน และตัวที่สองเป็นการผูก ให้ a เป็นพารามิเตอร์ตัวแรก คล้ายๆ currying นั่นเอง ดังนั้นเวลาใช้งาน เราจึงข้ามพารามิเตอร์ตัวแรกไป เพราะผูกไว้ก่อนหน้าแล้วนั่นเอง

จะเนื้อหาข้างต้นเราจะเห็นวิธีการค้นหาของใน object อันเป็นเอกลักษณ์เฉพาะตัวของ Python นั่นคือ ถ้าเป็น fields จะค้นหาใน object ก่อน แล้วถ้าไม่เจอจะไปหาใน class ซึ่งตรงกันข้ามกับ methods จะหาใน class ก่อน ถ้าไม่เจอค่อยไปหาใน object

Monkey Patching

มาเรียนรู้ศัพท์กันหน่อยครับ วันนี้เสนอคำว่า  “Monkey Patching “มันจะคืออะไร ก็เอาไว้ก่อนก็แล้วกัน มาดูรากของมันก่อน ดูว่าที่มาของมันนั้นมันมาจากไหน

รากของคำนี้จริงๆ แล้วมาจากคำว่า “Guerilla Warfare” หรือสงครามกองโจรนั่นเอง เมื่อถึงคำว่าสงครามกองโจร ก็ชวนให้นึงถึงคนนี้เลยครับ

che guevara

คนที่อยู่ท้ายรถบรรทุกคนนี้ชื่อ เช กูวารา เรารู้จักกันดีอยู่แล้ว เป็นแต่งตำราชื่อว่า “Guerilla Warfare” อันโด่งดังขึ้นมานั่นเอง ถือว่าเป็นตำราที่ทรงคุณค่าของโลกเล่มหนึ่ง แนวคิดของการรบแบบกองโจรนี้ ท่านเข้าใจดีอยู่แล้ว เอาคนน้อยชนะคนมาก มันบุกเราถอย มันล้าเราตี มันพักเราป่วน มันถอยเราไล่  โจมตีโดยไม่จำกัดเวลา ให้ศัตรูล้าจากการต้องเฝ้าระวังตลอดเวลา ไม่เหมือนกับอินเดียโบราณ เวลารบกัน ก็นกันตรงๆ ตามกติกาไปเลย รบกันเมื่ออาทิตย์ขึ้น แยกกันเมื่ออาทิตย์ตก พรุ่งนี้ค่อยกลับมารบกันใหม่

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

คราวนี้ย้อนกลับมาทางด้านคอมพิวเตอร์กันบ้าง มีการนิยามศัพท์ขึ้นมาคำหนึ่งว่า “guerilla  patch” ซึ่งเป็นการแก้แบบสายฟ้าแลบแบบเดียวกันกับข้างต้น ซึ่งเป็นการเปลี่ยนโค้ดโปรแกรมระหว่างรันกันเลยทีเดียว  ส่วนมากแล้วจะเป็นระดับภาษาเครื่อง คือโปรแกรมของเราที่เราเขียนเป็นภาษาเครื่อง ปกติแล้วมันจะอยู่ใน RAM ซึ่งแรมนั้นแก้ค่าได้อยู่แล้ว

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

คราวนี้มาถึงภาษาระดับสูงบ้าง การแก้โครงสร้างบางอย่างระหว่างเวลารันก็มีเช่นกัน แต่การแก้นั้นไม่ได้แก้กันแบบเป็นล่ำเป็นสันอะไรกันมากมาย และก็ไม่ได้แก้ด้วยเหตุผลดังเช่นในภาษาเครื่อง ก็เลยมีการประดิษฐ์คำขึ้นมาใหม่ ก็คือเราไม่ได้ทำอะไรใหญ่โตเหมือนกอริลล่า เราแก้แบบลิงเล็กๆ จุ๋มๆ จิ๋มๆ จึงเรียกว่า “monkey patch”

รู้ที่มาที่ไปแล้วว่า monkey patch มาจากไหน แต่บางท่านยังคงสงสัยอยู่ดีว่า แล้วมันคืออะไร เอาไว้ทำอะไร ผมบอกท่านด้วยเสียงอันดังได้ว่า เนื้อหาในบทนี้ที่ผมเล่าให้ท่านฟังมาตั้งแต่ต้น นั่นแหละครับเรื่องของ monkey patching

monkey patching ใน Python

money patching ใน Python นั้นก็คือการที่เราสามารถไปเพิ่มเติม field/method ให้แก่ class/object ได้ในภายหลังนั่นเอง ดังที่ผมกล่าวไว้แล้วข้างต้น ขอให้สังเกตให้ดีๆ นะครับ การเพิ่มเติมเหล่านี้เป็นารเเพิ่มตอนเข้าไปเวลารันโปรแกรมนะครับ หรือเวลา runtime นั่นเอง ดังนั้นในระหว่างการรันเราจึงสามารถใช้เงื่อนไขในการจะเพิ่มหรือจะลด fields/methods เมื่อใดก็ได้

การ patch fields/methods เข้าไปนั้น ก็เข้าใจกันได้ง่ายๆ ตรงไปตรงมา นั่นก็คือเพิ่มเติมความสามารถของ object นั้นนั่นเอง แต่ก็มีคำถามที่น่าสนใจว่า โดยปกติแล้วใครจะเป็นคน patch  ใช่เป็นคนที่สร้าง class หรือไม่ ท่านลองคิดดู

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

ในงาน Enterprise Application ทั่วไป แน่นอนครับว่าจะต้องมีระบบสิทธิ ซึ่งผูกกับ class ของพนักงาน หรือจะรวมตัวกันเป็นตัวเดียวกันก็ตามที แน่นอนครับว่า ไม่ว่าท่านจะใช้ framework ตัวไหนก็ตามมาใช้งาน จะไม่มีทางเลยที่มันจะรองรับความต้องการของท่านได้ครบทั้งหมด ท่านก็จะต้องมี fields บาง fields ที่ท่านต้องการเพิ่มเติมเข้าไปในระบบ เช่นชื่อคู่สมรส ชื่อพ่อแม่ ชั้นเงินเดือน ซึ่งแตกต่างกันออกไป

ในการเขียนโปรแกรมแบบกลางเก่ากลางใหม่นั้น เรานิยมใช้ OR-Mapping เป็นชั้นบางๆ เพื่อบังโลกของ RDBMS ออกจาก OO ซึ่งทำให้เราใช้งาน OO ได้เต็มรูปขึ้น เอาหละครับ คราวนี้เรามาลองดูว่าถ้าเราต้องการเพิ่ม fields เข้าไปใน class ลักษณะนี้ เรามีทางเลือกอะไรบ้าง

วิธีแรก ก็ง่ายๆ ครับ framework ส่วนมากในยุคปัจจุับน ได้รับมนตรา Opensource กันอยู่แล้ว ดังนั้นเราจึงมี source code อยู่ในมือที่เราสามารถเข้าไปแก้ไขได้เลย อยากได้ fields อะไรก็เพิ่มเข้าไป (แน่นอนครับ ทำตามมาตรฐานของ OR-Mapping ตัวที่เรากำลังใช้) ถามว่ามันโอเคไหม มันก็โอเคนะครับ ง่ายที่สุด แต่สิ่งที่ต้องแลกก็คือ ถ้าเราเอา framework รุ่นใหม่มาใช้ ของที่เราเพิ่มเข้าไปจะถูกทับ ก็ต้องเหนื่อยทำกันใหม่อีก

คราวนี้วิธีที่ 2 ซึ่งเป็นวิธีที่ยอดนิยม กล่าวคือเราอาจแยกเนื้อหาที่เพิ่มเติมนั้น ออกมาเป็นอีกตารางและอีก class หนึ่ง จากนั้นก็จะใช้แนวคิด OO พื้นฐานอาจจะเป็น association หรืออะไรก็แล้วแต่ รวมทั้ง design pattern ด้วยก็ได้ ข้อดีก็คือได้ความอิสระ ถึงจะเปลี่ยน framework เป็นรุ่นใหม่ ก็ไม่กระทบกระเทือน แต่ข้อเสียก็มีนั่นก็คือ โด้ดที่เขียนมันจะดูเหมือนคนป่วยที่เวลาเดินไปไหน ก็ต้องเดินจูงเสาน้ำเกลือไปดู และการทำความเข้าใจ ก็จะทำได้ยากขึ้น

มาดูวิธีที่ 3 คือ monkey patching บ้าง เราก็เขียนโค้ด patch เอาไว้ตอนต้นของโปรแกรม ซึ่งอ่านมาถึงบรรทัดนี้ก็คงเข้าใจกันดีอยู่แล้ว มาพูดกันเรื่องข้อดีขอเสียเลยดีกว่า ข้อดี ก็แน่นอนครับ เหมือนกับวิธีแรก เรา patch แก้เข้าไปได้เลย และได้ข้อดีตามวิธีที่สองคือ ไม่ว่าจะเปลี่ยน framework เป็นรุ่นต่อไป ก็จะไม่กระทบกับโปรแกรม แต่มันก็มีข้อเสียเหมือนกัน

ข้อเสียก็คือ มันขัดกับกฏเหล็กของ OO คือการอนุญาตให้คนอื่นเข้าถึง fields ของเราโดยตรงได้ ซึ่งในเชิงของ OO ตามมาตรฐาน ถือว่าเป็นเรื่องที่อันตรายมาก มันจะกลายเป็นลิงแก้แห หรือท่านเชื่อใน “We’re all consenting adults here” ตัวท่านเท่านั้นครับที่เป็นผู้ตัดสินใจ

ทิ้งท้าย

ในบทนี้ผมได้นำเสนอเรื่อง monkey patching ซึ่งเป็นเรื่องที่ผมติดค้างเมื่อเวลาเขียนเรื่อง ECMAScript ไปแล้ว หวังว่าท่านคงอ่านด้วยความเพลิดเพลิน แล้วพบกันในตอนที่สอง คราวหน้าจะเป็นเรื่องเฉพาะของ Python เองครับ

[Total: 2    Average: 5/5]

You may also like...

Leave a Reply

Your email address will not be published. Required fields are marked *