Tutorial

Overload Function บน Python ด้วย SingleDispatch

By Arnon Puitrakul - 07 ธันวาคม 2021 - 1 min read min(s)

Overload Function บน Python ด้วย SingleDispatch

บางครั้ง เวลาเราเขีน Script ออกมา มันจะมีเคสที่เราต้องการที่จะมีการแยก การทำงานของ Function ตาม DataType ที่เราใส่เข้ามา บางที บาง DataType เราอาจจะ Process มันได้ตรง ๆ เลย หรือบาง DataType เราจะต้อง Pre-Process ก่อน เมื่อก่อนเราอาจจะใช้วิธีว่า เราก็สร้าง Function แยกกันไปเลยดิ แยกชื่อแยกอะไรกันไปให้หมดเลย แล้วพอมา Optimise หน่อย ถ้ามันมีส่วนที่เหลือมกัน หรือทำงานเหมือนกันอยู่ เราก็ Refactoring ออกมาเป็นอีก Function เข้าไปอีก ทำให้สุดท้ายแล้วเราก็จะมี Funtion เยอะเต็มไปหมด แบบ งง ๆ แล้วพอมา Refactoring หรือแก้ก็คือ รับบทคนจีนหนึ่งเลยนะ รื้อ !!! ทำให้เราอยากจะมานำเสนอ Solution ของปัญหานี้คือการทำ Function Overload

Function Overloading คืออะไร ?

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

def cal_me (a,b) :
    return a + b

def cal_me(a) :
    return a

อยากให้ไปทำความรู้จักกับคำอีกคำนึงคือ Function Signature ไม่รู้นะว่าภาษาไทยเรียกอะไรเหมือนกัน แต่ถ้าเราลองดูที่ Function เราจะเห็น Signature ของมันอยู่ด้านบนสุดเลย นั่นคือ ตัว Function นี้มี Argument อะไรบ้าง นั่นเอง ดังตัวอย่างด้านบน เราจะเห็นว่าชื่อมันเป็นชื่อเดียวกัน

แต่ ๆ จากตัวอย่างนี้ ถ้าเราลองเอาไปรัน แล้วเราเรียก Signature ตัวที่ 1 นั่นคือ รับค่าอะไรเข้าไปไม่รู้ 2 ค่า แล้วพ่นผลบวกของมันออกมา เราจะได้ Error กลับมาแน่นอน เพราะ Python  มันจะ Treat Function เป็นตัวแปรแบบนึงเลย (ชนชั้นสูงหนึ่งงงง) ทำให้ การประกาศ Function ครั้งที่ 2 มันเป็นชื่อเดียวกัน มันเลยไปทับของอันที่ 1 หมดเลย ทำให้ถ้าเราเรียก Signature แบบที่ 2 มันจะรันได้เลย ทำให้การทำแบบนี้ มันไม่ได้เป็นการทำ Function Overload เลย ตัว Python เองเริ่มต้นมันจะไม่ได้ทำให้เรา ต่างจากภาษาอื่น ๆ อย่าง Java ที่เราไม่ต้องไปบอกอะไรมันเลย แค่เราใช้ชื่อเดียวกัน แค่ต่าง Signature มันก็รอดได้

Overload ด้วย SingleDispatch

ในเมื่อเราเขียนปกติแล้วมันไม่ยอม Overload ให้เรา เพราะธรรมชาติของ Python โดยเริ่มต้นมันไม่ได้ออกแบบมาให้เราทำแบบนั้น แต่ Python เองก็ออก Built-in Decorator มาเพื่อ Overload Function ให้เราด้วยละ ไม่ต้องไปหาทำ Install เพิ่มเลย ซึ่งสิ่งนั้นก็คือ SingleDispatch ตามหัวเรื่องของเราเลย

ในตัวอย่างนี้ เราต้องการที่จะเขียน Function สำหรับการบวกเลข โดยที่เราไม่รู้เลยว่า เลขที่เราเอาเข้ามา มันจะเป็น String หรือ Integer หรือ Float ทำให้เราจะต้องแยกการทำงานตาม DataType (เอาจริง ๆ ตัวอย่างนี้อาจจะยังไม่ดีที่สุด เพราะมันมีวิธีที่ดีกว่าในการเขียนอยู่ แต่เพื่อให้เข้าใจง่าย ขอเป็นตัวอย่างนี้ละกันนะ)

