8. ข้อผิดพลาดและการจัดการ (Errors and Exceptions)

8.1 ผิดพลาดทางโครงสร้างประโยค (Syntax Errors)
8.2 ข้อผิดพลาดตอนทำงาน (Exceptions)
8.3 การจัดการ (Handling Exceptions)
8.4 การยกข้อผิดพลาด (Raising Exceptions)
8.5 สร้างกับดักเอง (User-defined Exceptions)
8.6 ดักให้หมดจด (Defining Clean-up Actions)
8.7 ละเอียดนิดเพื่อความหมดจด (Predefined Clean-up Actions)


ข้อผิดพลาดมีสองแบบคือ ผิดทางโครงสร้างประโยค (syntax errors) หรือผิดตอนโปรแกรมทำงาน (exceptions)


8.1 ผิดพลาดทางโครงสร้างประโยค (Syntax Errors)

มือใหม่ต้องเจอทุกคน

>>> while True print 'Hello world'
  File "<stdin>", line 1, in ?
    while True print 'Hello world'
                   ^
SyntaxError: invalid syntax

ในที่นี้คือตก : หลัง True
ข้อผิดพลาดแบบนี้ต้องตามไปแก้ในโค๊ดอย่างเดียว


8.2 ข้อผิดพลาดตอนทำงาน (Exceptions)

เกิดตอนรัน

>>> 10 * (1/0)
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
ZeroDivisionError: integer division or modulo by zero

>>> 4 + spam*3
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
NameError: name 'spam' is not defined

>>> '2' + 2
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
TypeError: cannot concatenate 'str' and 'int' objects

บรรทัดสุดท้ายเป็นการบอกว่าผิดเรื่องอะไร
จากตัวอย่าง จะเห็นว่าผิดอยู่สามเรื่องคือ ZeroDivisionError NameError และ TypeError สามารถดูรายละเอียดทั้งหมดได้ที่ module-exceptions


8.3 การจัดการ (Handling Exceptions)

จัดการด้วยประโยค try: ... except[(ErrorType)]: ... [else: ...]
ตัวอย่างข้างล่างเป็นการจัดการข้อผิดพลาด โดยจะจัดการดูเฉพาะค่าที่เราป้อนเข้าไปว่าถูกต้องหรือไม่ (ValueError) แต่ถ้าเป็นข้อผิดพลาดแบบอื่น โปรแกรมจะปล่อยให้เป็นหน้าที่ของระบบ จึงทำให้เราสามารถใช้ปุ่ม Ctrl-C ในการตัดการทำงานของโปรแกรมได้ ซึ่งกรณีตัดการทำงานนี้จะเป็นข้อผิดพลาดแบบ KeyboardInterrupt

>>> while True:
...     try:
...         x = int(raw_input("Please enter a number: "))
...         break
...     except ValueError:
...         print "Oops!  That was no valid number.  Try again..."
...

เราระบุข้อผิดพลาดได้หลายแบบในครั้งเดียว

... except (RuntimeError, TypeError, NameError):
...     pass

หรืออาจดักเป็นขั้น ๆ จนในที่สุดไม่ใส่อะไรเลย คือ ดักทุกอย่างที่เหลือ

import sys

try:
    f = open('myfile.txt')
    s = f.readline()
    i = int(s.strip())
except IOError, (errno, strerror):
    print "I/O error(%s): %s" % (errno, strerror)
except ValueError:
    print "Could not convert data to an integer."
except:
    print "Unexpected error:", sys.exc_info()[0]
    raise

อาจใช้ร่วมกับ else: ในกรณีที่ try: ไม่ยอมแจ้งข้อผิดพลาด

for arg in sys.argv[1:]:
    try:
        f = open(arg, 'r')
    except IOError:
        print 'cannot open', arg
    else:
        print arg, 'has', len(f.readlines()), 'lines'
        f.close()

ถ้าเติมตัวแปร(อาจเป็นทูเปิล)หลัง Exception ตัวแปรนั้นจะเก็บค่าอินสแตนซ์ของการดัก

>>> try:
...    raise Exception('spam', 'eggs')
... except Exception, inst:
...    print type(inst)     # the exception instance
...    print inst.args      # arguments stored in .args
...    print inst           # __str__ allows args to printed directly
...    x, y = inst          # __getitem__ allows args to be unpacked directly
...    print 'x =', x
...    print 'y =', y
...
<type 'instance'>
('spam', 'eggs')
('spam', 'eggs')
x = spam
y = eggs

