Processing math: 100%

User Tools

Site Tools


python:function

3. ฟังก์ชัน

ในบริบทของการเขียนโปรแกรม ฟังก์ชัน (function) เป็นลำดับของคำสั่งต่างๆ ที่ทำงาน ต่อเนื่องกันที่ถูกกำหนดชื่อให้ เมื่อเรานิยามฟังก์ชัน เราต้องระบุชื่อและลำดับของคำสั่งต่างๆ หลังจากนั้น เราสามารถ “เรียก” ฟังก์ชันโดยใช้ชื่อที่ระบุไว้ได้

3.1 การเรียกฟังก์ชัน

เราได้เห็นตัวอย่างหนึ่งของการเรียกฟังก์ชันไปแล้ว:

>>> type(42)
<class 'int'>

ชื่อของฟังก์ชันนี้คือ type นิพจน์ในวงเล็บ เรียกว่า อาร์กิวเมนต์ (argument) ของฟังก์ชัน ผลลัพธ์ของฟังก์ชันนี้ คือ ชนิดของฟังก์ชัน (ในกรณีนี้ คือ ชนิดจำนวนเต็ม (int))

โดยปกติแล้วเราพูดได้ว่า ฟังก์ชันจะ “รับ” อาร์กิวเมนต์ และ “ส่งคืน” ค่าผลลัพธ์ของฟังก์ชัน ผลลัพธ์นี้ยังสามารถเรียกว่า “ค่าคืนกลับ” ได้อีกด้วย

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

>>> int('32')
32
>>> int('Hello')
ValueError: invalid literal for int(): Hello

ฟังก์ชัน int สามารถแปลงเลขจำนวนจุดลอย (floating-point) ไปเป็นจำนวนเต็มได้ แต่มันไม่ได้ใช้การปัดเศษ (ขึ้นหรือลง) มันจะตัดจุดทศนิยมออกไปเลย

>>> int(3.99999)
3
>>> int(-2.3)
-2

ฟังก์ชัน float แปลงจำนวนเต็มและสายอักขระไปเป็นจำนวนจุดลอย

>>> float(32)
32.0
>>> float('3.14159')
3.14159

อย่างสุดท้าย ฟังก์ชัน str แปลงอาร์กิวเมนต์ของมันไปเป็นสายอักขระ

>>> str(32)
'32'
>>> str(3.14159)
'3.14159'

3.2 ฟังก์ชันทางคณิตศาสตร์

ไพธอนมีมอดูลสำหรับฟังก์ชันทางคณิตศาสตร์ที่เราคุ้นเคยกัน มอดูล (module) เป็นไฟล์ที่บรรจุกลุ่มของฟังก์ชันที่เกี่ยวข้องกันไว้ด้วยกัน

ก่อนที่เราจะใช้ฟังก์ชันจากมอดูลใดๆ เราจะต้องนำเข้า (import) มอดูลนั้นก่อน ด้วย คำสั่งนำเข้า (import statement)

>>> import math

คำสั่งนี้สร้าง วัตถุมอดูล (module object) ที่ชื่อว่า math ถ้าเราสั่งให้แสดงวัตถุมอดูลอันนี้ เราจะได้ข้อมูลเกี่ยวกับตัววัตถุออกมา:

>>> math
<module 'math' (built-in)>

วัตถุมอดูลบรรจุฟังก์ชันต่างๆ และตัวแปรต่างๆ ที่ถูกนิยามขึ้นในมอดูล ในการเข้าถึงฟังก์ชันหนึ่งๆ ที่อยู่ในมอดูลนี้ เราจะต้องระบุชื่อของมอดูลและชื่อของฟังก์ชันที่เราต้องการ โดยการคั่นด้วยจุด รูปแบบดังกล่าว เรียกว่า สัญกรณ์จุด (dot notation)

>>> ratio = signal_power / noise_power
>>> decibels = 10 * math.log10(ratio)
 
>>> radians = 0.7
>>> height = math.sin(radians)

ตัวอย่างแรกใช้ฟังก์ชัน math.log10 ในการคำนวณอัตราส่วนของสัญญานต่อการรบกวน ในหน่วยเดซิเบล (สมมติว่า signal_power และ noise_power ได้ถูกนิยามไปแล้ว) มอดูล math ได้จัดเตรียมฟังก์ชัน log ซึ่งจะคำนวณค่า log ฐาน e ไว้ให้แล้ว

