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



ภาษา python เบื้องต้น บทที่ ๒๙: การสร้างฟังก์ชันที่มีความซับซ้อน
เขียนเมื่อ 2016/04/27 17:06
แก้ไขล่าสุด 2024/02/22 11:08
 

ในบทที่ผ่านๆมาได้พูดถึงเกี่ยวกับเรื่องของฟังก์ชันไปเยอะ และบทที่ ๑๙ ได้เริ่มพูดถึงการสร้างฟังก์ชัน แต่นั่นก็ยังเป็นเพียงเบื้องต้นเท่านั้น ในบทนี้จะมาพูดถึงเรื่องฟังก์ชันต่อ โดยเข้าถึงเนื้อหาที่ยากขึ้นไปอีก



ฟังก์ชันในฐานะตัวแปร

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

สามารถทำการสำเนาฟังก์ชันได้ง่ายโดยการใช้เครื่องหมายเท่ากับ = ตัวแปรที่มารับจะกลายเป็นฟังก์ชันซึ่งใช้งานได้เหมือนกับฟังก์ชันที่นิยามตอนแรก
def fuji():
    print('ฟังก์ชันถูกใช้งาน')
fun = fuji
fun()

ฟังก์ชันพื้นฐานที่ติดตัวอยู่แล้วก็สามารถสำเนาได้เช่นกัน เช่น
พิมพ์ = print

พอทำแบบนี้แล้วต่อไปเราก็จะใช้ พิมพ์('ข้อความ') แทน print('ข้อความ') ได้
พิมพ์('ทดสอบใช้พิมพ์') # ได้ ทดสอบใช้พิมพ์

หากลองนึกสนุกอาจจะลองเปลี่ยนชื่อฟังก์ชันให้กลายเป็นภาษาไทยหมดเลยดูก็ได้ แบบนั้นก็คงจะดูแปลกดี รองรับความต้องการของนักเขียนโปรแกรมที่มีความเป็นชาตินิยมได้ในระดับหนึ่งเลยทีเดียว

อย่างไรก็ตาม หากลองสั่ง print ตัวแปรที่รับค่ามาดูก็จะยังพบว่ามันขึ้นชื่อว่าเป็นฟังก์ชันตัวเดิมอยู่
พิมพ์(พิมพ์) # ได้ 
พิมพ์(fun) # ได้ 
พิมพ์(พิมพ์.__name__) # ได้ print

ไม่ใช่แค่นั้น ฟังก์ชันยังสามารถถูกเก็บอยู่ในข้อมูลชนิดกลุ่มอย่างลิสต์หรือทูเพิลได้ด้วย
def fujiA():
    print('A')
def fujiB():
    print('B')

fujiAB = (fujiA,fujiB)

แล้วเวลาจะเรียกใช้งานก็ใส่เลขลำดับแล้วจึงตามด้วยวงเล็บ ()
fujiAB[0]() # ได้ A
fujiAB[1]() # ได้ B

และพอเป็นแบบนี้ก็สามารถนำมาใช้กับ for ได้
[f() for f in fujiAB]
list(map(lambda f:f(),fujiAB))



สิ่งที่ต้องระวังอย่างหนึ่งก็คือ หากตัวแปรที่เก็บฟังก์ชันถูกเขียนแทนด้วยอะไรอย่างอื่น ฟังก์ชันนั้นก็จะหายไปและไม่สามารถใช้ได้อีก
fuji = 1
fuji() # ได้ TypeError: 'int' object is not callable

ซ้ำร้าย ฟังก์ชันที่ถูกเขียนทับได้นั้นยังไม่เว้นแม้กระทั่งฟังก์ชันพื้นฐานที่ติดมากับไพธอนอยู่แล้วด้วย เช่น
print = 'print'

เท่านี้ฟังก์ชัน print ก็จะไม่สามารถใช้การได้อีกต่อไป กลายเป็นแค่ตัวแปรสายอักขระคำว่า 'print' ไป
print(print) # ได้ TypeError: 'str' object is not callable

พอเป็นแบบนี้ไปแล้วหากอยากให้ print กลับมาใช้ได้เหมือนเดิมก็ต้องทำการปิดเชลโต้ตอบแล้วเปิดอันใหม่ ตัวแปรทั้งหมดที่ประกาศไว้จะถูกล้างทิ้งแล้วกลับสู่สภาพเริ่มต้น

อย่างไรก็ตาม ตัวแปรที่เคยใช้รับฟังก์ชันตัวที่ถูกเขียนทับเอาไว้ก็ยังทำงานได้ดีอยู่ ไม่ได้รับผลกระทบอะไรไปด้วย สามารถใช้งานแทนได้ หรือจะทำให้ตัวแปรเดิมกลับมาใช้งานเป็นฟังก์ชันเหมือนเดิมก็ได้
พิมพ์(print) # ได้ print
fun() # ได้ ฟังก์ชันถูกใช้งาน
print = พิมพ์



