Tutorial

Dunder/Magic Method บน Python (ตอน 2)

By Arnon Puitrakul - 04 พฤศจิกายน 2021 - 2 min read min(s)

Dunder/Magic Method บน Python (ตอน 2)

หลังจากรอบก่อนหน้าเรามาทำความรู้จักกับ Dunder หรือบางคนเรียกว่า Magic Method บน Python ไปแล้ว ที่ทำให้การเขียน Class ของเราสนุก และง่ายขึ้นกว่าเดิมมาก ผ่านการ Override Built-in Method ที่มากับ Python เลย วันนี้เราจะพาไปลองดูตัวที่ทำให้ล้ำกว่าตัวก่อนหน้า ผ่านตัวอย่างง่าย ๆ อย่างการสร้าง Class สำหรับการเก็บ Transaction ของบัญชีธนาคารละกันง่าย ๆ

Our Example

class BankAccount :
    def __init__ (self, account_name:str, starting_deposit=0) :
        self.account_name = account_name
        self.transactions = []
        
        if starting_deposit > 0 :
            self.transactions.append(starting_deposit)

    def deposit (amount:float) :
        self.transactions.append(amount)
    
    def withdraw (amount:float) :
        self.transactions.append(amount * -1)
    
    def get_amount () -> float:
        amount = 0
        for transaction in self.transactions :
            amount += transaction
        return amount

เราสร้างเป็น Class ง่าย ๆ ตัวนึงละกัน สำหรับเก็บบัญชีธนาคาร ซึ่ง Constructor เราก็จะรับชื่อบัญชี และ ถ้าเรามีเงินแรกเข้า เราก็ใส่เข้าไปมาด้วย ซึ่งในเงินฝากครั้งแรก เราก็จะเพิ่มลงไปใน Transaction ที่เก็บเป็น List ได้เลย

นอกจากนั้น เรายังสร้าง Method สำหรับการฝาก และ ถอนด้วย ซึ่งการฝากก็จะตรงตัวเลยคือ เราก็เพิ่มตัวเลขลงไปใน List ของ Transaction ได้เลย ส่วนการถอน เราก็ทำง่าย ๆ เหมือนการฝากเลย แต่เราก็ทำให้ค่าเงินมันติดลบด้วยการคูณด้วย -1 เข้าไปก็จะได้แล้ว อันนี้เรารู้อยู่แล้วนะว่ามันจะมีเคสที่แอ๊บแบ๊วได้ เช่น การฝากด้วยเลขติดลบ แต่วันนี้เราเราโฟกัสที่เรื่อง Dunder เนอะ เลยขอข้ามเรื่องนี้ไป เราไม่แก้นะ

__add__

>>> "Hello" + " " + "World"
"Hello World"

เริ่มจาก Method แรกกันก่อนเลย คือ add หรือสั้น ๆ ก็คือ บวก นั่นเอง ถ้าเราจำกันได้ว่าใน Python เราสามารถใช้เครื่องหมายบวกได้กับหลาย Data Type และ Class มาก ๆ อย่างในตัวอย่างด้านบน เราจะเห็นว่า เราเอา String 3 ตัวมาบวกรวมกัน มันก็คือการ Concatenate หรือก็คือการเอา String มาต่อกันเลยนั่นเอง

>> [2,10] + [5]
[2, 10, 5]

หรืออีกตัวอย่างเราสามารถที่จะเอา List มาบวกกันได้ตรง ๆ เลยอีกเหมือนกัน ซึ่งมันก็คือการเอา Element ใน List มาต่อกันนั่นเอง ทำให้เราจะเห็นได้ว่า จริง ๆ แล้วการบวก มันน่าจะมีอะไรกลไกอะไรบางอย่างซ่อนอยู่ที่ไม่ใช่แค่การ บวกเลข อย่างที่เราเข้าใจแน่ ๆ ใช่แล้ว มันคือการ Override add method นั่นเอง

def __add__ (self, new_amount) :
    if type(new_amount) is int or type(new_amount) is float :
        self.deposit(new_amount)

กลับมาที่ตัวอย่างของเราบ้าง เราอาจจะอยากให้ ถ้าเราเอา Object ของบัญชีเข้าไปบวกกับ เลขจำนวนนึงเป็นการฝากเงิน เราก็สามารถ Implement add method แบบด้านบนได้เลย ถ้าเราไม่คิดอะไรมาก เราก็อาจจะให้เอาค่าใหม่ที่ใส่เข้ามาเพิ่มลงไปใน Transaction ได้เลย แต่เราก็ทำให้ง่ายกว่านั้นด้วยการ Reuse deposit Method ที่เราเคยเขียนไว้ได้เลย ก็จะทำให้เราไม่ต้องเขียน Code ซ้ำซ้อน แต่ถ้าเราทำแค่นั้น เราก็จะเจอปัญหาใหม่ได้

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