ตัวอย่างที่สองเป็นการหาค่า sin ของตัวแปร radians ซึ่งชื่อของตัวแปรนี้ใบ้ให้เราทราบว่า ฟังก์ชัน sin และฟังก์ชันอื่นในตรีโกณมิติ (cos, tan, และอื่นๆ) นั้น รับอาร์กิวเมนต์ในหน่วยเรเดียน (radian) การแปลงจากหน่วยองศา (degree) มาเป็นหน่วยเรเดียนนั้น เราจะหารด้วย 180 และคูณด้วย π:

>>> degrees = 45
>>> radians = degrees / 180.0 * math.pi
>>> math.sin(radians)
0.707106781187

นิพจน์ math.pi นำตัวแปร pi มาจากมอดูล math ค่าของมันเป็นค่าจุดลอย ที่ประมาณค่าของ π มาอีกที โดยมีจุดทศนิยมประมาณ 15 ตำแหน่ง

ถ้าเรารู้จักตรีโกณมิติ เราจะสามารถตรวจสอบผลการทำงานก่อนหน้านี้ โดยเปรียบเทียบกับค่ารากที่สองของ 2 แล้วหารด้วย 2:

>>> math.sqrt(2) / 2.0
0.707106781187

3.3 การประกอบ

ถึงตอนนี้ เราได้ดูส่วนประกอบต่างๆ ของโปรแกรม—ตัวแปร นิพจน์ คำสั่ง—แบบแยกกัน โดยที่ไม่ได้พูดถึงว่า เราจะประกอบมันเข้าด้วยกันได้อย่างไร

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

x = math.sin(degrees / 360.0 * 2 * math.pi)

และแม้แต่การเรียกฟังก์ชัน:

x = math.exp(math.log(x+1))

ในเกือบทุกที่ เราสามารถใส่ค่านิพจน์ใดๆ ได้ ยกเว้นหนึ่งที่: ทางซ้ายของการกำหนดค่า จะต้องเป็นชื่อตัวแปร การวางนิพจน์แบบอื่นทางซ้ายนั้น จะเป็นข้อผิดพลาดเชิงวากยสัมพันธ์ (syntax error) (เราจะได้เห็นข้อยกเว้นของกฎข้อนี้ในภายหลัง)

>>> minutes = hours * 60                 # right
>>> hours * 60 = minutes                 # wrong!
SyntaxError: can't assign to operator

3.4 การเพิ่มฟังก์ชันใหม่

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

นี่คือตัวอย่าง:

def print_lyrics():
    print("I'm a lumberjack, and I'm okay.")
    print("I sleep all night and I work all day.")

def เป็นคำสำคัญที่ระบุว่า นี่คือ นิยามของฟังก์ชัน ชื่อของฟังก์ชัน คือ print_lyrics กฎการตั้งชื่อฟังก์ชันนั้นเหมือนกับกฎการตั้งชื่อตัวแปร: ตัวอักษร ตัวเลข และขีดล่างนั้นใช้ได้ แต่อักขระตัวแรกต้องไม่เป็นตัวเลข เราไม่สามารถใช้คำสำคัญเป็นชื่อของฟังก์ชัน และเราต้องหลีกเลี่ยง การใช้ชื่อตัวแปรและชื่อฟังก์ชันที่เหมือนกัน

วงเล็บว่างหลังชื่อฟังก์ชัน ระบุว่าฟังก์ชันนี้ไม่รับอาร์กิวเมนต์ใดๆ เข้ามาในฟังก์ชัน

บรรทัดแรกของนิยามฟังก์ชันเรียกว่า ส่วนหัว (header); ที่เหลือเรียกว่า ส่วนตัว (body) ส่วนหัวจะต้องลงท้ายด้วยเครื่องหมายทวิภาค (จุดคู่ หรือ โคลอน) (:) และส่วนตัวของฟังก์ชันจะต้อง ย่อหน้าเข้าไป การย่อหน้าเข้าไปนิยมใช้ช่องว่าง (space) 4 ตำแหน่ง ตัวของฟังก์ชันสามารถบรรจุคำสั่ง กี่คำสั่งก็ได้

(หมายเหตุผู้แปล: การย่อหน้าสามารถใช้ tab ได้ แต่ต้องเลือกการย่อหน้าให้เป็นรูปแบบเดียวกันทั้งไฟล์)

สายอักขระที่อยู่ในคำสั่งพิมพ์ถูกใส่อยู่ในเครื่องหมายอัญประกาศ อัญประกาศเดี่ยว (single quotes) และอัญประกาศ (double quotes) ทำงานเหมือนกัน คนส่วนใหญ่ใช้อัญประกาศเดี่ยว ยกเว้นในกรณีอย่างเช่น การมีเครื่องหมายอัญประกาศเดี่ยวตัวเดียว (apostrophe) ในสายอักขระ

