φυβλαςのβλογ
บล็อกของ 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)
หอดูดาวโบราณปักกิ่ง ตอนที่ ๑: แท่นสังเกตการณ์และสวนดอกไม้
พิพิธภัณฑ์สถาปัตยกรรมโบราณปักกิ่ง
เที่ยวเมืองตานตง ล่องเรือในน่านน้ำเกาหลีเหนือ
ตระเวนเที่ยวตามรอยฉากของอนิเมะในญี่ปุ่น
เที่ยวชมหอดูดาวที่ฐานสังเกตการณ์ซิงหลง
ทำไมจึงไม่ควรเขียนวรรณยุกต์เวลาทับศัพท์ภาษาต่างประเทศ

บทความแต่ละเดือน

2024年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

2023年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

2022年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

2021年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

2020年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

ค้นบทความเก่ากว่านั้น

ไทย

日本語

中文