φυβλαςのβλογ
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
蒙古语
语言学
maya
概率论
与日本相关的日记
与中国相关的日记
-- 与北京相关的日记
-- 与香港相关的日记
-- 与澳门相关的日记
与台湾相关的日记
与北欧相关的日记
与其他国家相关的日记
qiita
其他日志

按类别分日志



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

  查看日志

  推荐日志

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