อัญประกาศทุกแบบจะต้องเป็นเครื่องหมายที่เป็น “เส้นตรงๆ“ ซึ่งโดยปกติแล้วจะอยู่ถัดจากปุ่ม Enter บนคีย์บอร์ด ส่วนเครื่องหมายอัญประกาศ “แบบโค้ง” เช่นในประโยคนี้ จะผิดกฎของไพธอน

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

>>> def print_lyrics():
...     print("I'm a lumberjack, and I'm okay.")
...     print("I sleep all night and I work all day.")
...

ถ้าต้องการจบฟังก์ชัน ให้เรา enter บรรทัดเปล่าๆ เข้าไปหนึ่งบรรทัด

การนิยามฟังก์ชันจะสร้าง วัตถุฟังก์ชัน (function object) ที่มีชนิดเป็น function:

>>> print(print_lyrics)
<function print_lyrics at 0xb7e99e9c>
>>> type(print_lyrics)
<class 'function'>

กฎวากยสัมพันธ์ในการเรียกฟังก์ชันใหม่นั้นเหมือนกับการเรียกฟังก์ชันที่มีอยู่ในตัว (built-in function):

>>> print_lyrics()
I'm a lumberjack, and I'm okay.
I sleep all night and I work all day.

เมื่อเราได้นิยามฟังก์ชันแล้ว เราสามารถใช้มันในฟังก์ชันอื่นได้ด้วย เช่น ถ้าเราต้องการจะพิมพ์เนื้อเพลงบทที่แล้วอีกรอบ เราสามารถเขียนฟังก์ชันที่ชื่อว่า repeat_lyrics:

def repeat_lyrics():
    print_lyrics()
    print_lyrics()

จากนั้น ให้เรียก repeat_lyrics:

>>> repeat_lyrics()
I'm a lumberjack, and I'm okay.
I sleep all night and I work all day.
I'm a lumberjack, and I'm okay.
I sleep all night and I work all day.

แต่จริงๆ แล้วเนื้อเพลงมันไม่ได้เป็นแบบนั้นหรอกนะ

3.5 นิยามและการใช้งาน

เมื่อทำการรวบรวมโค้ดชิ้นต่างๆ จากเนื้อหาในส่วนที่ผ่านมา โปรแกรมที่สมบูรณ์ก็จะหน้าตาเป็นแบบนี้:

def print_lyrics():
    print("I'm a lumberjack, and I'm okay.")
    print("I sleep all night and I work all day.")
 
def repeat_lyrics():
    print_lyrics()
    print_lyrics()
 
repeat_lyrics()

โปรแกรมนี้ประกอบด้วยนิยามฟังก์ชัน 2 ตัว: print_lyrics และ repeat_lyrics นิยามฟังก์ชันทำงานเหมือนคำสั่งต่างๆ ทั่วไป แต่จะส่งผลให้เกิดการสร้างวัตถุฟังก์ชัน คำสั่งต่างๆ ที่อยู่ใน ฟังก์ชันจะไม่ทำงานจนกว่าฟังก์ชันนั้นจะถูกเรียก และตัวนิยามของฟังก์ชันเองจะไม่ทำให้เกิดผลลัพธ์ใดๆ

(หมายเหตุผู้แปล: นั่นคือ จะต้องมีการเรียกฟังก์ชันก่อน ถึงจะมีการทำงานใดๆ นั่นเอง)

เราอาจจะพอคาดได้ว่า เราจะต้องสร้างฟังก์ชันก่อนที่เราจะเรียกมันใช้งาน อีกนัยหนึ่งคือ นิยามของฟังก์ชัน จะต้องถูกรันก่อนที่ฟังก์ชันนั้นจะถูกเรียกใช้งาน

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

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

3.6 กระแสการดำเนินการ

เพื่อที่ให้แน่ใจว่าฟังก์ชันนั้นถูกนิยามก่อนการใช้งานครั้งแรก เราต้องรู้ลำดับการทำงานของคำสั่งก่อน ซึ่งเรียกว่า กระแสการดำเนินการ (flow of execution)

การดำเนินการของคำสั่งเริ่มจากคำสั่งแรกสุดของโปรแกรมเสมอ คำสั่งจะทำงานทีละคำสั่ง เรียงลำดับจากบนลงล่าง

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

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