from functools import singledispatch

@singledispatch
def add_num (a,b) :
    raise NotImplementError('Not Support DataType')

เราเริ่มจาก การเขียน Function นึงขึ้นมาคือ add_num ภายในนั้นเรา Raise Exception ออกมาเลย เพราะเราต้องการรับแค่ Type ที่เรากำลังจะเขียนเท่านั้น แต่เราสังเกตว่า เรามีการเรียกใช้ Decorator ที่ Function นั่นคือ singledispatch ที่เรา เรียกมาจาก functools ด้านบน

@add_num.register(str)
def _ (a, b) :
    return int(a) + int(b)

@add_num.register(float)
@add_num.register(int)
def _ (a, b) :
    return a + b

จากนั้นเราจะมา Implement ส่วนที่เรา Overload แล้ว เริ่มจากตัวง่าย ๆ ก่อนตัวแรกเลย การที่เราจะบอกว่า เราจะ Overload ตัวไหน เราจะต้องเติม Decorator บอกมันหน่อย โดยการเรียกชื่อ Function นั้น ๆ แล้วเรียก register ขึ้นมา พร้อมกับบอกว่า มันจะเป็น Type อะไร ในที่นี้เราเรียกออกมาเป็น String แล้ว เราก็ทำการ Implement ได้เลย เราทำง่าย ๆ เลย เราก็แปลงเลขให้ String กลายร่างเป็น Int แล้วบวกกันเลย แค่นั้นเลย แต่อยากให้สังเกตอีกเรื่อง ที่ชื่อ Function เราไม่ได้ใส่ชื่อเราใช้ Underscore ไปเลย เราเรียกพวกนี้ว่า Anonymous F'unction มันคือตัวเต็มของพวก Lambda Function ที่เราใช้งานกันนั่นแหละ

ถามว่าทำไม เราถึงใช้แบบนี้ได้ เพราะจริง ๆ แล้วไส้ในของมัน มันจะเอา Function ที่โดน Decorate นี่แหละเข้าไปไว้กับ add_num อยู่ดี ทำให้เวลาเราใช้งานจริง เราไม่ได้สนใจชื่อที่เราสร้างตรงนี้เลย ทำให้เราเลยสร้างแบบไม่ตั้งชื่อละกัน ง่ายดี ก็รู้กันดีว่า Programmer กับการตั้งชื่อมันไม่ถูกกัน ฮ่า ๆๆๆๆๆๆๆ

และอีก Function เราต้องการที่จะ Implement สำหรับ Float และ Int เราเลยทำการ Stack Decorator ซะเลย ตัวนึง Int ตัวนึง Float ได้เลย เราสามารถ Stack เท่าไหร่ก็เอาเลยนะ ได้หมดเลย ส่วนภายในเราก็บวกเลขปกติ ไม่ได้อะไร ที่เอาไปไว้อันที่ 2 เพราะจะบอกว่า มัน Stack กันได้เฉย ๆ ทำให้จริง ๆ แล้วเราสามารถเขียนได้อีกแบบ

from functools import singledispatch

@singledispatch
def add_num (a,b) :
    return a + b

@add_num.register(str) :
    return int(a) + int(b)

จะเห็นได้ว่า Version นี้มันจะแตกต่างกันตรงที่ Base Function ตัวแรก เราไม่ได้บอกให้มันโยน Exception ออกมา แต่เราให้มันเป็น Base จริง ๆ เลย คือให้บวกกันตรง ๆ ตามหน้าที่ของมันเลย ทำให้เราไม่ต้องเขียน 3 Function ยาว ๆ แล้วเอา Decorator ซ้อน ๆ กันให้ยาวโดยใช้เหตุอีกด้วย ถามว่า แบบไหนดีกว่าละ