ฟังก์ชันซ้อนฟังก์ชัน

ภายในฟังก์ชันนอกจากจะประกาศตัวแปรทั่วๆไปแล้วยังสามารถประกาศฟังก์ชันได้ด้วย แบบนี้จะเกิดเป็นฟังก์ชันซ้อนในฟังก์ชัน

เช่นเดียวกับที่ตัวแปรที่ประกาศในฟังก์ชันจะใช้ได้แค่ในฟังก์ชัน ฟังก์ชันด้านในที่ประกาศในฟังก์ชันด้านนอกก็สามารถใช้ได้แค่ในฟังก์ชันด้านในนั้นเท่านั้น จะมาใช้ด้านนอกไม่ได้
def nok():
    def nai():
        return 1
    a = nai()
    return a

print(nok()) # ได้ 1

ในนี้จะเห็นว่าภายในฟังก์ชัน nok มีการนิยามฟังก์ชัน nai ขึ้นและใช้ทันที

พอใช้ฟังก์ชัน nai ที่ด้านนอกจะเกิดข้อผิดพลาด
print(nai()) # ได้ NameError: name 'nai' is not defined

อย่างไรก็ตาม เราสามารถส่งฟังก์ชันภายในออกมาด้านนอกได้โดยการใส่ฟังก์ชันใน return
def nok():
    def nai():
        return 1
    return nai

print(nok()) # ได้ .nai at 0x1123bd620>

ให้ระวังว่ากรณีนี้ตรงคำสั่งที่ให้คืนค่าฟังก์ชันต้องเขียนเป็น return nai โดยไม่มีวงเล็บ ไม่ใช่ return nai() เพราะสิ่งที่ต้องการส่งออกมานั้นคือตัวฟังก์ชันด้านใน ไม่ใช่ค่าที่ฟังก์ชันด้านในคืนออกมาอีกต่อ

จะเห็นว่าฟังก์ชัน nok ทำงานโดยคืนค่าฟังก์ชัน nai ซึ่งอยู่ภายในออกมา และฟังก์ชันนี้สามารถนำมาใช้ได้จริงด้วย
a = nok()
print(a()) # ได้ 1 

หรืออาจจะเขียนสั้นกว่านั้น แม้ว่าจะดูแล้วชวนให้งงสักหน่อย นั่นคือ
print(nok()()) # ได้ 1

เห็นวงเล็บวางต่อกันสองอันแบบนี้อาจดูแล้วประหลาด แต่มันทำงานได้จริงๆ โดยที่ nok() จะคืนค่า nai มา ดังนั้น nok()() ก็จะเท่ากับ nai() ซึ่งจะคืนค่า 1

จะซ้อนกันอีกเป็น ๓ ชั้นก็ได้
def nok():
    def klang():
        def nai():
            return 1
        return nai
    return klang

print(nok()()()) # ได้ 1 

หรือจะหลายชั้นกว่านี้ก็ยังทำได้ แต่ก็จะยิ่งดูงงและซับซ้อนขึ้นเรื่อยๆ

พารามิเตอร์ของฟังก์ชันด้านนอกสามารถใช้ในฟังก์ชันด้านในได้
def nok(x):
    def nai():
        return x
    a = nai()
    return a

print(nok(5)) # ได้ 5

และถึงจะซ้อนหลายชั้น แต่ตัวแปรหรือพารามิเตอร์จากด้านนอกก็สามารถส่งเข้าไปถึงด้านในได้
def nok(x):
    def klang(y):
        def nai(z):
            return '%d+%d+%d'%(x,y,z)
        return nai
    return klang

print(nok(2)(3)(4)) # ได้ 2+3+4

การเขียนฟังก์ชันซ้อนๆกันแบบนี้อาจดูแล้วงงได้ง่าย ต้องพยายามค่อยๆไล่คิดตามถึงการทำงานของมันไปเรื่อยๆช้าๆตามลำดับขั้น

ความซับซ้อนยังไม่ได้มีเพียงเท่านี้ ลองนึกถึงสถานการณ์ที่ฟังก์ชันด้านนอกมีการคืนกลับฟังก์ชันด้านในออกมาทีละ หลายตัว โดยอาจอยู่ในรูปทูเพิลหรือลิสต์ เช่น
def nok():
    def nai1():
        return 1
    def nai2():
        return 2
    def nai3():
        return 3
    return (nai1,nai2,nai3)

print(nok())  # ได้ (.nai1 at 0x1123bdbf8>, .nai2 at 0x11272ef28>, .nai3 at 0x11272ed08>)