ก็ฟังดูง่ายนะ จนกระทั่งเราจำได้ว่าฟังก์ชันหนึ่งสามารถเรียกอีกฟังก์ชันหนึ่งได้ด้วย เมื่ออยู่ระหว่าง การทำงานในฟังก์ชันหนึ่งๆ โปรแกรมอาจจะทำคำสั่งที่ต้องเรียกฟังก์ชันอื่น จากนั้นในขณะที่ทำงานใน ฟังก์ชันใหม่นั้น โปรแกรมก็อาจจะเรียกทำงานฟังก์ชันอันอื่นอีก!

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

โดยสรุปแล้ว เมื่อเราอ่านโปรแกรม เราไม่จำเป็นจะต้องอ่านจากบนลงล่างเสมอไป บางครั้งมันเข้าท่าที่จะอ่านตามกระแสการดำเนินการ (flow of execution)

3.7 พารามิเตอร์และอาร์กิวเมนต์

ฟังก์ชันบางอันที่เราได้เห็นมานั้นต้องการอาร์กิวเมนต์ เช่น เมื่อเราเรียกฟังก์ชัน math.sin เราจะต้องผ่านเลขเข้าไปเป็นอาร์กิวเมนต์ ฟังก์ชันบางตัวรับค่ามากกว่าหนึ่งอาร์กิวเมนต์: ฟังก์ชัน math.pow รับค่าเข้ามาสองตัว คือ เลขฐานและเลขยกกำลัง

ในฟังก์ชัน อาร์กิวเมนต์จะถูกกำหนดตัวแปรให้ เรียกว่า พารามิเตอร์ (parameters) นี่คือนิยามของฟังก์ชันที่รับอาร์กิวเมนต์เข้ามา

def print_twice(bruce):
    print(bruce)
    print(bruce)

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

ฟังก์ชันนี้จะทำงานได้กับค่าอะไรก็ตามที่สามารถพิมพ์ออกมาได้

>>> print_twice('Spam')
Spam
Spam
>>> print_twice(42)
42
42
>>> print_twice(math.pi)
3.14159265359
3.14159265359

กฎของการประกอบ (rules of composition) อันเดียวกันกับที่ใช้กับฟังก์ชันที่มีอยู่ในตัว (built-in function) สามารถใช้ได้กับฟังก์ชันที่นักเขียนโปรแกรมนิยามขึ้นมาเอง ดังนั้น เราสามารถใช้นิพจน์ใดๆ เป็นอาร์กิวเมนต์สำหรับ ฟังก์ชัน print_twice:

>>> print_twice('Spam '*4)
Spam Spam Spam Spam
Spam Spam Spam Spam
>>> print_twice(math.cos(math.pi))
-1.0
-1.0

อาร์กิวเมนต์จะถูกประเมินค่าก่อนที่ฟังก์ชันจะถูกเรียก ดังนั้น ในตัวอย่างนี้ นิพจน์ 'Spam '*4 และ math.cos(math.pi) จะถูกประเมินค่าแค่ครั้งเดียว

(หมายเหตุผู้แปล: หลังจากประเมินค่า โปรแกรมจะส่งค่าที่ประเมินแล้วเข้าไปทำงานในฟังก์ชัน)

เราสามารถใช้ตัวแปรเป็นอาร์กิวเมนต์ได้ด้วย:

>>> michael = 'Eric, the half a bee.'
>>> print_twice(michael)
Eric, the half a bee.
Eric, the half a bee.

ชื่อของตัวแปรที่เราผ่านเข้าไปเป็นอาร์กิวเมนต์ (michael) ไม่มีอะไรเกี่ยวข้องกับชื่อของพารามิเตอร์ (bruce) มันไม่เกี่ยวว่าค่าหนึ่งๆ ถูกเรียกว่าอะไรในที่เดิม (ในส่วนที่เรียกฟังก์ชัน); ณ ที่นี่ ในฟังก์ชัน print_twice เราเรียกทุกตัว (ที่ถูกส่งเข้ามา) ว่า bruce

3.8 ตัวแปรและพารามิเตอร์เป็นค่าเฉพาะที่

เมื่อเราสร้างตัวแปรข้างในฟังก์ชัน มันจะมีค่า เฉพาะที่ (local) ซึ่งหมายความว่ามันจะมีตัวตนแค่ในฟังก์ชันนี้เท่านั้น เช่น:

def cat_twice(part1, part2):
    cat = part1 + part2
    print_twice(cat)

ฟังก์ชันนี้รับอาร์กิวเมนต์เข้ามาสองตัว เชื่อมต่อมันเข้าด้วยกัน และพิมพ์ผลลัพธ์สองครั้ง นี่คือตัวอย่างที่ใช้ฟังก์ชันนี้:

>>> line1 = 'Bing tiddle '
>>> line2 = 'tiddle bang.'
>>> cat_twice(line1, line2)
Bing tiddle tiddle bang.
Bing tiddle tiddle bang.

เมื่อฟังก์ชัน cat_twice จบการทำงาน ตัวแปร cat จะถูกทำลาย ถ้าเราพยายามที่จะพิมพ์มันออกมา เราจะได้ข้อยกเว้น (ข้อผิดพลาดตอนโปรแกรมทำงาน):

>>> print(cat)
NameError: name 'cat' is not defined

พารามิเตอร์ก็เป็นค่าเฉพาะที่เหมือนกัน เช่น ภายนอกฟังก์ชัน print_twice มันไม่มีสิ่งที่เรียกว่า bruce

3.9 แผนภาพแบบกองซ้อน

เพื่อที่จะติดตามว่าตัวแปรไหนใช้ที่ไหนได้บ้าง บางครั้งมันก็เป็นประโยชน์ที่จะวาด แผนภาพแบบกองซ้อน (stack diagram) เช่นเดียวกับแผนภาพสถานะ แผนภาพแบบกองซ้อนแสดงค่าของแต่ละตัวแปร แต่มันยังแสดงว่าตัวแปรนั้นเป็นของฟังก์ชันไหน

แต่ละฟังก์ชันถูกแสดงด้วย กรอบ (frame) กรอบเป็นกล่องที่มีชื่อฟังก์ชันอยู่ข้างๆ และมีพารามิเตอร์และตัวแปร ของฟังก์ชันนั้นอยู่ข้างใน แผนภาพแบบกองซ้อนสำหรับตัวอย่างที่แล้วแสดงอยู่ในรูปที่ 3.1

 แผนภาพแบบกองซ้อน (Stack diagram)
รูปที่ 3.1 แผนภาพแบบกองซ้อน (Stack diagram)

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

พารามิเตอร์แต่ละตัวอ้างอิงถึงค่าเดียวกันกับค่าของอาร์กิวเมนต์ที่ผ่านเข้ามา ณ ตำแหน่งเดียวกัน ดังนั้น part1 มีค่าเดียวกันกับ line1, part2 มีค่าเดียวกันกับ line2, และ bruce มีค่าเดียวกันกับ cat

ถ้ามีข้อผิดพลาดเกิดขึ้นระหว่างที่มีการเรียกฟังก์ชัน ไพธอนจะพิมพ์ชื่อฟังก์ชันที่มีปัญหา ชื่อฟังก์ชันที่เรียกฟังก์ชันที่มีปัญหา และชื่อฟังก์ชันที่เรียกฟังก์ชัน เมื่อกี้นี้ และย้อนกลับไปเรื่อยๆ จนถึง __main__

ตัวอย่างเช่น ถ้าเราพยายามที่จะเข้าถึง cat จากในฟังก์ชัน print_twice เราจะได้ ข้อผิดพลาดแบบทางชื่อ หรือ NameError:

Traceback (innermost last):
  File "test.py", line 13, in __main__
    cat_twice(line1, line2)
  File "test.py", line 5, in cat_twice
    print_twice(cat)
  File "test.py", line 9, in print_twice
    print(cat)
NameError: name 'cat' is not defined

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

ลำดับของฟังก์ชันต่างๆ ในการย้อนรอยเป็นลำดับเดียวกับลำดับของกรอบในแผนภาพแบบกองซ้อน ฟังก์ชันที่กำลังทำงานอยู่จะอยู่ข้างล่างสุด

3.10 ฟังก์ชันที่ให้ผลและฟังก์ชันที่ไม่ให้ผล

บางฟังก์ชันที่เราใช้กันมา เช่น ฟังก์ชันทางคณิตศาสตร์ มีการคืนผลลัพธ์กลับมาให้เรา เนื่องจากไม่มีชื่อที่ดีกว่านี้ ผมจะเรียกพวกมันว่า ฟังก์ชันที่ให้ผล (fruitful functions) ส่วนฟังก์ชันอื่นๆ จำพวกฟังก์ชัน print_twice จะทำงานต่างๆ โดยไม่มีการคืนค่ากลับมา พวกมันเรียกว่า ฟังก์ชันที่ไม่ให้ผล หรือฟังก์ชันวอยด์ (void functions)

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

x = math.cos(radians)
golden = (math.sqrt(5) + 1) / 2

เมื่อเราเรียกฟังก์ชันในโหมดโต้ตอบ ไพธอนจะแสดงผลออกมา

>>> math.sqrt(5)
2.2360679774997898

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