การดักข้อผิดพลาดนี้ ไม่เพียงแต่ดักเฉพาะบล๊อกนั้น แต่สามารถย้อนขึ้นไปถึงต้นตอได้

>>> def this_fails():
...     x = 1/0
... 
>>> try:
...     this_fails()
... except ZeroDivisionError, detail:
...     print 'Handling run-time error:', detail
... 
Handling run-time error: integer division or modulo by zero


8.4 การยกข้อผิดพลาด (Raising Exceptions)

เราสามารถยกข้อผิดพลาดมาแสดงได้ เช่น

>>> raise NameError, 'HiThere'
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
NameError: HiThere

พารามิเตอร์ตัวแรกเป็นชื่อข้อผิดพลาด อันหลังเป็นข้อความการผิดพลาด ซึ่งอาจเขียนอีกรูปนึงว่า raise NameError('HiThere')

สามารถใช้คำสั่ง raise เพื่อยกข้อผิดพลาดขึ้นอีกครั้ง ภายในบล๊อกที่ดักความผิดพลาดได้

>>> try:
...     raise NameError, 'HiThere'
... except NameError:
...     print 'An exception flew by!'
...     raise
...
An exception flew by!
Traceback (most recent call last):
  File "<stdin>", line 2, in ?
NameError: HiThere


8.5 สร้างกับดักเอง (User-defined Exceptions)

เราอาจสร้างกับดักเองโดยสร้างเป็นคลาส ซึ่งเวลานำมาใช้ต้องผันมาจากคลาสที่สร้างไว้อีกที

>>> class MyError(Exception):
...     def __init__(self, value):
...         self.value = value
...     def __str__(self):
...         return repr(self.value)
... 
>>> try:
...     raise MyError(2*2)
... except MyError, e:
...     print 'My exception occurred, value:', e.value
... 
My exception occurred, value: 4

>>> raise MyError, 'oops!'
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
__main__.MyError: 'oops!'

จากตัวอย่างนี้ เมธอด __init__ ของคลาสเริ่มคือ Exception ถูกครอบด้วยโค๊ดของเรา

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

class Error(Exception):
    """Base class for exceptions in this module."""
    pass

class InputError(Error):
    """Exception raised for errors in the input.

    Attributes:
        expression -- input expression in which the error occurred
        message -- explanation of the error
    """

    def __init__(self, expression, message):
        self.expression = expression
        self.message = message

class TransitionError(Error):
    """Raised when an operation attempts a state transition that's not
    allowed.

    Attributes:
        previous -- state at beginning of transition
        next -- attempted new state
        message -- explanation of why the specific transition is not allowed
    """

    def __init__(self, previous, next, message):
        self.previous = previous
        self.next = next
        self.message = message

ดูเรื่องคลาสได้ในบทต่อไป


8.6 ดักให้หมดจด (Defining Clean-up Actions)

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

>>> try:
...     raise KeyboardInterrupt
... finally:
...     print 'Goodbye, world!'
... 
Goodbye, world!
Traceback (most recent call last):
  File "<stdin>", line 2, in ?
KeyboardInterrupt

ประโยชน์ของวลี finally: คือ ถ้าผ่านบล๊อกของ except: และ else: มาแล้ว ถ้ายังไม่แจ้งข้อผิดพลาด จะถูกแจ้งในบล๊อกนี้ และไม่ว่าจะพบคำสั่ง break continue หรือ return ก็ตาม โค๊ดในส่วนนี้ก็ยังจะถูกรันเสมอ

>>> def divide(x, y):
...     try:
...         result = x / y
...     except ZeroDivisionError:
...         print "division by zero!"
...     else:
...         print "result is", result
...     finally:
...         print "executing finally clause"
...

>>> divide(2, 1)
result is 2
executing finally clause

>>> divide(2, 0)
division by zero!
executing finally clause

>>> divide("2", "1")
executing finally clause
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "<stdin>", line 3, in divide
TypeError: unsupported operand type(s) for /: 'str' and 'str'


8.7 ละเอียดนิดเพื่อความหมดจด (Predefined Clean-up Actions)

ควรมีความรอบคอบในการเขียนโค๊ด ลองดู

for line in open("myfile.txt"):
    print line

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

with open("myfile.txt") as f:
    for line in f:
        print line

ตัวอย่างนี้ ไฟล์ออปเจคต์ f จะถูกลบออกจากหน่วยความจำเมื่อโปรแกรมจบ