จะเห็นว่าผลที่ได้ออกมาเป็นทูเพิลที่มีฟังก์ชัน ๓ อันด้านใน สามารถลองนำแต่ละอันมาใช้ได้โดยใส่เลขพิกัด และถ้าต้องการให้ฟังก์ชันทำงานก็เติมวงเล็บ () ต่อไปอีก
print(nok()[0]) # ได้ .nai1 at 0x1123bdbf8>
print(nok()[0]()) # ได้ 1
print(nok()[1]()) # ได้ 2

นอกจากนี้หากต้องการให้ฟังก์ชันทั้งหมดทำงานแล้วคืนค่าออกมาพร้อมกัน อาจใช้ for หรือ map ช่วยก็ได้
print([f() for f in nok()]) # ได้ [1, 2, 3]
print(list(map(lambda x:x(),nok()))) # ได้ [1, 2, 3]

คราวนี้ลองให้ฟังก์ชันทั้งในและนอกแต่ละตัวมีพารามิเตอร์ แบบนี้ก็จะยิ่งดูซับซ้อนขึ้นไปอีก
def nok(t):
    def nai1(x):
        return t+x
    def nai2(y):
        return t+y
    def nai3(z):
        return t+z
    return (nai1,nai2,nai3)

print(nok('A')[0]('1'))
print([f('3') for f in nok('B')])
print([f(str(i)) for i,f in enumerate(nok('C'),start=1)])
print(list(map(lambda f:f[1](str(f[0])),enumerate(nok('D'),start=4))))
print([nok(x)[y](z) for x,y,z in zip('123',(0,1,2),'ABC')])
print(list(map(lambda f:nok(f[0])[f[1]](f[2]),zip('456',(0,1,2),'DEF')))) 

ผลลัพธ์
A1
['B3', 'B3', 'B3']
['C1', 'C2', 'C3']
['D4', 'D5', 'D6']
['1A', '2B', '3C']
['4D', '5E', '6F']

ลองดูแบบซ้อนหลายชั้น
def nok(x):
    def klang1(y):
        def nai1(z):
            return x+y+z+'๑'
        def nai2(z):
            return x+y+z+'๒'
        return (nai1,nai2)
    def klang2(y):
        def nai1(z):
            return x+y+z+'๓'
        def nai2(z):
            return x+y+z+'๔'
        return (nai1,nai2)
    return (klang1,klang2)

print([nok(x)[a](y)[0]('ก') for x,a,y in zip('12',(0,1),'AB')])
print([nok(x)[a](y)[b](z) for x,a,y,b,z in zip('1234',(0,1,0,1),'ABCD',(1,0,0,1),'กขคง')]) 

ผลลัพธ์
['1Aก๑', '2Bก๓']
['1Aก๒', '2Bข๓', '3Cค๑', '4Dง๔']

ยกตัวอย่างมามากมายและซับซ้อนขึ้นเรื่อยๆ หากใครค่อยๆลองดูแล้วไล่ตามมาเรื่อยๆน่าจะค่อยๆชำนาญขึ้นทีละน้อย

ทั้งหมดนี้เป็นพื้นฐานที่จะช่วยให้มองโปรแกรมออกได้ง่ายขึ้นเวลาที่เจอสถานการณ์ที่ต้องคิดซ้อนกันหลายตลบ



การสร้างฟังก์ชันจากฟังก์ชัน

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

หากมองดูแล้วจะเห็นว่าสิ่งที่ได้ทำมาในตัวอย่างก่อนๆนั้นเรียกได้ว่าเป็นการใช้ฟังก์ชันเพื่อสร้างฟังก์ชัน

แนวคิดเรื่องนี้ค่อนข้างยุ่งยากซับซ้อนพอสมควร ดังนั้นต้องค่อยๆอ่านทำความเข้าใจให้ดี และจำเป็นต้องเข้าใจคุณสมบัติอย่างหนึ่งของภาษาไพธอนที่เรียกว่าโคลเฌอร์ (closure)

ก่อนอื่นลองดูพิจารณาตัวอย่างนี้
def nok():
    x = '==ตัวแปรที่ประกาศในฟังก์ชัน nok แต่นอกฟังก์ชัน nai=='
    def nai():
        print(x)
    return nai

ni = nok()

จะเห็นว่ามีการนิยามฟังก์ชัน nok ขึ้นมาโดยที่ข้างในมีตัวแปร x จากนั้นก็นิยามฟังก์ชัน nai ซึ่งมีการเรียกใช้ตัวแปร x จากนั้นสุดท้ายฟังก์ชัน nok จะ return ฟังก์ชัน nai ออกมา

สุดท้าย สั่งให้ฟังก์ชัน nok ทำงาน โดยคืนค่ามาแล้วให้ตัวแปร ni รับไว้ ดังนั้นในตอนนี้ตัวแปร ni จะรับเอาฟังก์ชัน nai และจะสามารถใช้งานได้ในฐานะฟังก์ชัน nai