math.sqrt(5)

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

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

>>> result = print_twice('Bing')
Bing
Bing
>>> print(result)
None

ค่า None ไม่ใช่ค่าเดียวกับสายอักขระ 'None' แต่มันเป็นค่าพิเศษที่มีชนิดของข้อมูลเป็นของมันเอง:

>>> type(None)
<class 'NoneType'>

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

3.11 ทำไมต้องใช้ฟังก์ชัน

มันอาจจะไม่ชัดเจนเท่าไหร่ว่าทำไมมันถึงคุ้มค่ากับการปวดหัวเพื่อที่จะแบ่งโปรแกรมเป็นฟังก์ชันต่างๆ มันมีเหตุผลหลายประการ ดังนี้:

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

3.12 การดีบัก

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

ในบางแง่มุม การดีบักเหมือนกับงานสืบสวน เราจะเจอเบาะแสและเราจะต้องวินิจฉัยกระบวนการ และเหตุการณ์ที่ทำให้เกิดผลลัพธ์แบบที่เราเห็นอยู่นี้

(หมายเหตุผู้แปล: คิดว่าผลนี้มันเกิดมาจากไหน ได้อย่างไร)

การดีบักยังเป็นเหมือนกับการทดลองทางวิทยาศาสตร์ เมื่อเรามีความคิดว่าอะไรที่น่าจะผิดพลาด เราจะแก้โปรแกรม และลองดูอีก ถ้าสมมติฐานของเราถูก เราจะสามารถทำนายผลของการแก้ไขนั้นได้ และเราก็ได้ก้าวเข้าใกล้ โปรแกรมที่สามารถทำงานได้อีกก้าวหนึ่งแล้ว ถ้าสมมติฐานของเราผิด เราก็ต้องคิดสมมติฐานขึ้นมาใหม่ เป็นอย่างที่เชอร์ล็อค โฮล์มส์ชี้ให้เห็นว่า “เมื่อเรากำจัดความเป็นไปไม่ได้แล้ว, อะไรก็ตามที่เหลือ, ไม่ว่ามันจะ ดูเป็นไปไม่ได้ขนาดไหน, ก็จะต้องเป็นความจริง“ (A. Conan Doyle, The Sign of Four)

สำหรับบางคนแล้ว การเขียนโปรแกรมและการดีบักเป็นอย่างเดียวกัน นั่นคือ การเขียนโปรแกรมเป็นกระบวนการของการ ค่อยๆ ดีบักโปรแกรมจนกว่ามันจะทำสิ่งที่เราต้องการได้ หลักการคือ เราควรจะเริ่มจากโปรแกรมเล็กๆ ที่ทำงานได้ และแก้ไขไปทีละน้อย นั่นคือการดีบักไปพร้อมกับการเขียนโปรแกรม

(หมายเหตุผู้แปล: เขียนเพิ่มและดีบักเพิ่มทีละเล็กทีละน้อย)

ตัวอย่างเช่น ลินิกซ์ (Linux) เป็นระบบปฏิบัติการที่มีโค้ดเป็นล้านๆ บรรทัด แต่มันเริ่มมาจากโปรแกรมเล็กๆ ที่ ลินัส ทอร์วัลด์ส (Linus Torvalds) ใช้สำรวจชิป Intel 80386 จากคำบอกเล่าของ ลาร์รี กรีนฟิลด์ (Larry Greenfield) “โครงงานแรกๆ ของลินัส คือ โปรแกรมที่จะสับเปลี่ยนระหว่างการพิมพ์ AAAA และ BBBB โปรแกรมนี้ได้วิวัฒนาการมาเป็นลินิกซ์” (The Linux Users’ Guide Beta Version 1)

