ข้อผิดพลาดในโปรแกรมแบ่งออกใหญ่ๆเป็น ๓ ชนิด ดังที่ได้กล่าวไปแล้วในบทที่ ๒ ได้แก่
- ข้อผิดพลาดทางวากยสัมพันธ์ (syntax error)
- ข้อผิดพลาดขณะการทำงาน (runtime error)
- ข้อผิดพลาดเชิงตรรกะ (logical error)
ในบทนี้จะพูดถึงข้อผิดพลาดขณะการทำงาน และการจัดการกับมัน
ปกติเวลาที่เจอข้อผิดพลาดแบบนี้โปรแกรมจะหยุดทำงานทันที โดยส่งข้อความมาบอกว่ามีการผิดพลาดอะไรเกิดขึ้น
ข้อผิดพลาดที่เกิดขึ้นนั้นมีหลากหลายชนิดมาก ซึ่งบอกให้รู้ว่ามีอะไรผิดพลาด เพื่อจะได้แก้ไขได้ถูกต้อง มีส่วนช่วยในการพัฒนาโปรแกรม
ชนิดของข้อผิดพลาด ในไพธอนข้อผิดพลาดถือเป็นคลาสรูปแบบหนึ่ง ชนิดของความผิดพลาดถูกแบ่งเป็นคลาสต่างๆ และมีการแตกคลาสย่อยออกไปด้วย ในที่นี้ขอยกตัวอย่างที่เจอบ่อย
NameError
เกิดขึ้นเวลาที่เรียกใช้ตัวแปรที่ไม่มีอยู่ มักเจอเวลาที่ตั้งใจจะใช้สายอักขระแต่ลืมใส่เครื่องหมายคำพูด
print(สวัสดี) # ได้ NameError: name 'สวัสดี' is not defined
TypeError
เกิดขึ้นเวลาที่ใช้ออบเจ็กต์ผิดชนิด เช่นเวลาคำนวณแล้วเอาข้อมูลที่ไม่ควรจะบวกกันได้มาบวกกัน
print(1+'1') # ได้ TypeError: unsupported operand type(s) for +: 'int' and 'str'
ZeroDivisionError
เกิดขึ้นเวลาที่คำนวณแล้วมีการหาร 0
print(1/0) # ได้ ZeroDivisionError: division by zero
ValueError
เกิดขึ้นเมื่อใส่ค่าที่ไม่ควรจะใส่ลงไปในฟังก์ชัน
import math
math.asin(2) # ได้ ValueError: math domain error
ModuleNotFoundError
เกิดขึ้นเมื่อใช้คำสั่ง
import
แล้วมีข้อผิดพลาด เช่นพิมพ์ชื่อมอดูลที่ไม่มีอยู่หรือยังไม่ได้ลงเอาไว้ หรือพิมพ์ชื่อผิด
import mat # ได้ ModuleNotFoundError: No module named 'mat'
IndexError
เกิดขึ้นเวลาที่อ้างอิงข้อมูลชนิดลำดับเช่นลิสต์, ทูเพิล, สายอักขระ แล้วใส่ดัชนีเกินจากค่าที่มีอยู่จริง
a = (0,1,2,3)
print(a[4]) # ได้ IndexError: tuple index out of range
KeyError
เกิดขึ้นเวลาที่เรียกใช้ดิกชันนารีแล้วใส่คีย์ที่ไม่มีอยู่
b = {'ก':1,'ข':2,'ค':3}
b['ง'] # ได้ KeyError: 'ง'
FileNotFoundError
เกิดขึ้นเมื่อเวลาที่เปิดไฟล์แล้วไม่มีไฟล์นั้นอยู่
f = open('xxxx.txt','r',encoding='utf-8') # ได้FileNotFoundError: [Errno 2] No such file or directory: 'xxxx.txt'
นอกจากนี้ก็ยังมีอีกมากมาย ถ้าได้เจอก็จะได้ทำความรู้จักกับมันเอง (แม้ว่าอาจจะไม่ได้อยากเจอก็ตาม)
ความผิดพลาดบางชนิดเป็นชนิดใกล้เคียงกัน มีซูเปอร์คลาสร่วมกัน เช่น
IndexError
กับ
KeyError
เป็นซับคลาสของ
LookupError
print(issubclass(IndexError,LookupError)) # ได้ True
print(issubclass(KeyError,LookupError)) # ได้ True
print(LookupError.__subclasses__()) # ได้ [<class 'IndexError'>, <class 'KeyError'>, <class 'encodings.CodecRegistryError'>]
ความผิดพลาดทั้งหมดเป็นคลาสย่อยของคลาสที่ชื่อ
Exception
คลาสนี้เป็นคลาสรวมของความผิดพลาดพื้นฐานทุกชนิด
print(issubclass(IndexError,Exception)) # ได้ True
print(issubclass(LookupError,Exception)) # ได้ True
print(Exception.__subclasses__()) # ได้คลาสของความผิดพลาดชนิดต่างๆออกมามากมาย
ความจริงแล้ว
Exception
ยังเป็นซับคลาสของคลาสที่ใหญ่กว่าคือ
BaseException
ซึ่งยังรวมความผิดพลาดที่นอกเหนือจากนี้ไป ซึ่งในที่นี้จะยังไม่พูดถึง
print(BaseException.__subclasses__())
ได้
[<class 'BaseExceptionGroup'>, <class 'Exception'>, <class 'GeneratorExit'>, <class 'KeyboardInterrupt'>, <class 'SystemExit'>, <class 'asyncio.exceptions.CancelledError'>, <class 'pkg_resources._vendor.more_itertools.more.AbortThread'>]
การสร้างข้อผิดพลาดขึ้นมาเอง ความผิดพลาดที่กระทบต่อการทำงานภายในโปรแกรมนั้นสามารถถูกตรวจจับและขึ้นเตือนได้ทั้งหมดดังที่ได้ยกตัวอย่างมาแล้ว
แต่นอกเหนือจากนั้นอาจมีความผิดพลาดที่เราไม่ต้องการ แต่โปรแกรมก็ทำงานไปตามปกติไม่ได้เตือนว่ามีการขัดข้องอะไร จัดเป็นข้อผิดพลาดเชิงตรรกะ
เช่นลองเขียนโปรแกรมง่ายๆที่คำนวณยอดเงินคงเหลือหลังกดตัง ซึ่งก็คือเงินในบัญชีลบด้วยเงินที่ถอนไป
banchi = 10000
thon = int(input())
khongluea = banchi - thon
print(khongluea)
ถ้าเงินที่ถอนน้อยกว่าเงินในบัญชีก็ไม่มีปัญหาอะไร แต่ถ้าเงินที่ถอนมากกว่าเมื่อไหร่ ก็จะได้เลขติดลบ เช่นใส่
12000
ก็จะได้ผลเป็น
-2000
ซึ่งในความเป็นจริงเป็นไปไม่ได้ที่จะถอนเงินเกิน
ดังนั้นเราต้องเขียนให้โปรแกรมรู้ด้วยว่าจะถอนเงินเกินแบบนี้ไม่ได้
โดยเราสามารถกำหนดสถานการณ์ที่จะให้โปรแกรมแสดงข้อผิดพลาดขึ้นมาได้เองโดยใช้คำ สั่ง
raise
โดยเขียน
raise
แล้วตามด้วยชนิดของความผิดพลาด วงเล็บด้วยข้อความที่จะขึ้นเตือน
banchi = 10000
thon = int(input())
khongluea = banchi - thon
if(khongluea<0):
raise ValueError('ยอดเงินในบัญชีไม่พอ')
else:
print(khongluea)
ValueError
ในที่นี้คือชนิดของความผิดพลาดซึ่งเราสามารถกำหนดได้เอง ที่จริงจะกำหนดเป็นอะไรก็ได้ แต่ต้องเป็นชนิดที่เขากำหนดให้แต่แรก
ที่จริงแล้ว
ValueError
อาจไม่ค่อยถูกต้องนักที่จะใช้ในกรณีแบบนี้ ข้อผิดพลาดที่เราสร้างขึ้นเองส่วนใหญ่แล้วก็ไม่ได้ตรงกับรูปแบบที่มีให้อยู่แล้วแต่แรก
แต่เราสามารถสร้างรูปแบบความผิดพลาดขึ้นมาเองได้ด้วย ความผิดพลาดก็เป็นคลาสชนิดหนึ่ง คือเป็นคลาสย่อยของคลาส
Exception
การสร้างคลาสของความผิดพลาดขึ้นมาใหม่ก็คือสร้างคลาสโดยประกาศให้เป็นซับคลาสของ
Exception
การสร้างคลาสของความผิดพลาด คลาสของความผิดพลาดสามารถสร้างขึ้นมาได้ด้วยคำสั่ง
class
เหมือนกับคลาสทั่วไป โดยวงเล็บ
Exception
ไว้เป็นซูเปอร์คลาส หรือจะใช้ซูเปอร์คลาสเป็นข้อผิดพลาดชนิดอื่นก็ได้เช่นกัน
ตัวอย่าง สร้างคลาส
MonetaError
ขึ้นมาเตือนเวลาที่เงินไม่พอ
class MonetaError(Exception): # ประกาศคลาส ให้เป็นซับคลาสของ Exception
def __init__(self):
Exception.__init__(self, 'ยอดเงินในบัญชีไม่พอ') # กำหนดข้อความที่แสดงเมื่อมีข้อผิดพลาด
banchi = 10000
thon = int(input())
khongluea = banchi - thon
if(khongluea<0):
raise MonetaError # เรียกใช้คลาส
else:
print(khongluea)
เมื่อป้อนตัวเลขเกิน 10000 เข้าไปจะได้ผลเป็น
__main__.MonetaError: ยอดเงินในบัญชีไม่พอ
เราอาจส่งค่าที่ขาดไปให้ไปแสดงในข้อความเตือนผิดพลาดด้วยได้ โดยใส่อาร์กิวเมนต์หลังชื่อคลาส แล้วตอนประกาศคลาสก็กำหนดส่วนที่ให้แสดงผลด้วย
class MonetaError(Exception):
def __init__(self, khat): # เพิ่มตัวแปรมาตัวหนึ่ง คือเงินที่ขาด
Exception.__init__(self, 'ยอดเงินในบัญชีไม่พอ ขาดไป %d'%khat) #นำตัวแปรที่เพ่ิมมาแสดงผลด้วย
banchi = 10000
thon = int(input())
khongluea = banchi - thon
if(khongluea<0):
raise MonetaError(-khongluea) # ใส่ค่าเงินที่ติดลบเกินไป
else:
print(khongluea)
เมื่อป้อนค่าเงินถอนไป 12000 ผลที่ได้คือ
__main__.MonetaError: ยอดเงินในบัญชีไม่พอ ขาดไป 2000
การปล่อยให้โปรแกรมดำเนินต่อเมื่อเจอข้อยกเว้น ปกติถ้าเจอข้อผิดพลาดอะไรโปรแกรมจะหยุดทันที แต่ในบางครั้งมันก็เป็นความผิดพลาดที่เราคาดการณ์ได้อยู่แล้ว และไม่อยากให้โปรแกรมหยุดทำงานทันที แต่ให้โปรแกรมทำอะไรอย่างอื่นแทน
กรณีแบบนี้จะใช้คำสั่ง
try
และ
except
ตัวอย่าง เขียนโปรแกรมสำหรับอ่านไฟล์จากโปรแกรม
f = open('xxxx.txt','r',encoding='utf-8')
print(f.read())
f.close()
หากไฟล์ xxxx.txt ไม่มีตัวตนอยู่จะได้ว่า
FileNotFoundError: [Errno 2] No such file or directory: 'xxxx.txt'
หากเราไม่ต้องการให้เกิดความขัดข้องขึ้นแม้ว่าจะเปิดไฟล์ไม่สำเร็จก็อาจเขียนโดยใช้
try
และ
except
ดังนี้
try:
f = open('xxxx.txt','r',encoding='utf-8')
print(f.read())
f.close()
except:
print('เปิดไฟล์ไม่สำเร็จ')
ผลลัพธ์
เปิดไฟล์ไม่สำเร็จ
จากตัวอย่างจะเห็นว่าเมื่อไฟล์เปิดไม่สำเร็จก็จะเกิดการกระทำคำสั่งที่อยู่ในโครงสร้าง
except
แต่หากเปิดไฟล์สำเร็จ เช่นลองรัน
f = open('xxxx.txt','w',encoding='utf-8')
f.write('xxxxxxxx')
f.close()
แล้วค่อยรันโค้ดข้างต้นอีกทีจะได้ผลลัพธ์เป็น
xxxxxxxx
โดยโค้ดส่วนที่อยู่ในโครงสร้าง
except
จะไม่ถูกอ่านเมื่อไม่มีการขัดข้องอะไรเกิดขึ้น
โครงสร้างของ
try
และ
except
นี้ว่าไปแล้วก็คล้าย
if
และ
else
แต่ต่างกันตรงที่ว่า
try
และ
except
ต้องอยู่คู่กันตลอด จะละ
except
ทิ้งไม่ได้
ถ้าไม่ต้องการให้มีการทำอะไรก็อาจแค่เติมเลข
0
หรืออาจใช้
pass
ก็ได้ เช่น
for i in range(-3,4):
try:
print(6/i)
except:
0
ผลลัพธ์
-2.0
-3.0
-6.0
6.0
3.0
2.0
จะเห็นว่ามีการวนด้วย
for
ทั้งหมด ๗ ครั้ง แต่มีเลขออกมาแค่ ๖ ตัว เพราะเมื่อ
i
เท่ากับ
0
จะไม่เกิดอะไรขึ้น ถูกข้ามไปเลย
การจำกัดชนิดของความผิดพลาด ในส่วนของ
except
ถ้าเราเขียนแค่
except
เฉยๆก็จะมีการทำไม่ว่าจะเกิดความผิดพลาดแบบไหนก็ตาม แต่เราสามารถจำกัดเงื่อนไขของความผิดพลาดไปได้ด้วยการใส่ชนิดของความผิดพลาด ไว้ข้างหลัง
ลอง
try:
f = open('xxxxxxx.txt','r',encoding='utf-8')
print(f.read())
f.close()
except ValueError:
print('เปิดไฟล์ไม่สำเร็จ')
โค้ดนี้ต่างจากตัวอย่างที่แล้วแค่เพิ่มคำว่า
ValueError
เข้ามาข้างหลัง
except
แต่ผลที่ได้ก็คือความผิดพลาดยังคงเกิดขึ้นมาตามปกติ
ที่เป็นแบบนี้ก็เนื่องจากว่าความผิดพลาดที่เกิดขึ้นนี้เป็นแบบ
FileNotFoundError
ไม่ใช่
ValueError
การเขียน
ValueError
ไว้ข้างหลัง
except
แบบนี้จะทำให้มันทำงานเฉพาะเมื่อมีข้อผิดพลาดแบบ
ValueError
เท่านั้น ดังนั้น
except
จึงไม่ทำงานในกรณีนี้
สามารถใช้
except
ที่มีเงื่อนไขต่างกันได้ในเวลาเดียวกัน
try:
f = open('xxxxxxx.txt','r',encoding='utf-8')
print(f.read())
f.close()
except ValueError:
print('ค่าผิดพลาด')
except FileNotFoundError:
print('เปิดไฟล์ไม่สำเร็จ')
except
อันสุดท้ายอาจไม่ต้องเขียนชนิดลงไปก็ได้ ซึ่งมันจะทำงานเมื่อมีข้อผิดพลาดนอกเหนือจากที่ระบุข้างต้นมาแล้วทั้งหมด เช่น
try:
print(1/0)
except ValueError:
print('ค่าผิดพลาด')
except FileNotFoundError:
print('เปิดไฟล์ไม่สำเร็จ')
except:
print('เกิดปัญหาบางประการ')
การใช้ raise กับ try หากมีการใช้
raise
ในโครงสร้าง
try
ก็จะทำให้เกิดการทำอะไรใน
except
ได้เช่นกัน
try:
raise ValueError
except ValueError:
print('ค่าผิดพลาด')
คลาสของความผิดพลาดที่สร้างขึ้นมาเองก็สามารถนำมาใช้ได้เช่นกัน
class BakaError(Exception):
def __init__(self):
Exception.__init__(self)
try:
raise BakaError
except BakaError:
print('baka baka baka')
การเก็บข้อความเตือนข้อผิดพลาด ปกติแล้วเวลาเกิดข้อผิดพลาดจะมีการสร้างออบเจ็กต์ที่เป็นอินสแตนซ์ของคลาสความผิดพลาดนั้นขึ้น
ออบเจ็กต์นั้นเป็นตัวกำหนดข้อความที่แสดงออกมาเมื่อมีความผิดพลาดเกิดขึ้น โดยจะต่างกันออกไปตามคลาสของความผิดพลาด
แต่ถ้าหากใช้
try
และ
except
แล้วเกิดข้อผิดพลาดขึ้นใน
try
จะไม่มีการแสดงข้อความที่เตือนถึงข้อผิดพลาด แต่จะทำเหมือนไม่มีอะไรเกิดขึ้นเลย
แต่หากต้องการเก็บออบเจ็กต์ที่เกิดจากความผิดพลาดนั้นไว้เพื่อแสดงผลก็สามารถทำได้ โดยเพิ่มคำว่า
as
ไปด้านหลังชนิดของข้อผิดพลาดซึ่งตามหลัง
except
อีกที แล้วหลัง
as
ใส่ตัวแปรที่ต้องการให้มารับออบเจ็กต์ของความผิดพลาด
import math
try:
math.acos(2)
except Exception as er:
print(type(er)) # ได้ <class 'ValueError'>
print(er) # ได้ math domain error
ในตัวอย่างนี้ตัวแปร
er
จะเก็บออบเจ็กต์ของความผิดพลาดเอาไว้ พอใช้
type
ก็จะแสดงคลาสของความผิดพลาด ในที่นี้คือ
ValueError
เมื่อใช้
print
กับออบเจ็กต์ของความผิดพลาดจะเป็นการแสดงข้อความเตือนที่ผิดพลาด
***ในไพธอน ๒ จะมีวิธีการเขียนต่างออกไป โดยใช้จุลภาค
,
แทน
as
>>> รายละเอียด try ซ้อน try โครงสร้าง
try
สามารถใช้ซ้อนกันได้ เช่น
try:
try:
1/0
except:
print('มีข้อผิดพลาดข้างใน')
except:
print('มีข้อผิดพลาดข้างนอก')
จะได้
มีข้อผิดพลาดข้างใน
จะเห็นว่าพอมีข้อผิดพลาดอยู่ภายในจะมีการทำในสิ่งที่อยู่ในโครงสร้าง
except
ด้านใน แต่มันก็ทำให้โปรแกรมทำงานต่อไปตามปกติ ดังนั้น
except
ด้านนอกก็จะถูกมองข้ามไป
หากต้องการให้เวลามีข้อผิดพลาดข้างในแล้วมีการทำ
except
ข้างนอกด้วยก็อาจจะใส่
raise
ภายใน
except
ด้านในอีกที
try:
try:
1/0
except:
print('มีข้อผิดพลาดข้างใน')
raise Exception
except:
print('มีข้อผิดพลาดข้างนอก')
ได้
มีข้อผิดพลาดข้างใน
มีข้อผิดพลาดข้างนอก
การใช้ else กับ try และ except โครงสร้าง
try
except
สามารถต่อด้วย
else
ได้ด้วย โดยจะตรงข้ามกับ
except
คือเป็นคำสั่งที่จะทำเมื่อไม่มีการขัดข้องเกิดขึ้นเท่านั้น
ลองดูตัวอย่าง กรณีที่มีข้อผิดพลาด
try:
print(1/0) # ขัดข้อง
except:
print(2) # ทำงาน
else:
print(3) # ไม่ทำงาน
ผลลัพธ์
2
เทียบกับกรณีที่ทำงานตามปกติไม่มีข้อผิดพลาด
try:
print(1) # ไม่ขัดข้อง
except:
print(2) # ไม่ทำงาน
else:
print(3) # ทำงาน
ผลลัพธ์
1
3
การใช้ finally ในการจัดการกับข้อยกเว้นนอกจาก
try
,
except
และ
else
แล้วยังมี
finally
ซึ่งใช้น้อยกว่า จึงขอแยกไปเขียนเป็นเนื้อหาเสริมในอีกหน้า
>> การใช้ finally ร่วมกับ try และ except ในการจัดการข้อยกเว้น อ้างอิง