>>> account_1 = BankAccount("Tom")
>>> account_1.get_amount()
0
>> account_1 + 20
>> account_1.get_amount()
20

เมื่อเรา Implement Dunder Method อย่าง add ไปแล้ว เมื่อเราสร้าง Account ขึ้นมา รอบแรก เราขอดูจำนวนเงินที่มีอยู่ มันก็จะเป็น 0 เพราะเรายังไม่ได้เพิ่มเงินเข้าไปในบัญชีเลย เราเลยลองเพิ่มเข้าไปสัก 20 บาทละกัน ผ่านการเอา 20 เข้าไปบวกกับบัญชีเลย และเมื่อเราเช็คยอดเงินคงเหลืออีกครั้ง เราก็จะเห็นว่า 20 บาทที่เราบวกไปมันเข้ามาแล้วนั่นเอง

__sub__

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

def __sub__ (self, new_amount) :
    if type(new_amount) is int or type(new_amount) is float :
        self.withdraw(new_amount)

เราก็ทำเหมือนเดิมเลย คือการเช็คก่อนว่ามันเป็นตัวเลขหรือยัง และ ถ้าใช่แทนที่เราจะฝาก เราก็ถอนเงินออกไปเท่านั้นเอง

>>> account_1 - 10
>>> account_1.get_amount()
10

เมื่อ Implement ไปแล้ว เราลองมาถอนเงินออกไปกันดู เรามีอยู่ 20 จากรอบก่อน และเราหักไป 10 ทำให้เรา จะต้องเหลืออยู่ 10 ก็จะเป็นเหมือนในตัวอย่างด้านบนเลย

จะเห็นได้ว่ามีทั้งบวก และ ลบ ใช่แล้ว อย่างที่คิดเลย มันมีทุกเครื่องหมายให้เรา Override ได้หมดเลยนะเช่น mul สำหรับการคูณ และ truediv สำหรับการหารเป็นต้น

__lt__ และ __gt__

หลังจากเราบวกและลบมาได้แล้ว เราลองมาเปรียบเทียบกันบ้างดีกว่า ใน Python เราก็จะมีเครื่องหมายสำหรับการเปรียบเทียบอยู่หลายตัวด้วยกันคือ <,>, <=, >= และ == ซึ่งแน่นอนว่าปกติแล้ว เราก็ไม่สามารถที่จะ เปรียบเทียบ Object ทั่ว ๆ ไปกับ Object ได้ดังนั้น Python เลยทำให้เราสามารถที่จะ Override ได้เหมือนตอนเราบวกและลบเลย ตัวอย่างเช่น น้อยกว่า ก็ใช้ lt หรือ Lower Than กับ มากกว่า ก็จะเป็น gt หรือ Greater Than

def __lt__ (self, other) :
    if isinstance(other, self.__class__):
           return self.get_amount() < other.get_amount()
       return False

def __gt__ (self, other) :
    if isinstance(other, self.__class__):
           return self.get_amount() > other.get_amount()
       return False

ในการเขียนเปรียบเทียบ เราขอเป็นการเทียบละกันว่าบัญชีไหนเงินเยอะกว่า หรือน้อยกว่า ซึ่งการจะเรียกจำนวนเงินเข้ามาได้ เราจะต้องเรียกผ่าน Method ที่เราสร้างกันไว้ที่ชื่อว่า get_amount ซึ่งมันไม่มีใน Class อื่นแน่ ๆ ทำให้เราต้องเข้ามาเช็คก่อนว่า Object นี้มันเป็น Class เดียวกัน หรือก็คือ BankAccount หรือไม่ ถ้าใช่ เราก็สามารถคืนค่ากลับไปเป็น Boolean ที่เกิดจากการเปรียบเทียบ น้อยกว่า หรือมากกว่าได้เลยนั่นเอง

>> account_2 = BankAccount("Sia O", 1000)
>> account_1 > account_2
False

ในการทดสอบ เราก็ลองสร้าง Account ขึ้นมาใหม่ดู ให้มีเงิน 1,000 ไปเลย และเราก็เอามาเปรียบเทียบเลย ระหว่าง account_1 ที่เงินเหลืออยู่ 10 กับ account_2 ที่เราพึ่งสร้างไป แล้วเราก็ลองเปรียบเทียบดูเลย เราจะเห็นว่า มันก็จะได้ออกมาแบบที่เราเขียนไว้เลย แต่ถ้าเราลองเช็คเท่ากับละ มันก็จะ Error เพราะเรายังไม่ได้ Override ตัว eq Method นั่นเอง ถ้าเราอยากจะทำให้มันเช็คได้ เราก็แค่ไป Implement เราก็จะใช้ได้แล้วง่าย ๆ เลย

__del__

