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

ภาพประกอบจากคลังภาพสต็อก ไม่ใช่ภาพจากเหตุการณ์จริง
- ใน Python ตัวแปรไม่ใช่กล่องเก็บค่า แต่เป็นป้ายชื่อที่ชี้ไปยังอ็อบเจกต์ในหน่วยความจำ
- การแก้ไขข้อมูลแบบ Mutable (เช่น list) ส่งผลกระทบต่อทุกคนที่ใช้ข้อมูลร่วมกัน
- การรีบาวด์ชื่อตัวแปร (เช่น n = ...) จะเปลี่ยนผลเฉพาะชื่อในขอบเขตนั้นเท่านั้น
- CPython มีการแคชจำนวนเต็มล่วงหน้าในช่วง -5 ถึง 256 ซึ่งใช้งานจริงในหน่วยความจำ
ในภาษาโปรแกรมมิ่งส่วนใหญ่ คุณมักจะได้เรียนรู้ตั้งแต่เนิ่นๆ ว่าค่าและตัวแปรนั้นแยกออกจากกัน โดยมองว่าตัวแปรเป็นกล่องที่มีป้ายชื่อและเราหยอดค่าลงไปข้างใน แต่ภาษา Python ได้พลิกแนวคิดนั้นเงียบๆ ใน Python ทุกสิ่งคืออ็อบเจกต์ ไม่ว่าจะเป็นตัวเลขจำนวนเต็ม, สตริง, ลิสต์, ฟังก์ชัน แม้กระทั่งคลาสเอง และตัวแปรไม่เคยเป็นกล่อง แต่มันคือชื่อที่ชี้ไปยังอ็อบเจกต์ที่อาศัยอยู่ ณ ที่ใดที่หนึ่งในหน่วยความจำ
แนวคิดเพียงข้อเดียวนี้สามารถอธิบายช่วงเวลาที่ชวนให้สงสัยว่า เกิดอะไรขึ้น ได้อย่างน่าประหลาดใจ เช่น ทำไมการเปลี่ยนแปลงลิสต์หนึ่งจึงดูเหมือนไปเปลี่ยนอีกลิสต์หนึ่ง ทำไมการเช็คเงื่อนไขด้วย a is b บางครั้งจึงเป็น True และบางครั้งเป็น False แม้ว่าค่าจะเท่ากัน และทำไมฟังก์ชันจึงสามารถแก้ไขลิสต์ของคุณได้แต่ทำกับตัวเลขจำนวนเต็มไม่ได้ เรามาคลายปมเหล่านี้กันทีละประเด็น
ทุกอ็อบเจกต์ใน Python จะมีคุณสมบัติหลักสองประการที่เราสามารถสอบถามได้เสมอคือ มันคืออะไร และมันอยู่ที่ไหน ซึ่งความแตกต่างนี้เองคือจุดที่มักทำให้หลายคนสับสนเมื่อเริ่มต้นเขียนโค้ด
เมื่อส่งอ็อบเจกต์แบบ Immutable เข้าไปเป็นอาร์กิวเมนต์ ตัวเรียกใช้งานจะไม่ได้รับผลกระทบใดๆ ดังตัวอย่างโค้ดด้านล่างนี้
- ฟังก์ชัน increment(n) จะทำการเพิ่มค่า n ขึ้น 1 ซึ่งเป็นการผูกชื่อตัวแปรระดับท้องถิ่น (local name) เข้ากับอ็อบเจกต์ตัวเลขใหม่
- ค่าของตัวแปร a ฝั่งผู้เรียกใช้งานจึงยังคงเดิมไม่เปลี่ยนแปลง