แต่ว่าลองมองดูในฟังก์ชัน nai จะเห็นว่ามีการเรียกใช้ตัวแปร x แต่ว่าดังที่เคยกล่าวไปแล้วว่าตัวแปรที่ประกาศภายในฟังก์ชันจะถูกเรียก ใช้ได้แค่ภายในฟังก์ชันเท่านั้น และพอฟังก์ชันสิ้นสุดการทำงานลงตัวแปรนั้นก็จะถูกลบไป

ถ้าเช่นนั้นแล้ว แบบนี้แสดงว่าฟังก์ชัน ni น่าจะไม่สามารถทำงานได้หรือเปล่า? เพราะฟังก์ชัน nok ได้สิ้นสุดการทำงานไปแล้ว เหลือไว้แค่ฟังก์ชัน nai ที่คืนค่าออกมา

คำตอบคือ ไม่เป็นเช่นนั้น ซึ่งก็เป็นดังเช่นเดียวกันกับที่ได้เห็นในตัวอย่างที่ผ่านๆมาแล้ว ฟังก์ชัน ni สามารถทำงานได้ตามปกติไม่มีขาดตกบกพร่อง
ni() # ได้ ==ตัวแปรที่ประกาศในฟังก์ชัน nok แต่นอกฟังก์ชัน nai==

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

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

ลักษณะนี้เหมือนกับว่าฟังก์ชัน nok นั้นเป็นแค่ฟังก์ชันที่ทำหน้าที่ผลิตฟังก์ชัน nai เพื่อจะนำฟังก์ชัน nai ที่ได้มาใช้งานอีกที

ความวิเศษของมันไม่ได้มีแค่นี้ เพราะในแต่ละครั้งฟังก์ชันที่ได้ออกมานั้นอาจมีคุณสมบัติไม่เหมือนกัน เช่นขึ้นอยู่กับพารามิเตอร์ที่ใส่ไปตอนแรก
def nok(a):
    def nai():
        print(a)
    return nai

ni2 = nok(2)
ni7 = nok(7)
ni2() # ได้ 2
ni7() # ได้ 7

จะเห็นว่าฟังก์ชัน ni2 และ ni7 ก็ถูกสร้างขึ้นมาจากฟังก์ชัน nok เหมือนกัน เพียงแต่ใส่ค่าต่างกันไปทำให้มันมีคุณสมบัติไม่เหมือนกัน

แม้ว่า ni2 และ ni7 จะเกิดจากฟังก์ชัน nai เหมือนกัน แต่ว่าในเวลาที่ฟังก์ชัน ni2 ถูกสร้างขึ้นมามันได้จดจำว่า a มีค่าเป็น 2 ส่วน ni7 จะจำว่า a มีค่าเป็น 7

ดังนั้นจะกลายเป็นว่าเหมือนเทียบเท่ากับการที่เราสร้างฟังก์ชันด้วย def ขึ้นมา ๒ ครั้ง
def ni2():
    print(2)
def ni7():
    print(7)

นั่นหมายความว่าหากใช้วิธีนี้เราสามารถสร้างฟังก์ชันที่มีความหลากหลายขึ้นมาได้จากการนิยามด้วย def แค่ครั้งเดียว

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

อย่างไรก็ตามเนื่องจากนิยามมาจาก nai เหมือนกันดังนั้นหากลอง print ดูข้อมูลของตัวฟังก์ชันจะพบว่าชื่อก็ยังเป็น nai เหมือนกัน แต่ถึงอย่างนั้นก็มีการเก็บค่าคนละที่แยกต่างหาก
print(ni2) # ได้ .nai at 0x112770d08>
print(ni7) # ได้ .nai at 0x112770048>
print(ni2.__name__) # ได้ nai
print(ni7.__name__) # ได้ nai 

คราวนี้จะลองทำฟังก์ชันขึ้นมาทีเดียวหลายอันแล้วสั่งให้ทำงานพร้อมกันดูก็ทำได้
def nok(a):
    def nai():
        print(a, end='>')
    return nai

nini = [nok(i) for i in range(10)]
for f in nini:
    f()
# ได้ 0>1>2>3>4>5>6>7>8>9>

แนวทางนี้ถือว่าน่าสนใจทีเดียว เพราะสามารถนำไปประยุกต์อะไรต่างๆได้

หนึ่งในการประยุกต์ที่ใช้งานได้ดีและถูกใช้อย่างกว้างขวางก็คือสิ่งที่เรียกว่าเดคอเรเตอร์ (decorator) ซึ่งเป็นหัวข้อใหญ่ที่จะพูดถึงในบทถัดไป



อ้างอิง




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

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

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

หมวดหมู่

-- คอมพิวเตอร์ >> เขียนโปรแกรม >> 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月

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

ไทย

日本語

中文