ใน
บทที่แล้วได้เริ่มแนะนำถึงเดคอเรเตอร์ไปแล้ว ในบทนี้จะอธิบายต่อโดยยกตัวอย่างของเดคอเรเตอร์ที่ซับซ้อนขึ้น
เดคอเรเตอร์ซ้อนกัน เดคอเรเตอร์สามารถนำมาซ้อนกันหลายชั้นได้ คือการแต่งฟังก์ชันที่ผ่านการแต่งแล้วมากขึ้นไปอีก
ลองดูตัวอย่างนี้
def dekoboko(f):
def g():
return f() + ' dekoboko'
return g
def dekopin(f):
def g():
return f() + ' dekopin'
return g
@dekoboko
@dekopin
def odeko():
return 'odeko'
print(odeko()) # ได้ odeko dekopin dekoboko
ในนี้ได้มีการสร้างฟังก์ชันเพื่อมาใช้เป็นเดคอเรชัน ๒ อันคือ
dekoboko
กับ
dekopin
จากนั้นทั้ง ๒ อันถูกนำมาใช้เพื่อตกแต่งฟังก์ชัน
odeko
โดยวางซ้อนกันไป
การวางซ้อนกันแบบนี้จะมีค่าเทียบเท่ากับการเขียนว่า
def odeko():
return 'odeko'
odeko = dekoboko(dekopin(odeko))
บรรทัดสุดท้ายอาจมองยากหน่อย ถ้าลองแยกดูให้เห็นภาพชัดขึ้นจะได้ว่า
odeko = dekopin(odeko)
odeko = dekoboko(odeko)
นั่นคือเริ่มแรก
odeko
ถูกตกแต่งด้วย
dekopin
ก่อน จากนั้นจึงถูกตกแต่งด้วย
dekoboko
ตามมา
ผลที่ได้ก็คือฟังก์ชัน
odeko
ซึ่งเดิมทีจะคืนค่าแค่คำว่า
odeko
ก็จะถูกเติมคำว่า
dekopin
จากนั้นก็ถูกเติมคำว่า
dekoboko
ตามลำดับ
จะเห็นว่าเมื่อวางซ้อนกันแบบนี้ลำดับความสำคัญของสองตัวที่วางจะไม่เท่ากัน โดยตัวที่อยู่ด้านบนจะถูกวางตกแต่งซ้อนเข้าไปทีหลัง
และแน่นอนว่าจะซ้อนกันไปกี่ชั้นก็ได้ไม่เพียงแค่ ๒ ชั้น หลักการก็เป็นในลักษณะเดียวกัน
เดคอเรเตอร์ที่ตามด้วยวงเล็บ มีสิ่งที่ซับซ้อนยิ่งกว่าเดคอเรเตอร์ที่ซ้อนกัน นั่นคือเดคอเรเตอร์ที่ตามด้วยวงเล็บ
ความจริงแล้วนี่ไม่ควรจะเป็นหัวข้อใหม่ แต่ยกมาพูดถึงเพราะเป็นรูปแบบการเขียนที่คนเจอแล้วอาจตกใจได้
ลองดูตัวอย่างนี้
def dekopin():
def pin(f):
def g():
return f() + ' dekopin'
return g
return pin
@dekopin()
def odeko():
return 'odeko'
print(odeko()) # odeko dekopin
จะเห็นว่า
@dekopin()
มีวงเล็บอยู่ข้างหลังแทนที่จะใส่แค่ชื่อฟังก์ชันทำให้ดูแล้วแปลกตา และแค่เห็นฟังก์ชัน
dekopin
มี
def
ซ้อนกัน ๓ ชั้นแบบนี้ก็คงจะรู้สึกลำบากใจแล้ว แต่มาลองค่อยๆไล่ดูไปทีละนิด
ก่อนอื่น ส่วนฟังก์ชัน
dekopin
หากเขียนโดยไม่ใช่
@
แล้วก็จะกลายเป็น
def odeko():
return 'odeko'
odeko = dekopin()(odeko)
ดูแล้วน่าจะเข้าใจง่ายกว่าเดิมขึ้นมาหน่อย แต่ก็อาจจะยังดูซับซ้อนอยู่ดี
dekopin
มีวงเล็บตามอยู่ข้างหลังอันหนึ่งก่อนที่จะเป็นวงเล็บที่มี
odeko
หมายความว่ามันจะทำงานในฐานะฟังก์ชันทันทีก่อน และผลที่ได้ก็คือจะคืนค่าออกมา ซึ่งในที่นี้จะได้ว่าคืนค่าฟังก์ชัน
pin
ออกมา
ดังนั้นผลที่ได้ก็เหมือนกับการเขียนว่า
odeko = pin(odeko)
หรือก็คือ
@pin
def odeko():
return 'odeko'
นั่นหมายความว่าสิ่งที่เป็นเดคอเรเตอร์จริงๆคือฟังก์ชัน
pin
ที่อยู่ข้างใน
dekopin
อีกที
หากฟังก์ชัน
dekopin
ต้องการพารามิเตอร์ในวงเล็บก็ต้องใส่ตาม เช่น
def dekopin(x):
def pin(f):
def g(y):
return f(y)+x
return g
return pin
@dekopin('pin')
def odeko(s):
return s
print(odeko('dekodeko')) # ได้ dekodekopin
สรุปแล้วก็คือ นี่เป็นการใช้ฟังก์ชันที่สร้างขึ้นมาจากฟังก์ชันอื่นมาเป็นเดคอเรเตอร์ต่อ อีกที ความจริงก็ไม่ใช่ว่ามีหลักการอะไรใหม่ แต่ก็เป็นรูปแบบที่อาจเจอได้บ่อยจึงขอมาอธิบายตรงนี้เพื่อเน้นย้ำสักหน่อย
การใช้ functools.wraps และ functools.update_wrapper เวลาที่ใช้เดคอเรเตอร์เพื่อทำการตกแต่งฟังก์ชันที่มีอยู่เดิมแม้ว่าผลการตกแต่ง จะออกมาเป็นฟังก์ชันที่คล้ายกับเดิมแค่มีการตกแต่งเพิ่มเติม แต่ความจริงแล้วฟังก์ชันที่ได้ก็คืออีกฟังก์ชัน ไม่ใช่ตัวฟังก์ชันเดิมก่อนแต่ง
เพื่อให้เข้าใจความหมายที่ต้องการอธิบาย ลองดูตัวอย่างนี้
def deko(f):
def neko():
'<<นี่เป็นฟังก์ชันที่ถูกสร้างขึ้นภายใน deko>>'
return f() + 'neko'
return neko
@deko
def koko():
'<<นี่คือฟังก์ชันที่ชื่อว่า koko>>'
return 'konoko'
print(koko()) # ได้ konokoneko
print(koko) # ได้ .neko at 0x11251c0d0>
print(koko.__name__) # ได้ neko
print(koko.__doc__) # ได้ <<นี่เป็นฟังก์ชันที่ถูกสร้างขึ้นภายใน deko>>
จะเห็นว่าในตัวอย่างนี้เราใช้ฟังก์ชัน
deko
เพื่อทำการตกแต่งฟังก์ชัน
koko
ผลที่ได้จากการทำงานของฟังก์ชัน
koko
ที่ถูกแต่งแล้วก็คือได้ข้อความ
konokoneko
นั้นเป็นไปตามที่ต้องการไม่มีปัญหา
แต่สิ่งแปลกๆที่จะเห็นก็คือเมื่อลองตรวจสอบชื่อของฟังก์ชันจะพบว่าฟังก์ชันชื่อ
neko
และพอดูด็อกสตริงก็จะพบว่าปรากฏด็อกสตริงของฟังก์ชัน
neko
หากลองไล่ดูสิ่งที่เกิดขึ้นแล้วจะพบว่านี่เป็นเหตุการณ์ที่สมเหตุสมผลดีแล้ว เพราะเมื่อใช้เดคอเรเตอร์ไปนั้นก็เทียบเท่ากับการเขียนว่า
koko = deko(koko)
ซึ่งสิ่งที่
deko
นั้น
return
กลับมาก็คือฟังก์ชัน
neko
เลยกลายเป็นว่าฟังก์ชันชื่อ
neko
ไปอยู่ในตัวแปรชื่อ
koko
แม้ว่าฟังก์ชัน
neko
นี้จะสืบทอดคุณสมบัติต่างๆของ
koko
แต่ว่าชื่อฟังก์ชันและด็อกสตริงไม่ได้สืบทอดมาด้วย
ดังนั้นหากต้องการจะอนุรักษ์ชื่อฟังก์ชันและด็อกสตริงให้คงเดิมไว้จำเป็นจะต้องกระทำการบางอย่างเพิ่มเติม
ทางแก้หนึ่งอาจลองคิดง่ายๆเช่นเขียน
neko.__doc__ = f.__doc__
ไว้ก่อน
return neko
แบบนี้เราก็จะได้ด็อกสตริงของฟังก์ชันเดิม แต่นั่นก็ยังไม่ใช่การแก้ปัญหาที่เด็ดขาดถูกทาง
วิธีการที่นิยมใช้กันก็คือใช้ฟังก์ชัน
wraps
หรือฟังก์ชัน
update_wrapper
ซึ่งอยู่ในมอดูล
functools
functools
เป็นมอดูลที่รวบรวมฟังก์ชันที่เอาไว้จัดการเกี่ยวกับฟังก์ชัน มีฟังก์ชันหลายตัวที่มีประโยชน์ แต่ในที่นี้จะพูดถึง ๒ ตัวซึ่งมักใช้คู่กับเดคอเรเตอร์
เริ่มจาก
update_wrapper
ก่อน เราสามารถใช้ฟังก์ชันนี้เพื่อแก้ฟังก์ชัน
deko
ให้เป็นแบบนี้
import functools
def deko(f):
def neko():
'<<นี่เป็นฟังก์ชันที่ถูกสร้างขึ้นภายใน deko>>'
return f() + 'neko'
return functools.update_wrapper(neko,f)
ข้อแตกต่างจากฟังก์ชัน
deko
เดิมก็มีเพียงแค่
return neko
เปลี่ยนเป็น
return functools.update_wrapper(neko,f)
เท่านั้น
การทำงานของฟังก์ชันนี้ดูแล้วเข้าใจไม่ยาก นั่นคือไปแก้ข้อมูลต่างๆเช่น
__doc__
และ
__name__
ของฟังก์ชันที่ใส่เป็นอาร์กิวเมนต์ตัวแรกแทนที่ด้วยของอาร์กิวเมนต์ตัวหลัง
ดังนั้นอาร์กิวเมนต์ตัวแรกก็ใส่เป็นชื่อฟังก์ชันที่สร้างขึ้นในเดคอเรเตอร์ (ในที่นี้คือ
neko
) และอาร์กิวเมนต์ตัวหลังคือฟังก์ชันที่ต้องการตกแต่ง (ในที่นี้คือ
f
)
ฟังก์ชันนี้จะให้ค่าคืนกลับเป็นฟังก์ชัน
neko
ที่ถูกแก้แล้ว จากนั้นเราก็นำค่านี้มาใส่ใน
return
แทนที่จะใส่
neko
เฉยๆแบบตอนแรก
หลังแก้ฟังก์ชัน
deko
แล้วจากนั้นลองดูใหม่คราวนี้จะได้ผลตามที่ต้องการ
print(koko) # ได้
print(koko.__doc__) # ได้ <<นี่คือฟังก์ชันที่ชื่อว่า koko>>
ต่อมาลองดูอีกวิธีคือใช้ฟังก์ชัน
wraps
ก็จะเขียนคล้ายๆกัน
import functools
def deko(f):
def neko():
'<<นี่เป็นฟังก์ชันที่ถูกสร้างขึ้นภายใน deko>>'
return f() + 'neko'
return functools.wraps(f)(neko)
แค่เปลี่ยนจาก
functools.update_wrapper(neko,f)
เป็น
functools.wraps(f)(neko)
เท่านั้น ส่วนการทำงานก็เช่นเดิม
จะเห็นว่าการที่เขียน
functools.wraps(f)(neko)
แทน
neko
นั้นก็เท่ากับว่า
functools.wraps(f)
ทำตัวเป็นเดคอเรเตอร์ที่ตกแต่ง
neko
นั่นหมายความว่าอาจเขียนในรูปแบบของเดคอเรเตอร์
import functools
def deko(f):
@functools.wraps(f)
def neko():
'<<นี่เป็นฟังก์ชันที่ถูกสร้างขึ้นภายใน deko>>'
return f() + 'neko'
return neko
ที่จริงแล้วนี่เป็นรูปแบบที่นิยมใช้กันมากที่สุด เพราะสั้นที่สุด แค่เพิ่ม
@functools.wraps(f)
ลงไปเท่านั้น
การใช้เดคอเรเตอร์เพื่อสร้างเดคอเรเตอร์แบบนี้อาจดูแล้วทำให้เข้าใจยาก แต่นี่เป็นรูปแบบการใช้ที่เจอบ่อย ดังนั้นควรจะต้องพยายามทำความเข้าใจไว้
การใช้
wraps
หรือ
update_wrapper
จะทำให้เดคอเรเตอร์ดูสมบูรณ์แบบยิ่งขึ้น ดังนั้นอาจถูกใช้เป็นประจำเวลาสร้างฟังก์ชันสำหรับใช้เป็นเดคอเรเตอร์
สรุปเนื้อหา ในสองบทที่ผ่านมาได้อธิบายให้เข้าใจถึงพื้นฐานของเดคอเรเตอร์ที่จำเป็น แต่ยังไม่ได้พูดถึงการนำไปใช้งานจริงๆนัก ตอนนี้จึงอาจจะยังนึกภาพไม่ออกว่าจะเอาไปใช้ทำอะไร
ความจริงแล้วเดคอเร เตอร์ถูกนำมาใช้งานอย่างกว้างขวางทีเดียว ไม่เช่นนั้นคงไม่ถูกคิดขึ้นมา ดังนั้นจึงมีความจำเป็นที่จะต้องทำความเข้าใจเอาไว้
แม้ว่าจะเป็นเรื่องที่เข้าใจค่อนข้างยากก็ตาม หากลองพยายามอ่านทำความเข้าใจมันจนสำเร็จแล้วภายหลังก็จะพบว่ามันคุ้มค่าที่จะพยายาม
อ้างอิง