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)

เราอาจสร้างชนิดสิ่งผิดปรกติเองโดยสร้างเป็นคลาส ซึ่งต้องผัน (derive) มาจากคลาส Exception ไม่ว่าจะโดยตรงหรือโดยอ้อม

>>> 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 ดังนี้

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

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

Taxonomy upgrade extras: 
Creative Commons License ลิขสิทธิ์ของบทความเป็นของเจ้าของบทความแต่ละชิ้น
ผลงานนี้ ใช้สัญญาอนุญาตของครีเอทีฟคอมมอนส์แบบ แสดงที่มา-อนุญาตแบบเดียวกัน 3.0 ที่ยังไม่ได้ปรับแก้