ภาพประกอบจากคลังภาพสต็อก ไม่ใช่ภาพจากเหตุการณ์จริง
ในทางตรงกันข้าม หากส่งอ็อบเจกต์แบบ Mutable เข้าไปและมีการกลายพันธุ์ในที่เดิม (Mutated in place) ผู้เรียกใช้งานจะเห็นการเปลี่ยนแปลงที่เกิดขึ้นทันที
- การเรียกใช้เมธอด .append(4) กับลิสต์ จะเป็นการเปลี่ยนแปลงอ็อบเจกต์ลิสต์เดิมตัวเดียวกัน
- ตัวแปร l ที่ถูกส่งเข้าไปจะแสดงผลลัพธ์เป็น [1, 2, 3, 4]
อย่างไรก็ตาม หากส่งอ็อบเจกต์แบบ Mutable เข้าไป แต่มีการรีบาวด์ชื่อตัวแปรภายในฟังก์ชัน ผู้เรียกใช้งานก็จะไม่ได้รับผลกระทบเช่นกัน
- การกำหนดค่า n = v ภายในฟังก์ชัน assign_value จะเป็นการรีบาวด์เฉพาะชื่อในขอบเขตท้องถิ่นเท่านั้น
- ตัวแปร l1 จึงยังคงไม่เปลี่ยนแปลงค่า ถึงแม้ว่าจะมีการส่ง l2 เข้ามาทางพารามิเตอร์ก็ตาม
กฎเหล็กที่ควรจำให้ขึ้นใจคือ การทำให้อ็อบเจกต์เกิดการเปลี่ยนแปลง (เช่น การเรียกใช้เมธอดอย่าง .append หรือการกำหนดค่าผ่านอินเด็กซ์) จะส่งผลกระทบต่อทุกคนที่ใช้งานร่วมกัน แต่การรีบาวด์ชื่อตัวแปรใหม่ (เช่น n = ...) จะส่งผลกระทบเฉพาะชื่อนั้นๆ เท่านั้น
ในทางวิศวกรรมซอฟต์แวร์ ความเข้าใจเรื่องการส่งผ่านค่า (Pass by assignment) ใน Python เป็นหัวใจสำคัญที่ช่วยป้องกันข้อผิดพลาดประเภท Side Effects ที่มักเกิดขึ้นโดยไม่รู้ตัว เมื่อตัวแปรหลายตัวอ้างอิงถึงข้อมูลโครงสร้างชุดเดียวกัน (Aliasing) การปรับแต่งข้อมูลผ่านตัวแปรตัวหนึ่งจะสะท้อนไปยังจุดอื่นๆ ทั้งหมด การแยกระหว่างการเปลี่ยนสภาพอ็อบเจกต์ (Mutation) กับการเปลี่ยนการอ้างอิงชื่อ (Rebinding) จึงเป็นทักษะที่แยกนักพัฒนามือใหม่ออกจากมืออาชีพ
อีกจุดหนึ่งที่เรื่องของตัวตน (Identity) มักสร้างความประหลาดใจให้กับโปรแกรมเมอร์คือกลไกการแคชตัวเลขขนาดเล็ก โดยขนาดของแคชดังกล่าวถูกกำหนดไว้ในซอร์สโค้ดของ CPython ผ่านค่าคงهاสองตัวในไฟล์ Objects/longobject.c ซึ่งร่วมกันแคชตัวเลขจำนวนเต็มในช่วง -5 ถึง 256 รวมเป็นจำนวน 5 + 257 = 262 อ็อบเจกต์ที่ถูกจัดเตรียมไว้ล่วงหน้า
อย่างไรก็ตาม คุณควรระลึกไว้เสมอว่าไม่ควรพึ่งพาพฤติกรรมนี้ในโค้ดของคุณ เพราะแคชตัวเลขจำนวนเต็มขนาดเล็กนี้เป็นเพียงรายละเอียดเชิงการ ใช้งาน (Implementation detail) ของ CPython เท่านั้น ให้ใช้เครื่องมือเปรียบเทียบค่าด้วย == และสงวนการใช้ is ไว้สำหรับการตรวจสอบตัวตน เช่น x is None เท่านั้น
สำหรับ Tuples และ frozensets ถือเป็นคอนเทนเนอร์แบบ Immutable ซึ่งคุณไม่สามารถเพิ่ม ลบ หรือแทนที่ธาตุภายในได้ แต่มีความซ่อนเร้นที่ต้องระวังคือ ความไม่เปลี่ยนแปลงของตัวคอนเทนเนอร์ไม่ได้ทำให้เนื้อหาข้างในไม่เปลี่ยนแปลงตาม ทัปเปิลสามารถถืออ็อบเจกต์แบบ Mutable เอาไว้ได้ และอ็อบเจกต์นั้นก็ยังคงสามารถเปลี่ยนแปลงได้เช่นกัน นอกจากนี้ควรสังเกตเรื่องความแปลกประหลาดของทัปเปิลว่างและทัปเปิลที่มีสมาชิกตัวเดียวด้วย
สรุปสั้นๆ คือจดจำไว้ว่าให้ใช้ == สำหรับเปรียบเทียบค่า และใช้ is สำหรับตรวจสอบตัวตน เพียงเท่านี้คุณก็จะเขียนโค้ดที่ทำงานได้ตรงตามความคาดหมายทุกประการ
ที่มา: Dev.to
พบข้อมูลผิดพลาดในบทความนี้? แจ้งปัญหาบทความนี้
ความคิดเห็น
แสดงความคิดเห็น