φυβλαςのβλογ
บล็อกของ phyblas



ภาษา python เบื้องต้น บทที่ ๒๕: การจัดการกับข้อยกเว้น
เขียนเมื่อ 2016/04/04 23:01
แก้ไขล่าสุด 2024/02/22 11:06
 

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



อ้างอิง




-----------------------------------------

囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧

ดูสถิติของหน้านี้

หมวดหมู่

-- คอมพิวเตอร์ >> เขียนโปรแกรม >> python

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

สารบัญ

รวมคำแปลวลีเด็ดจากญี่ปุ่น
มอดูลต่างๆ
-- numpy
-- matplotlib

-- pandas
-- manim
-- opencv
-- pyqt
-- pytorch
การเรียนรู้ของเครื่อง
-- โครงข่าย
     ประสาทเทียม
ภาษา javascript
ภาษา mongol
ภาษาศาสตร์
maya
ความน่าจะเป็น
บันทึกในญี่ปุ่น
บันทึกในจีน
-- บันทึกในปักกิ่ง
-- บันทึกในฮ่องกง
-- บันทึกในมาเก๊า
บันทึกในไต้หวัน
บันทึกในยุโรปเหนือ
บันทึกในประเทศอื่นๆ
qiita
บทความอื่นๆ

บทความแบ่งตามหมวด



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

  ค้นหาบทความ

  บทความแนะนำ

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

บทความแต่ละเดือน

2024年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

2023年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

2022年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

2021年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

2020年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

ค้นบทความเก่ากว่านั้น

ไทย

日本語

中文