Instance Creation บน Python ไส้มันเป็นยังไง

หลังจากเราเอาเรื่องของ Dunder Method มาเล่ากันเมื่อไม่นานมานี้ ทำให้มีคนถามเราเข้ามาในเรื่องของ new และ init ที่เป็นหนึ่งใน Dunder Method ที่เราใช้ในในการสร้าง Instnace หรือ Object บน Python นั่นเอง วันนี้เลยจะมาอธิบายเพิ่มเติมกันว่า ทั้ง 2 Methods นี้มันต่างกันอย่างไร การสร้าง Instance ใน Python มันทำยังไง และเราจะใช้งานมันได้อย่างไรบ้าง

__init__()

เริ่มจากตัวที่เราใช้งานกันบ่อย ๆ ก่อนอย่าง Init หรือบางครั้งเราก็จะเรียกมันว่า Constructor ก็ได้เหมือนกัน โดยที่ Method นี้มันจะถูกเรียกเป็น Method แรกหลังจากที่ Instance ของเราถูกสร้างเรียบร้อยแล้ว โดยที่เราอาจจะมีการเรียกคำสั่งสำหรับการ Init ค่าอะไรบางอย่างก็ได้ เช่นการเพิ่ม Attribute ต่าง ๆ ลงไป

class Customer :
    def __init__ (self, name, surname) :
        self.name = name
        self.surname = surname
        
        print(self.__dict__)

ตัวอย่างด้านบน เราทำการสร้าง Class Customer ขึ้นมา โดยที่เราเรียก init ให้ทำการรับค่าเข้ามาเป็น name และ surname เข้ามา แล้วให้มันเอาไปใส่ใน Attribute ของมัน จากนั้น เพื่อเป็นการเช็คว่า มันเข้าไปอยู่ใน Attribute จริง ๆ เราก็เลยเรียกคำคำสั่ง Dict ออกมา

{'name': 'My name', 'surname' : 'My Surname'}

คำสั่ง Dict นี้ก็เป็น Dunder Method ตัวหนึ่งเหมือนกันที่โดยค่าเริ่มต้น มันจะพ่น Attribute ออกมาเป็น Dictionary ออกมาให้เราได้เลย จากตัวอย่าง เราทดลองสร้าง Object ออกมา เราก็จะเห็นได้เลยว่า มันก็จะพ่น Dictionary ออกมาให้เรา แปลว่า Constructor มันทำงานแน่นอน

แต่เราอยากให้สังเกตตรงที่ว่า init สิ่งที่มันทำจริง ๆ คือ มันจะทำการ Mutate ตัวเอง โดยที่ไม่ได้มีการคืนค่าอะไรกลับไปเลย เราไม่เคยเห็น init ที่คืนค่ากลับไปแน่ ๆ ทำให้เกิดคำถามว่า เอ๊ะ แล้วถ้า Init มันไม่ได้ร้าง Object ออกมา แล้วใครเป็นใครสร้างละ

__new__()

นั่นทำให้เราจะต้องมาทำความรู้จัก Dunder Method อีกหนึ่งตัวคือ new นั่นเอง โดยที่มันมี Signature คือ มันจะต้อง Return Object ของ Class นั้น ๆ กลับไป พร้อมกับมีการรับ Argument เข้ามาด้วย

def __new__(cls, *args, **kwargs):
        obj = object.__new__(cls)
        return obj

จากด้านบน เป็นตัวอย่างที่ง่ายที่สุดสำหรับการ Override new Method ด้วย Default Method นี่แหละง่าย ๆ เลย สั้น ๆ คือ เรารับเข้ามาว่า Class ที่เราจะสร้างมันคืออะไร ส่วน Argument อื่น ๆ เราเขียนเข้ามาเฉย ๆ เป็น Pattern ยังไม่ต้องไปสนใจนะ เดี๋ยวจะ งง จากนั้น เราก็จะเข้าไปสร้าง Instance ขึ้นมาจริง ๆ ละ ผ่าน new Method ของ object อีกที หรือจริง ๆ แล้ว เพื่อให้ อ๋อ เข้าไปอีก เราสามารถแทนที่มันได้ด้วย super().__new__(cls) (เราจะบอกว่าเขียนแบบนี้ดีกว่านะ เพราะว่า ไม่ว่ายังไง แม่มันก็คือ object แน่นอน แต่ถ้าเราเขียนแบบในตัวอย่าง วันนึงคน Maintain Python บอกว่า ไม่เอาละ ไม่อยากให้ชื่อ object แล้วอยากเปลี่ยนเป็นชื่ออื่น เขียนแบบตัวอย่างแตกได้ ใช้แบบที่เราบอกเมื่อครู่รอดง่ายกว่าเยอะ) ได้ เอ๊ะอะไรมั้ยฮ่ะ ใช่แล้ว จริง ๆ แล้ว Object ทั้งหมดที่เราเห็นใน Python มันมีแม่คนเดียวกัน คือ Class ที่ชื่อว่า object

>>> object
<class 'object'>