แล้วถ้าเราต้องการที่จะลบ Account ของเราละ เราจะทำยังไงได้บ้าง ปกติแล้วใน Python ถ้าเราต้องการที่จะลบตัวแปรอะไรบางอย่างออกไป เราสามารถใช้คำสั่ง del ได้เลย ซึ่งเอาจริง ๆ แล้วเหมือนเดิมเลย del มันก็จะไปเรียก Method ข้างในอีกทีนึง ซึ่งมันก็คือ __del__ นั่นเอง

def __del__ (self) :
    self.transactions = []
    print("Account " + self.account_name + " has been deleted")

ในการลบจริง ๆ เราก็ทำไม่ยากเลย คือ เราก็แค่ Clear Transaction ออกให้หมด หรือก็คือทำให้เป็น List ว่าง และอาจจะมีการ Print ออกทางหน้าจอหน่อยว่า โอเค เราทำการลบ Account ออกแล้วนะ แค่นั้นเลย

>>> del account_2
Account Sia O has been deleted

>>> account_2
NameError: name 'account_2' is not defined

ถ้าเราลองดู ตอนแรกเรารู้อยู่แล้วว่า account_2 มันถูกสร้างไว้ก่อนหน้านี้ ในตัวอย่างนี้เราเลยทำการลบ account_2 ดูว่ามันจบจริงมั้ย เมื่อเราสั่งลบไป มันก็จะมีบอกว่า Account โดนลบไปแล้วตามที่เราเขียนไว้เลย และเมื่อเราลองเรียก account_2 หลังจากที่เราลบไปแล้ว เราจะได้ Error กลับมาบอกว่ามันไม่มี Account 2 อยู่ในนั้น แต่ใน del method เราแค่เอา Transaction ออกนิ เราไม่ได้ ลบออกไปเลยนิ ที่เป็นแบบนี้เป็นเพราะสิ่งที่ Python ทำคือ มันจะทำการเรียก __del__ และทำงานไปจากนั้นมันก็จะเอาออกไปเลย ทำให้เราเข้าถึงไม่ได้อีกนั่นเอง

__repr__

และสุดท้าย เราอยากให้ถ้าเราเรียกแค่ Object เฉย ๆ เราต้องการให้มันแสดงรายละเอียดของบัญชี พร้อมกับยอดเงินที่มีอยู่ออกมา เราก็สามารถทำได้ผ่านการ Override Method ที่ชื่อว่า repr เราว่าหลาย ๆ คนอาจจะไม่คุ้นเคยกับชื่อของ Method ตัวนี้ แต่มันเป็นตัวที่เราใช้กันเยอะมาก ๆ เช่น เมื่อเราสร้าง String ขึ้นมา โดยเฉพาะเมื่อเราอยู่บน Interactive Shell เราพิมพ์ชื่อ String ขึ้นมาโต้ง ๆ เลย มันก็จะออกมาเป็น String ที่เราใส่เข้าไป นั่นแหละคือมันไปเรียก repr ของ String เลย เราก็สามารถที่จะ Override Method นี้ได้เหมือนกัน

def __repr__ (self) :
   return repr(self.account_name + " with Amount " + str(self.amount) + " THB")

เราก็แค่ Implement คำสั่งง่าย ๆ กลับไปเลยให้มันคืนค่ากลับไปเป็น Repr ที่ในนั้นเราจะเป็น String ประกอบด้วย ชื่อบัญชี และ จำนวนเงินที่เรามี

>>> account_1
Tom with Amount 10 THB

เมื่อเราลองเรียก account_1 ขึ้นมาดู เราก็จะเห็นว่า มันก็จะได้ String ที่เรากำหนดไว้ใน repr Method เป๊ะ ๆ เลย ก็ทำให้เราสามารถใช้งาน Object ได้ง่ายขึ้นเยอะมาก ๆ เป็นคำสั่งที่เราใช้ไม่บ่อย แต่ใช้ทีมันจะ Powerful มาก ๆ เลย

สรุป

Dunder Method ที่เราเอามาแชร์กันในวันนี้ก็แน่นอนว่า ยังเป็นแค่ส่วนหนึ่งของ Dunder Method ที่ Python มีให้เราใช้ แต่หมวดหมู่ในตอน 1 และ 2 เรานำเสนอน่าจะครบแล้วละว่ามันน่าจะทำอะไรได้บ้าง อาจจะต้องไปเปิด Document เพิ่มหน่อยว่ามันมีอะไรที่เราต้องการใช้มั้ย ถ้ามีแนะนำว่าใช้ไปเลย เพราะมันทำให้การทำงานเราง่ายขึ้นมาก ๆ อย่างไม่น่าเชื่อเลย ตอนแรกที่เรารู้จัก และได้ลองใช้ก็คือมันสุด ๆ ไปเลย การเขียน Script สะอาดขึ้นมากจริง ๆ แนะนำเลย ลองแล้วจะติดใจ