ในบทที่ผ่านๆมาได้พูดถึงเกี่ยวกับเรื่องของฟังก์ชันไปเยอะ และ
บทที่ ๑๙ ได้เริ่มพูดถึงการสร้างฟังก์ชัน แต่นั่นก็ยังเป็นเพียงเบื้องต้นเท่านั้น ในบทนี้จะมาพูดถึงเรื่องฟังก์ชันต่อ โดยเข้าถึงเนื้อหาที่ยากขึ้นไปอีก
ฟังก์ชันในฐานะตัวแปร ในภาษาไพธอน ฟังก์ชันถือว่าเป็นออบเจ็กต์ชนิดหนึ่ง สามารถถูกนำมาใช้ทำอะไรต่างๆได้เหมือนกับตัวแปรชนิดอื่นๆ
สามารถทำการสำเนาฟังก์ชันได้ง่ายโดยการใช้เครื่องหมายเท่ากับ
=
ตัวแปรที่มารับจะกลายเป็นฟังก์ชันซึ่งใช้งานได้เหมือนกับฟังก์ชันที่นิยามตอนแรก
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) ซึ่งเป็นหัวข้อใหญ่ที่จะพูดถึงในบทถัดไป
อ้างอิง