ถ้าเราลองเช็คว่า object มันเป็นอะไร มันก็จะบอกว่า อ่อ ชั้นเป็น Class แหละ ซึ่งจริง ๆ เราเรียกมันว่า แม่ทุกสถาบันได้เลย เพราะมันเป็นต้นกำเนินแห่ง Object ทั้งปวงใน Python ซึ่งคำสั่ง New ของมัน ก็จะรับ Class เข้าไป แล้วก็จะพ่น Object ของ Class ที่เราใส่เข้าไปกลับมาให้เรานั่นเอง ทำให้เราสามารถเอาตัวแปร obj ไปรับในตัวอย่างก่อนหน้า และ Return กลับไปได้เลยนั่นเอง

กับเราจะบอกว่า จริง ๆ แล้วลองไปเช็คดูได้ new ของ Object กับ new ใน Object ที่เราสร้างมันเป็นตัวเดียวกันเลย

มันทำงานสัมพันธ์กันยังไง ?

อ่านมาแล้ว เออ มันดูเหมือนจะเป็นคนละเรื่องกันเลยนะ อื้ม... ก็ใช่แหละ เพราะมันทำหน้าที่คนละอย่างกัน self เขาทำหน้าที่ในการ Init Object ของเราให้พร้อมใช้งานต่าง ๆ อะไรก็ว่ากันไป แต่ตัวที่สร้าง Object จริง ๆ มันคือ new แต่ถึงแม้ว่า มันจะทำงานกันคนละหน้าที่ แต่มันก็มีความสัมพันธ์กันอยู่อย่างแนบเนียน

เวลาเราเขียน Method ต่าง ๆ สงสัยมั้ยว่า self มันมาจากไหน ??? ทำไม เราไม่เขียนเราจะเรียก Method แล้วพัง มันจะบอกว่า Function นี้รับ Argument 2 ตัว เราใส่เข้ามาตัวเดียว ตอนเราใส่เราก็ใส่เข้ามาตัวเดียวเหมือนใน Signature ที่เราเขียนเลยนะ เอ๊ะอะไรของมัน !!!!

จริง ๆ แล้ว Self ก็ตามชื่อเลย มันก็คือตัว Object เอง ที่พ่นออกมาจาก new Method นี่ละ ตัว Python มันจะต้อง Inject self เข้าไปในทุก ๆ ที่ เพื่อให้เรายังคงเรียกตัวเองได้ ซึ่งมันก็คือตัวเดียวกับ Object ที่เราเรียกจากข้างนอกเลย ตัวอย่างง่าย ๆ

class Employee :
     def __init__ (self, name:str) :
         self.name = name
 
     def get_me (self) :
         return self
 
 emp_a = Employee("Alice")
 print(id(emp_a), id(emp_a.get_me()))

เราสร้าง Class ง่าย ๆ ขึ้นมาตัวนึง โดยที่เราสร้าง Method ตัวนึงคือ get_me สิ่งที่มันทำคือ เราขอ Self ออกมาเลย แล้วเราก็เอา Class นี้แหละไปสร้างเป็น Object ออกมา เราขอ ID ของ emp_a ซึ่งก็คือ Object ที่เราสร้างจาก Class Employee และ self ที่อยู่ใน Object ของ emp_a ผ่าน Method get_me ที่เราสร้างไว้ก่อนหน้านี้

4337806688 4337806688

ผลที่ได้ เราจะเห็นว่า มันเป็นตัวเลขชุดเดียวกันเลย นั่นแปลว่าจริง ๆ แล้ว Object ที่เราเรียกอยู่ข้างนอกนั่นทั้งหมด มันคือตัวเดียวกับ Self ที่อยู่ใน Class ที่เราอ้างถึง 100% เลย แน่นอน เพราะมันคือตัวเดียวกันยังไงละ นั่นแปลว่าอะไร ?

นั่นแปลว่า จริง ๆ แล้ว มันนำไปสู่การตอบคำถามเรื่อง ไก่กับไข่อะไรเกิดก่อนกัน ไม่ใช่ละ แต่ก็ใส่เคียง มันทำให้เราเห็นได้เลยว่าจริง ๆ แล้วในการทำงานของการสร้าง Instance จริง ๆ มันจะเริ่มไปเรียก new Method ที่อยู่ใน Class นั่นแหละ แล้วก็ไปเรียก new ที่อยู่ใน Class object เพื่อที่จะพ่น self หรือก็คือตัว Object จริง ๆ ออกมา จากนั้น Constructor อย่าง init ถึงจะทำงานเป็นไม้ต่อ เพื่อทำการประกาศค่าเริ่มต้นของ Object นั้น ๆ ตามที่เรากำหนดไว้นั่นเอง

สรุป

ดังนั้น ในวันนี้เราสามารถแยกความแตกต่างระหว่าง การสร้าง และ การ Init Object ได้แล้วผ่าน Dunder Method อย่าง new และ init ที่เราคุ้นเคยกันเป็นอย่างดี โดยที่ new จะทำหน้าที่ในการสร้าง instance ขึ้นมาจริง ๆ ผ่านการไปเรียก new จาก object หรือ super() ที่เป็น Class แม่ของมัน จากนั้นค่อยพ่น Class กลับมา แล้วค่อยไปเรียก init อีกรอบเพื่อทำการ Initiate ตามที่เราต้องการนั่นเอง