φυβλαςのβλογ
บล็อกของ phyblas



ภาษา python เบื้องต้น บทที่ ๓๑: ทำความเข้าใจเดคอเรเตอร์มากยิ่งขึ้น
เขียนเมื่อ 2016/04/28 00:33
แก้ไขล่าสุด 2024/02/22 11:09
 

ในบทที่แล้วได้เริ่มแนะนำถึงเดคอเรเตอร์ไปแล้ว ในบทนี้จะอธิบายต่อโดยยกตัวอย่างของเดคอเรเตอร์ที่ซับซ้อนขึ้น



เดคอเรเตอร์ซ้อนกัน

เดคอเรเตอร์สามารถนำมาซ้อนกันหลายชั้นได้ คือการแต่งฟังก์ชันที่ผ่านการแต่งแล้วมากขึ้นไปอีก

ลองดูตัวอย่างนี้
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 จะทำให้เดคอเรเตอร์ดูสมบูรณ์แบบยิ่งขึ้น ดังนั้นอาจถูกใช้เป็นประจำเวลาสร้างฟังก์ชันสำหรับใช้เป็นเดคอเรเตอร์



สรุปเนื้อหา

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

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

แม้ว่าจะเป็นเรื่องที่เข้าใจค่อนข้างยากก็ตาม หากลองพยายามอ่านทำความเข้าใจมันจนสำเร็จแล้วภายหลังก็จะพบว่ามันคุ้มค่าที่จะพยายาม



อ้างอิง




-----------------------------------------

囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧

ดูสถิติของหน้านี้

หมวดหมู่

-- คอมพิวเตอร์ >> เขียนโปรแกรม >> python

ไม่อนุญาตให้นำเนื้อหาของบทความไปลงที่อื่นโดยไม่ได้ขออนุญาตโดยเด็ดขาด หากต้องการนำบางส่วนไปลงสามารถทำได้โดยต้องไม่ใช่การก๊อปแปะแต่ให้เปลี่ยนคำพูดเป็นของตัวเอง หรือไม่ก็เขียนในลักษณะการยกข้อความอ้างอิง และไม่ว่ากรณีไหนก็ตาม ต้องให้เครดิตพร้อมใส่ลิงก์ของทุกบทความที่มีการใช้เนื้อหาเสมอ

สารบัญ

รวมคำแปลวลีเด็ดจากญี่ปุ่น
มอดูลต่างๆ
-- numpy
-- matplotlib

-- pandas
-- manim
-- opencv
-- pyqt
-- pytorch
การเรียนรู้ของเครื่อง
-- โครงข่าย
     ประสาทเทียม
ภาษา javascript
ภาษา mongol
ภาษาศาสตร์
maya
ความน่าจะเป็น
บันทึกในญี่ปุ่น
บันทึกในจีน
-- บันทึกในปักกิ่ง
-- บันทึกในฮ่องกง
-- บันทึกในมาเก๊า
บันทึกในไต้หวัน
บันทึกในยุโรปเหนือ
บันทึกในประเทศอื่นๆ
qiita
บทความอื่นๆ

บทความแบ่งตามหมวด



ติดตามอัปเดตของบล็อกได้ที่แฟนเพจ

  ค้นหาบทความ

  บทความแนะนำ

ตัวอักษรกรีกและเปรียบเทียบการใช้งานในภาษากรีกโบราณและกรีกสมัยใหม่
ที่มาของอักษรไทยและความเกี่ยวพันกับอักษรอื่นๆในตระกูลอักษรพราหมี
การสร้างแบบจำลองสามมิติเป็นไฟล์ .obj วิธีการอย่างง่ายที่ไม่ว่าใครก็ลองทำได้ทันที
รวมรายชื่อนักร้องเพลงกวางตุ้ง
ภาษาจีนแบ่งเป็นสำเนียงอะไรบ้าง มีความแตกต่างกันมากแค่ไหน
ทำความเข้าใจระบอบประชาธิปไตยจากประวัติศาสตร์ความเป็นมา
เรียนรู้วิธีการใช้ regular expression (regex)
การใช้ unix shell เบื้องต้น ใน linux และ mac
g ในภาษาญี่ปุ่นออกเสียง "ก" หรือ "ง" กันแน่
ทำความรู้จักกับปัญญาประดิษฐ์และการเรียนรู้ของเครื่อง
ค้นพบระบบดาวเคราะห์ ๘ ดวง เบื้องหลังความสำเร็จคือปัญญาประดิษฐ์ (AI)
หอดูดาวโบราณปักกิ่ง ตอนที่ ๑: แท่นสังเกตการณ์และสวนดอกไม้
พิพิธภัณฑ์สถาปัตยกรรมโบราณปักกิ่ง
เที่ยวเมืองตานตง ล่องเรือในน่านน้ำเกาหลีเหนือ
ตระเวนเที่ยวตามรอยฉากของอนิเมะในญี่ปุ่น
เที่ยวชมหอดูดาวที่ฐานสังเกตการณ์ซิงหลง
ทำไมจึงไม่ควรเขียนวรรณยุกต์เวลาทับศัพท์ภาษาต่างประเทศ

ไทย

日本語

中文