By Arnon Puitrakul - 31 สิงหาคม 2021
เมื่อหลายวันก่อนมีน้องถามเรื่องของ Functools ใน Python เลยทำให้นึกถึงความเป็น First Class Citizen ของ Function ใน Python ว่ามันเป็นชนชั้นที่ความสามารถเยอะมาก ๆ เราสามารถ Pass เป็น Argument เข้าไปซ้อน ๆ Function อีกทีได้ แต่ Feature ตัวนึงที่เจ๋งมาก ๆ และหลาย ๆ คนมองข้ามไปคือ Decorator
Decorator มันเป็น Pattern หรือท่านึงใน Python ที่ทำให้เราสามารถเพิ่มความสามารถหรือ เปลี่ยนอะไรบางอย่างกับ Function หรือ Object ที่เรามีอยู่แล้ว โดยที่เราไม่ต้องเข้าไปแก้ข้างในเลย ถ้าคิดง่าย ๆ มองว่า Function หรือ Object เป็นเค้กก้อนหนึ่ง ดังนั้น Decorator มาทำหน้าที่เป็นพวกครีม และน้ำตาลต่าง ๆ ที่เราเอามาใช้ตกแต่ง เพื่อทำให้ Function นั้นมีความสามารถตามที่เราต้องการมากขึ้นนั่นเอง วันนี้เราจะพาไปดูว่าจริง ๆ แล้วใน Decorator มันมีท่าไหนบ้างที่เราสามารถนำมาใช้งานได้ และตัวอย่างของ Decorator ที่โคตรมีประโยชน์กับการทำงานของเรามาก ๆ กัน
อย่างที่เราบอกว่า Function ใน Python เรียกได้ว่า เป็น First Class Citizen บน Python เลย เพราะสิ่งที่มันทำได้ มันทำได้เยอะมาก ๆ ต่างจากบางภาษาที่มันเกิดมาเป็นแค่ Function ให้เรา Reuse Code เฉย ๆ เราลองค่อย ๆ มาดูกันว่า Function ใน Python มันจะทำอะไรได้เยอะขนาดไหนกัน
def greeting (name) :
print('Hello ' + name)
a = greeting
a("World")
Hello World
อย่างแรกคือ การ Assign Function ลงไปในตัวแปร ทำให้ตัวแปรนั้นสามารถนำตัวได้เหมือน Function ทุกอย่าง จะเห็นได้ว่า ในบรรทัดสุดท้าย เราสามารถเรียก greeting ที่เป็น Function จาก a ได้เลยตรง ๆ เพราะเรา Assign ให้มันก่อนหน้าแล้ว เป็น Pattern ที่อ่านครั้งแรก กำหมัดมาก มันเหมือนกับว่า Function เดียวกัน แต่มีหลายร่างจำแลงได้ ปวดหัวเลยละ
def show_me () :
print("This is me")
def wrapper (func) :
print("I am Wrapper")
func()
wrapper(show_me)
I am Wrapper
This is me
นอกจากที่เราจะ Assign สร้างร่างจำแลงได้แล้ว เรายังสามารถ Pass Function เป็น Argument เข้าไปที่ Function อื่น ๆ ได้อีก หรือตรงกันข้าม เรายังมามารถ Return กลับมาเป็น Function ก็ได้เช่นกัน ปังปุไปเลยค่าาาา
def add_number (number) :
def add_one (number) :
return number + 1
return add_one(number)
print(add_number(1))
2
เช่นเดียวกับการเป็นตัวแปรทุกประการ มันมีคุณสมบัติของ Scope เหมือนกับตัวแปรทุกประการ ทำให้เราสามารถประกาศ Function ภายใน Function ได้ (Nested Function) แต่ Function ที่เราประกาศภายใน Function จะมี Scope อยู่ภายใน Function แม่ของมันเท่านั้น ทำให้เราไม่สามารถเรียก Function add_one จากภายนอก Function add_number ได้นั่นเอง
def add_number (number) :
def add_one () :
return number + 1
return add_one()
print(add_number(1))
2
มองผ่าน ๆ จะคล้าย ๆ กับตัวอย่างก่อนหน้าเลย แต่มันต่างอยู่นิดนึง สังเกตตรง add_one() เราไม่ได้ใส่ Argument ลงไปให้มัน แต่ในการทำงาน เราเรียก number ขึ้นมา ใช่แล้วการที่เป็น Nested Function ทำให้มันสามารถเข้าถึงตัวแปรที่อยู่ด้านบนของมันได้ทั้งหมด รวมถึง number ที่เป็น Argument ใส่เข้ามาตอนเรียก add_number
จากความสามารถตรงนี้ ทำให้เราบอกได้เลยว่า Function คือ First Class Citizen จริง ๆ แล้วเผลอ ๆ เราแทบไม่ต้องแยกด้วยซ้ำว่าอะไรคือตัวแปร หรือ อะไรคือ Function เพราะคุณสมบัติของมันแทบจะเหมือนกันทุกประการ มันทำได้เยอะสุด ๆ ไปเลย อ่านมาถึงตอนนี้ อาจจะ งง ว่า แล้วเรามาเรียกร้องสิทธิ์อะไรให้กับ Function มิทราบ จะบอกว่า มันเป็นพื้นฐานของ Decorator ที่เราจะคุยกันในบทความนี้ เลยต้องเอามาเล่าซะหน่อย
ก่อนหน้าที่เราอธิบายว่า Decorator มันคืออะไร อาจจะ งง ว่าแล้วยังไง เราลองมาทำให้มันดูเป็นรูปธรรมขึ้นกันดีกว่า ในตัวอย่างนี้ เราจะลองยกตัวอย่างของ Decorator อย่างง่ายขึ้นมาให้ดูกัน
def add_more_dec (call_func):
def add_one () :
return call_func() + 1
return add_one
def init_one () :
return 1
print(add_more_dec(init_one))
2
ใน Code ด้านบน อาจจะดู งง ๆ นิดหน่อย แต่ขอให้นึกถึงสมบัติของ Function ใน Python ให้ดี ๆ ก่อนอื่น เราสร้าง add_more_dec เป็น Function ตัวนึง รับ Function ตัวนึงเข้ามา ที่ด้านในเราก็สร้าง Nested Function อีก เป็น add_one ที่เราไม่ได้รับค่าอะไรเลย แต่เรียก call_func ที่มาจาก add_more_dec สุดท้ายก็ ให้ค่าจาก call_func แล้วบวกด้วย 1 กลับไป สุดท้าย add_more_dec ก็เอา add_one Function กลับไป
จากนั้น เราสร้าง Function init_one ขึ้นมา เพื่อให้มัน Return 1 กลับไป และสุดท้าย เราก็เรียก add_more_dec ที่ด้านใน Pass init_one() เป็น Argument เข้าไป ท้ายสุด มันจะ Evaluate ออกมาเป็น 1 + 1 ทำให้เราได้คำตอบ 2 ออกมา ถึงแม้ว่าเราจะไม่ได้แก้ init_one() เลยแม้แต่บรรทัดเดียว ซึ่งขัดกับ init_one() แน่นอน
def add_more_dec (call_func):
def add_one () :
return call_func() + 1
return add_one
@add_more_dec
def init_one () :
return 1
print(init_one)
แต่การเขียนแบบนี้มันยุ่งยาก มันต้องซ้อน ๆ เข้าไปเรื่อย ๆ สุดท้าย Code เราจะบานออกข้างเยอะไปหน่อย ทำให้ Python บอกว่า ไม่เอาแล้ว เราจะไม่เขียนแบบนี้ แต่เราจะเขียนเป็น Decorator แทน ก็คือ การใช้เครื่องหมาย @ และตามด้วย Decorator ที่เราต้องการ จากนั้นเวลาเราจะเรียกอีกกี่ครั้ง Decorator ที่เรากำหนดไว้มันก็จะทำงานพร้อม ๆ กับ Function ที่เราใส่เข้าไปเสมอ
@add_more_two_dec
@add_more_dec
def init_one () :
return 1
print(init_one)
หรือกระทั่ง เราสามารถใส่หลาย ๆ Decorator ใน 1 Function ได้เช่นกัน โดยที่เราใส่ @ เพิ่มไปเรื่อย ๆ จนกว่าเราจะหมด
ถึงตรงนี้ เราน่าจะพอเห็นภาพมากขึ้นแล้วว่าจริง ๆ แล้วการที่เราใส่ @ ด้านหน้า จริง ๆ แล้วมันคือ การลดรูปของการเอา Function มาต่อ ๆ กันเรื่อย ๆ นั่นเอง เพื่อให้มันอ่านได้ง่ายขึ้น และ เราเองก็ยังสามารถสร้าง Decorator ขึ้นมาใช้เองได้ ต่อไปเราลองมาดูกันบ้างดีกว่าว่ามันยังทำอะไรได้อีกบ้าง บอกเลย แซ่บ เผ็ดช์ร้อน !!!!
เราลองมาดูความ Advance ของมันขึ้นไปอีก (ถ้าพิมพ์ติดกัน ใจมันไม่เกเร อยู่ไม่สุข) ความเจ๋งของมันทำได้อีกหลายอย่าง Advance เข้าไปอีก
def time_two_dec (call_func):
def time_two (number) :
return call_func(number) + number
return time_two
@time_two_dec
def init_number (number) :
print(number)
return number
print(init_number(9))
9
18
เอาละ เริ่ม Advance ขึ้นอีกนิด ในตัวอย่างนี้ เราปรับปรุง Decorator ให้เจ๋งขึ้นอีก โดยการเพิ่มความสามารถในการรับ Argument เข้ามาได้ด้วย เราขอเปลี่ยนตัวอย่างเป็นการ คูณ 2 ละกัน เรารู้ว่า การคูณ 2 คือการเอาจำนวนนั้น ๆ บวกเข้าไปซ้ำอีกที สิ่งที่เราทำคล้ายกับตัวอย่างก่อนหน้าเลย แต่เปลี่ยนนิดหน่อย จากเดิมที่ init_one มันจะคืนกลับมาเป็น 1 เสมอ เราเปลี่ยนให้เป็น init_number() แทนที่สามารถรับตัวเลขเข้ามาได้ตัวนึง จากนั้นก็ Return ตัวเลขนั้นกลับไป
ในส่วนของ Decorator เราสร้าง Nested Function ด้านในคือ time_two สิ่งที่มันทำคือ เราจะเรียก Function ที่ Pass เข้ามาในชื่อว่า call_func โดยใส่ Parameter เป็นตัวเลขที่ Input เข้ามาจาก Parameter ของ time_two() อีกที และเอาผลลัพธ์นั้นมาบวกกับ number อีกรอบ เลย กลายเป็นการคูณ 2
สังเกตในผลลัพธ์ เราจะมี 2 บรรทัด ซึ่งอันแรกมันเกิดจากอันที่เราสั่ง print() ใน init_number() ซึ่งมันจะถูกเรียกตอน return call_func(number) + number เท่านี้ก็ทำให้เราสามารถ Pass Argument ลงไปใน Decorator ได้แล้ว หรือ ถ้าเราอยากได้ General-Purposed Argument เราก็สามารถใช้ท่าเดิมได้ โดยการกำหนด Argument ของ Function เป็น *args,**kwargs หรือก็คือ การเอา Positional และ Keyword Argument ตามท่าปกติที่เราใช้สร้าง Function ใน Python อยู่แล้ว
def greet_more (dec_arg1, dec_arg2) :
def decorator (call_func) :
def get_greeting_msg (func_arg1, func_arg2) :
print("Greeting", func_arg1, func_arg2, dec_arg1, dec_arg2, " from inside wrapper")
return call_func(func_arg1, func_arg2)
return get_greeting_msg
return decorator
@greet_more('Thomas', 'Tom')
def greet_two_people (name1, name2) :
print('Greeting', name1, name2, 'From Normal Function')
greet_two_people('Alice', 'Edison')
Greeting Alice Edison Thomas Tom from inside wrapper
Greeting Alice Edison From Normal Function
ไปให้สุดหยุดที่มึนไปเลยจ้าาา ตัวอย่างนี้ เราต้องการให้ Decorator ของเราสามารถรับค่าเข้ามาได้ เราเลยทำการสร้าง Decorator ซ้อน Decorator เข้าไปอีก จะเป็น greet_more > decorator > get_greeting_msg เราลองค่อย ๆ มาดูทีละส่วนละกัน ดูทั้งหมดเลย ตอนแรกที่คุยกับเพื่อนเรื่องนี้แล้วอ่านกัน ทะเลาะกันซะงั้นอีบ้าาา !!!
def greet_more (dec_arg1, dec_arg2) :
...
return decorator
เราเริ่มจากชั้นนอกสุดก่อน เราสร้าง Function ชื่อ greet_mode ขึ้นมา โดยที่รับ Argument เข้าไป 2 ค่าด้วยกัน และสุดท้าย เราก็คืนกลับเป็น Function เหมือนกับ Decorator ปกติเลย คือ decorator() ที่เป็น Function ที่กำลังจะประกาศด้านล่างต่อในขั้นตอนต่อไป
def decorator (call_func) :
def get_greeting_msg (func_arg1, func_arg2) :
print("Greeting", func_arg1, func_arg2, dec_arg1, dec_arg2, " from inside wrapper")
return call_func(func_arg1, func_arg2)
return get_greeting_msg
ความซับซ้อนเอ๋ยจงเยอะขึ้นเรื่อย ๆ ใช่แล้ววว ด้านในของ decorator() เราจะรับ Argument เป็น Function เหมือน Decorator ที่เราสร้างกันก่อนหน้านี้ทุกประการ จริง ๆ เอาใหม่ decorator ตัวนี้ เหมือนกับตัวที่เราสร้างก่อนหน้านี้เลย แค่ว่า ภายใน Nested Function ของ decorator() มันมีการรับค่า ดั่งที่เรายกตัวอย่างมาก่อนหน้านี้แล้ว สุดท้ายไส้ในสุด เราก็เรียก print() โดยที่สังเกตค่าที่เราใช้ Print ออกมา มันจะมี func_arg1 และ func_arg2 อันนี้ปกติ เป็น Parameter ที่ได้จาก get_greeting_msg() แน่นอน แต่ตัว dec_arg1 และ dec_arg2 นี่สิ
จำที่เราคุยกันก่อนหน้านี้ได้มั้ยว่า มันมี Variable Scope อยู่ dec_arg1 และ dec_arg2 มันเป็น Parameter ที่ใส่เข้ามาตอน greet_more และ get_greeting_msg ก็อยู่ในตระกูลเดียวกัน (เรียกว่าเป็น หลาน ก็ได้) ทำให้ภายใน get_greeting_msg สามารถเรียก Parameter ที่มาจาก greet_more ได้
@greet_more('Thomas', 'Tom')
def greet_two_people (name1, name2) :
print('Greeting', name1, name2, 'From Normal Function')
greet_two_people('Alice', 'Edison')
ท้ายสุด เราก็ใช้ call_func ซึ่งเป็น Function ที่เป็น Parameter จาก decorator() อีกที มันก็จะมาจากการที่เราเอา Function นี้แหละ ไป Decorate ที่ greet_two_people() ทำให้ call_function มันเลยเท่ากับ greet_two_people() นั่นเอง
ที่เหลือก็ง่ายละ เราเรียก greet_two_people() ซึ่งมัน Decorate ด้วย greet_more() ทำให้มันก็จะไปทำงานตรงนั้นก่อน และสุดท้ายภายในสุดของ get_greeting_msg() มันก็จะกลับมาเรียก greet_two_people() อีกที ทำให้เราได้ผลลัพธ์อย่างที่เราเห็น
จริง ๆ แล้ว การใช้พวก Decorator มันเอาไปทำอะไรได้เยอะมาก ๆ เลยเหมือนกับที่เรามักจะพูดถึง Pattern อื่น ๆ ขึ้นกับจินตนาการของเราจะไปถึงเลย เรายกตัวอย่างง่าย ๆ เป็นการวัดเวลาในการทำงานละกัน น่าจะใช้กันเยอะ
import time
def time_counter_decorator (func) :
def measure_time (max_num) :
start_time = time.time()
result = func(max_num)
elapsed = time.time() - start_time
print(func.__name__ , 'spend', elapsed , 'sec(s)')
return result
return measure_time
@time_counter_decorator
def adding_number (max_num) :
result = 0
for i in range(max_num):
result += i
return result
result = adding_number(100000000)
print('Result is', result)
adding_number spend 2.8512260913848877 sec(s)
Result is 4999999950000000
จากตัวอย่างนี้ เราสร้าง time_counter_decorator() ที่มี measure_time() สำหรับการวัดเวลาในการทำงานของ Function ที่เราใส่เข้าไป เราเลยสร้างเป็น Closure ในการวัดเวลาทั่ว ๆ ไปขึ้นมา โดยให้มันเก็บเวลาก่อนที่จะรัน และ เก็บเวลาหลังรัน ลบกัน เราก็จะได้เวลาในการรันออกมาแค่นี้เลย เป็น Decorator ง่าย ๆ หรือถ้าเราอยากให้มัน Reuse ได้มากขึ้น เราสามารถเปลี่ยนเป็น Argument max_num ให้กลายเป็น *args,**kwargs มันก็จะได้แบบด้านล่างนี้
import time
def time_counter_decorator (func) :
def measure_time (*args,**kwargs) :
start_time = time.time()
result = func(*args,**kwargs)
elapsed = time.time() - start_time
print(func.__name__ , 'spend', elapsed , 'sec(s)')
return result
return measure_time
@time_counter_decorator
def adding_number (max_num) :
result = 0
for i in range(max_num):
result += i
return result
result = adding_number(100000000)
print('Result is', result)
แค่เปลี่ยนเท่านี้ก็ทำให้ Decorator ที่เราสร้างขึ้นมา สามารถเอาไปใช้วัดเวลาในการรันกับ Function อะไรก็ได้แล้ว เราแค่เดิม Decoration เข้าไปใน Function นั้น ๆ ก็เป็นอันใช้ได้แล้ว ง่ายมาก ๆ
วันนี้ก็ยาวมากพอควรแล้ว ขอตัดเพียงเท่านี้ก่อน วันนี้เรามาทำความรู้จัก Decorator ที่เป็น Feature บน Python ทำให้เราสามารถเพิ่มความสามารถ หรือเปลี่ยนแปลงการทำงานของ Function โดยที่เราไม่ต้องเข้าไปแก้ไข Function เดิมเลย หลักการของมันสั้น ๆ คือ เราพยายามมาคล่อม Function ที่เป็นของเรา ทำให้เราสามารถเปลี่ยน หรือเพิ่มความสามารถได้นั่นเอง โดย Python อนุญาติให้เราเขียนอยู่ในรูปแบบของ @ ตามด้วยชื่อ Decorator ได้เลยทำให้การใช้งานง่าย และ Code ดูสะอาดตามากขึ้น จากตัวอย่างวันนี้ถือว่าเป็นของเล็กน้อยมาก ๆ ในตอนหน้า เราจะพามาดูตัวอย่างของ Decorator ที่เราใช้บ่อย ๆ ทั้งเก็บเป็น Snippet และโหลดมาใช้เลย บอกเลยว่า แซ่บมาก ทำให้เราเขียน Code ได้ดีขึ้น วัดผลง่ายขึ้น และ ทำให้ตรวจสอบความผิดพลาดได้ง่ายขึ้นด้วย
เราเป็นคนที่อ่านกับซื้อหนังสือเยอะมาก ปัญหานึงที่ประสบมาหลายรอบและน่าหงุดหงิดมาก ๆ คือ ซื้อหนังสือซ้ำเจ้าค่ะ ทำให้เราจะต้องมีระบบง่าย ๆ สักตัวในการจัดการ วันนี้เลยจะมาเล่าวิธีการที่เราใช้ Obsidian ในการจัดการหนังสือที่เรามีกัน...
หากเราเรียนลงลึกไปในภาษาใหม่ ๆ อย่าง Python และ Java โดยเฉพาะในเรื่องของการจัดการ Memory ว่าเขาใช้ Garbage Collection นะ ว่าแต่มันทำงานยังไง วันนี้เราจะมาเล่าให้อ่านกันว่า จริง ๆ แล้วมันทำงานอย่างไร และมันมีเคสใดที่อาจจะหลุดจนเราต้องเข้ามาจัดการเองบ้าง...
ก่อนหน้านี้เราเปลี่ยนมาใช้ Zigbee Dongle กับ Home Assistant พบว่าเสถียรขึ้นเยอะมาก อุปกรณ์แทบไม่หลุดออกจากระบบเลย แต่การติดตั้งมันเข้ากับ Synology DSM นั้นมีรายละเอียดมากกว่าอันอื่นนิดหน่อย วันนี้เราจะมาเล่าวิธีการเพื่อใครเอาไปทำกัน...
เมื่อหลายวันก่อนมีพี่ที่รู้จักกันมาถามว่า เราจะโหลด CSV ยังไงให้เร็วที่สุด เป็นคำถามที่ดูเหมือนง่ายนะ แต่พอมานั่งคิด ๆ ต่อ เห้ย มันมีอะไรสนุก ๆ ในนั้นเยอะเลยนี่หว่า วันนี้เราจะมาเล่าให้อ่านกันว่า มันมีวิธีการอย่างไรบ้าง และวิธีไหนเร็วที่สุด เหมาะกับงานแบบไหน...