ข้อผิดพลาดในโปรแกรมแบ่งออกใหญ่ๆเป็น ๓ ชนิด ดังที่ได้กล่าวไปแล้วในบทที่ ๒ ได้แก่
- ข้อผิดพลาดทางวากยสัมพันธ์ (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

ImportError เกิดขึ้นเมื่อใช้คำสั่ง import แล้วมีข้อผิดพลาด เช่นพิมพ์ชื่อมอดูลที่ไม่มีอยู่หรือยังไม่ได้ลงเอาไว้ หรือพิมพ์ชื่อผิด
import mat # ได้ ImportError: 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 'Exception'>, <class 'GeneratorExit'>, <class 'KeyboardInterrupt'>, <class 'SystemExit'>]



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

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

เช่นลองเขียนโปรแกรมง่ายๆที่คำนวณยอดเงินคงเหลือหลังกดตัง ซึ่งก็คือเงินในบัญชีลบด้วยเงินที่ถอนไป
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



อ้างอิง