สำหรับผลลัพธ์อาจจะคล้ายกันนะ แต่ในแง่ของการ Trace และการตรวจสอบต่าง ๆ แล้วเราชอบเขียนแบบแรกมากกว่า เพราะถ้าเราเขียนแบบที่ 2 ถ้าเราไม่ได้เอา String มาใส่ มันก็จะวิ่งไปทำงานบน Function แรกทั้งหมด แล้วถ้าเกิดมันมี Test Case ที่เราโยนอะไรที่มันไม่ใช่ String แล้วไปแล้ว DataType นั้นไม่รองรับการบวกกันขึ้นมาละ งานนี้แหละ เละ แล้วหาสาเหตเหนื่อยแน่นอน แต่ถ้าเราโยน Exception ออกมาเลย มันจะทำให้เราตรวจสอบปัญหาได้ง่ายขึ้นมาก ๆ

Frequent Pitfall

จากเมื่อกี้เราได้เรียนรู้วิธีการใช้งาน SingleDispatch กันไปแล้ว มันสามารถเอาไปใช้ได้ในหลาย ๆ เคสมาก ๆ แต่มันจะมี Pitfall นึงที่เราเจอเยอะมาก ๆ โดยเฉพาะคนที่เขียนภาษาอื่น ๆ ที่มี Function Overloading มาอย่างเช่น Java เป็นต้น

add_num('1', 2)

จากตัวอย่างที่แล้ว ถ้าเราเรียก Function ด้วย Argument แบบนี้คิดว่า มันจะไปเรียกตัวไหนกัน เพราะตัวนึง มันเป็น String อีกตัวเป็น Int แต่ที่เรา Implement ไว้มันเป็น Int ทั้งหมดเลย ไม่ก็ String ทั้งหมดเลย นิน่า มันจะเรียกตัวไหนออกมา

คำตอบคือ มันจะเรียกตัวที่เป็น String ออกมา เพราะ จริง ๆ แล้ว SingleDispatch มันสนใจแค่ Argument ตัวแรกเท่านั้น ตัวที่เหลือมันไม่ได้สนใจเลย ทำให้ถ้าเรามีเหตุการณ์ประมาณนี้ เราอาจจะต้องมีการเช็คเพิ่มนิดนึง เพื่อให้ปลอดภัยมากขึ้น หรือไม่ก็เขียน Dunder Function ครอบไว้เพื่อแปลงของก่อนที่เราจะยัดเข้ามาก็ทำให้รอดได้ง่ายเหมือนกัน

ทำให้ ตัว SingleDispatch มันเป็นการทำ Overloading ที่ไม่เหมือนกับภาษาอื่น ๆ ที่รองรับโดยตรงเท่าไหร่ เพราะมันเช็คตัวเดียว แค่ตัวแรกเท่านั้น แต่ภาษาอื่นมันอ่านทั้ง Signature เลย ทำให้เป็นเรื่องที่เรียกได้ว่าเป็น Frequent Pitfall สำหรับการใช้งานเลย โดยเฉพาะคนที่ชินมาจากภาษาอื่น ๆ

สรุป

SingleDispatch เป็น Built-in Decorator ตัวนึงที่ทำให้เราสามารถทำ Function Overloading ได้ง่าย ๆ โดยที่เราไม่ต้อง Install อะไรทั้งนั้น เพียงแค่ Import เข้ามา แล้ว Decorate ลงไปใน Function ที่เราต้องการ แล้วเขียน Function เข้ามาแล้ว Decorate ด้วย register Method แต่ข้อระวังคือ มันจะเช็คแค่ Argument ตัวแรกเท่านั้น ทำให้ถ้าเรามีหลายตัวอาจจะต้องเช็คเองเพิ่มหน่อยน่าจะทำให้ปลอดภัยขึ้นมาก ซึ่ง SingleDispatch สามารถนำไปใช้งานได้ในหลาย ๆ สถานการณ์มาก ๆ ทำให้เราไม่ต้องแยกชื่อ Function เยอะ ๆ ที่ทำให้การดูแลรักษา การจัดการยากมาก ๆ เรียกชื่อเดียวแล้วจบ เดี๋ยวมันไปเรียกอันที่ถูกต้องมาให้เราเอง ลองเอาไปเล่นกันได้ ~