3.13 อภิธานศัพท์

  • ฟังก์ชัน (function): ลำดับของคำสั่งที่มีชื่อเรียก ซึ่งทำงานอะไรสักอย่างที่มีประโยชน์ ฟังก์ชันอาจจะรับหรือไม่รับอาร์กิวเมนต์ และอาจจะมีผลลัพธ์หรือไม่ก็ได้
  • นิยามฟังก์ชัน (function definition): คำสั่งที่ใช้สร้างฟังก์ชันใหม่ โดยระบุชื่อ พารามิเตอร์ และคำสั่งที่อยู่ในฟังก์ชัน
  • วัตถุฟังก์ชัน (function object): ค่าที่ถูกสร้างโดยนิยามฟังก์ชัน ชื่อฟังก์ชันคือตัวแปรที่อ้างอิงถึงวัตถุฟังก์ชัน
  • ส่วนหัว (header): บรรทัดแรกของนิยามฟังก์ชัน
  • ส่วนตัว (body): กลุ่มลำดับของคำสั่งที่อยู่ในนิยามฟังก์ชัน
  • พารามิเตอร์ (parameter): ชื่อที่ถูกใช้ในฟังก์ชันเพื่ออ้างอิงถึงค่าที่ถูกผ่านเข้ามาเป็นอาร์กิวเมนต์
  • การเรียกฟังก์ชัน (function call): คำสั่งที่ทำให้ฟังก์ชันทำงาน ประกอบด้วย ชื่อฟังก์ชัน ตามด้วยรายการ อาร์กิวเมนต์ในวงเล็บ
  • อาร์กิวเมนต์ (argument): ค่าที่ส่งผ่านไปให้ฟังก์ชันเมื่อฟังก์ชันถูกเรียกใช้งาน ค่านี้ถูกกำหนดให้กับพารามิเตอร์ที่ ตรงตำแหน่งกันในฟังก์ชัน
  • ตัวแปรเฉพาะที่ (local variable): ตัวแปรที่ถูกนิยามขึ้นภายในฟังก์ชัน ตัวแปรเฉพาะที่สามารถถูกใช้งานได้ ภายในฟังก์ชันของมันเท่านั้น
  • ค่าคืนกลับ (return value): ผลลัพธ์ของฟังก์ชัน ถ้าการเรียกฟังก์ชันนั้นถูกใช้เป็นนิพจน์ ค่าคืนกลับจะกลายมาเป็นค่าของนิพจน์นั้นเลย
  • ฟังก์ชันที่ให้ผล (fruitful function): ฟังก์ชันที่คืนค่ากลับมา
  • ฟังก์ชันที่ไม่ให้ผล หรือ ฟังก์ชันวอยด์ (void function): ฟังก์ชันที่คืนค่ากลับมาเป็น None เสมอ
  • None: ค่าพิเศษที่ถูกคืนกลับมาโดยฟังก์ชันวอยด์
  • มอดูล (module): ไฟล์ที่บรรจุฟังก์ชันและนิยามต่างๆ ที่เกี่ยวข้องกัน
  • คำสั่งนำเข้า (import statement): คำสั่งที่อ่านไฟล์มอดูลและสร้างวัตถุมอดูล
  • วัตถุมอดูล (module object): ค่าที่ถูกสร้างขึ้นโดยคำสั่ง import ที่เอื้อให้สามารถเข้าถึงค่าที่ถูกนิยามในมอดูล
  • สัญกรณ์จุด (dot notation): กฎวากยสัมพันธ์สำหรับเรียกฟังก์ชันในมอดูลอื่น ซึ่งให้ระบุชื่อของมอดูล ตามด้วยเครื่องหมายจุดและชื่อฟังก์ชันที่ต้องการเรียก
  • การประกอบ (composition): การใช้นิพจน์เป็นส่วนประกอบของนิพจน์อีกอันที่ใหญ่กว่า หรือคำสั่งที่เป็นส่วนประกอบของอีกคำสั่งที่ใหญ่กว่า
  • กระแสการดำเนินการ (flow of execution): ลำดับการทำงานของคำสั่งต่างๆ
  • แผนภาพแบบกองซ้อน (stack diagram): การใช้รูปภาพแสดงกองของฟังก์ชัน ตัวแปรของแต่ละฟังก์ชัน และค่าที่ถูกอ้างถึง
  • กรอบ (frame): กล่องในแผนภาพแบบกองซ้อนที่แสดงการเรียกฟังก์ชันหนึ่งๆ ซึ่งประกอบด้วยตัวแปรเฉพาะที่ และพารามิเตอร์ของฟังก์ชัน
  • การย้อนรอย (traceback): รายการของฟังก์ชันต่างๆ ที่ทำงานอยู่ โดยจะถูกพิมพ์ออกมาเมื่อเกิดข้อยกเว้น (ความผิดพลาดตอนโปรแกรมทำงาน)

3.14 แบบฝึกหัด

แบบฝึกหัด 1
ให้เขียนฟังก์ชันที่ชื่อว่า right_justify ซึ่งรับสายอักขระที่ชื่อว่า s เป็นพารามิเตอร์ และพิมพ์ สายอักขระดังกล่าวโดยมีช่องว่างนำหน้า แล้วทำให้อักษรตัวสุดท้ายของสายอักขระอยู่ในหลักที่ 70 ของการแสดงผล

>>> right_justify('monty')
                                                                 monty

