Tutorial

Decorator ของตกแต่ง Function บน Python

By Arnon Puitrakul - 31 สิงหาคม 2021

Decorator ของตกแต่ง Function บน Python

เมื่อหลายวันก่อนมีน้องถามเรื่องของ Functools ใน Python เลยทำให้นึกถึงความเป็น First Class Citizen ของ Function ใน Python ว่ามันเป็นชนชั้นที่ความสามารถเยอะมาก ๆ เราสามารถ Pass เป็น Argument เข้าไปซ้อน ๆ Function อีกทีได้ แต่ Feature ตัวนึงที่เจ๋งมาก ๆ และหลาย ๆ คนมองข้ามไปคือ Decorator

Decorator คืออะไร ?

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

Function คือโคตรของ First Class Citizen บน Python

อย่างที่เราบอกว่า 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 มันคืออะไร อาจจะ งง ว่าแล้วยังไง เราลองมาทำให้มันดูเป็นรูปธรรมขึ้นกันดีกว่า ในตัวอย่างนี้ เราจะลองยกตัวอย่างของ 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 ขึ้นมาใช้เองได้ ต่อไปเราลองมาดูกันบ้างดีกว่าว่ามันยังทำอะไรได้อีกบ้าง บอกเลย แซ่บ เผ็ดช์ร้อน !!!!

More A d v a n c e d ? 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() อีกที ทำให้เราได้ผลลัพธ์อย่างที่เราเห็น

Example: Function Timer

จริง ๆ แล้ว การใช้พวก 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 ได้ดีขึ้น วัดผลง่ายขึ้น และ ทำให้ตรวจสอบความผิดพลาดได้ง่ายขึ้นด้วย

Read Next...

จัดการ Docker Container ง่าย ๆ ด้วย Portainer

จัดการ Docker Container ง่าย ๆ ด้วย Portainer

การใช้ Docker CLI ในการจัดการ Container เป็นท่าที่เราใช้งานกันทั่วไป มันมีความยุ่งยาก และผิดพลาดได้ง่ายยังไม่นับว่ามี Instance หลายตัว ทำให้เราต้องค่อย ๆ SSH เข้าไปทำทีละตัว มันจะดีกว่ามั้ย หากเรามี Centralised Container Managment ที่มี Web GUI ให้เราด้วย วันนี้เราจะพาไปทำความรู้จักกับ Portainer กัน...

Host Website จากบ้านด้วย Cloudflare Tunnel ใน 10 นาที

Host Website จากบ้านด้วย Cloudflare Tunnel ใน 10 นาที

ปกติหากเราต้องการจะเปิดเว็บสักเว็บ เราจำเป็นต้องมี Web Server ตั้งอยู่ที่ไหนสักที่หนึ่ง ต้องใช้ค่าใช้จ่าย พร้อมกับต้องจัดการเรื่องความปลอดภัยอีก วันนี้เราจะมาแนะนำวิธีการที่ง่ายแสนง่าย ปลอดภัย และฟรี กับ Cloudflare Tunnel ให้อ่านกัน...

จัดการข้อมูลบน Pandas ยังไงให้เร็ว 1000x ด้วย Vectorisation

จัดการข้อมูลบน Pandas ยังไงให้เร็ว 1000x ด้วย Vectorisation

เวลาเราทำงานกับข้อมูลอย่าง Pandas DataFrame หนึ่งในงานที่เราเขียนลงไปให้มันทำคือ การ Apply Function เข้าไป ถ้าข้อมูลมีขนาดเล็ก มันไม่มีปัญหาเท่าไหร่ แต่ถ้าข้อมูลของเราใหญ่ มันอีกเรื่องเลย ถ้าเราจะเขียนให้เร็วที่สุด เราจะทำได้โดยวิธีใดบ้าง วันนี้เรามาดูกัน...

ปั่นความเร็ว Python Script เกือบ 700 เท่าด้วย JIT บน Numba

ปั่นความเร็ว Python Script เกือบ 700 เท่าด้วย JIT บน Numba

Python เป็นภาษาที่เราใช้งานกันเยอะมาก ๆ เพราะความยืดหยุ่นของมัน แต่ปัญหาของมันก็เกิดจากข้อดีของมันนี่แหละ ทำให้เมื่อเราต้องการ Performance แต่ถ้าเราจะบอกว่า เราสามารถทำได้ดีทั้งคู่เลยละ จะเป็นยังไง เราขอแนะนำ Numba ที่ใช้งาน JIT บอกเลยว่า เร็วขึ้นแบบ 700 เท่าตอนที่ทดลองกันเลย...