คำใบ้: ใช้การเชื่อมต่อสายอักขระและการทำซ้ำ นอกจากนี้ ไพธอนได้เตรียมฟังก์ชันที่ชื่อว่า len ซึ่งจะคืนค่า ความยาวของสายอักขระ ดังนั้น ค่าของ len('monty') คือ 5

แบบฝึกหัด 2
วัตถุฟังก์ชัน คือ ค่าที่เราสามารถกำหนดให้กับตัวแปร หรือผ่านเข้าไปเป็นอาร์กิวเมนต์ เช่น ฟังก์ชัน do_twice เป็นฟังก์ชันที่รับวัตถุฟังก์ชันเป็นอาร์กิวเมนต์ และเรียกฟังก์ชันนั้นสองรอบ (ฟังก์ชัน f() จะทำงาน 2 ครั้ง)

def do_twice(f):
    f()
    f()

นี่คือตัวอย่างที่ใช้ฟังก์ชัน do_twice เพื่อเรียกฟังก์ชัน print_spam ให้ทำงานสองครั้ง

def print_spam():
    print('spam')
 
do_twice(print_spam)
  1. ให้เขียนตัวอย่างนี้ลงในสคริปต์ และทดสอบโปรแกรมดู
  2. ให้แก้ไขฟังก์ชัน do_twice ให้รับอาร์กิวเมนต์เข้ามา 2 ตัว เป็นวัตถุฟังก์ชัน 1 ตัว และค่า 1 ค่า ให้เรียกฟังก์ชันที่รับเข้ามา 2 รอบ โดยผ่านอีกค่าที่รับเข้ามาเป็นอาร์กิวเมนต์ของฟังก์ชันดังกล่าว
  3. ให้คัดลอกนิยามของฟังก์ชัน print_twice จากก่อนหน้านี้ในบทนี้แล้วใส่ในสคริปต์ของเรา
  4. ให้ใช้ฟังก์ชัน do_twice ที่แก้ไขไปก่อนหน้านี้เพื่อเรียกฟังก์ชัน print_twice สองครั้ง โดยให้ผ่าน 'spam' เป็นอาร์กิวเมนต์
  5. ให้นิยามฟังก์ชันใหม่ที่ชื่อว่า do_four ซึ่งรับวัตถุฟังก์ชัน 1 ตัว และค่า 1 ค่า และเรียกฟังก์ชันที่รับเข้ามา 4 รอบโดยผ่านอีกค่าที่รับเข้ามาเป็นพารามิเตอร์ จะต้องมีคำสั่งแค่ 2 คำสั่งเท่านั้นในตัวฟังก์ชันนี้ ไม่ใช่ 4 คำสั่ง

เฉลย: http://thinkpython2.com/code/do_four.py

แบบฝึกหัด 3
หมายเหตุ: ข้อนี้จะต้องใช้คำสั่งและลักษณะเฉพาะที่เราเรียนมาจนถึงตอนนี้เท่านั้น

1. ให้เขียนฟังก์ชันที่วาดรูปตารางดังต่อไปนี้

+ - - - - + - - - - +
|         |         |
|         |         |
|         |         |
|         |         |
+ - - - - + - - - - +
|         |         |
|         |         |
|         |         |
|         |         |
+ - - - - + - - - - +

คำใบ้: ในการพิมพ์ค่ามากกว่า 1 ค่าบนหนึ่งบรรทัด เราสามารถพิมพ์ค่าหลายค่าต่อกันโดยคั่นด้วยเครื่องหมายจุลภาค (,):

print('+', '-')

คำสั่ง print ขึ้นบรรทัดใหม่โดยปริยาย แต่เราสามารถยกเลิกพฤติกรรมนี้ และใส่ช่องว่างในตอนท้ายของการพิมพ์ได้ โดยทำแบบนี้:

print('+', end=' ')
print('-')

ผลของคำสั่งเหล่านี้ คือ '+ -' คำสั่ง print ที่ไม่มีอาร์กิวเมนต์จะจบบรรทัดปัจจุบันและขึ้นบรรทัดใหม่

2. ให้เขียนฟังก์ชันที่วาดรูปตารางแบบรูปที่แล้ว แต่ให้มี 4 แถวและ 4 หลัก

เฉลย: http://thinkpython2.com/code/grid.py. ที่มา: แบบฝึกหัดข้อนี้ดัดแปลงมาจากแบบฝึกหัดใน Oualline, Practical C Programming, Third Edition, O’Reilly Media, 1997.

https://greenteapress.com/thinkpython2/html/thinkpython2004.html

python/function.txt · Last modified: 2021/08/30 09:55 (external edit)

Page Tools