# Exception ใน Python: try, except, else, finally, raise และ exception ของตัวเอง

URL: https://skillaria.top/th/guides/python/python-exceptions

Exception ใน Python คือออบเจกต์ที่บอกว่าเกิดข้อผิดพลาด และหยุดการทำงานปกติของโค้ดจนกว่าบล็อก try/except จะดักจับได้ else ทำงานเมื่อไม่มีข้อผิดพลาด ส่วน finally ทำงานเสมอ สามารถเรียกข้อผิดพลาดเองด้วยคำสั่ง raise เช่น raise ValueError("จำนวนเงินติดลบ") และสร้าง exception ของตัวเองได้โดยสืบทอดคลาส Exception

Exception ใน Python คือออบเจกต์ที่บอกว่าเกิดข้อผิดพลาดและหยุดการทำงานปกติของโปรแกรม หากไม่มีใครดักจับ Python จะแสดง traceback และจบการทำงาน การดักจับทำด้วยโครงสร้าง `try/except` ส่วนการเรียกข้อผิดพลาดเองทำด้วยคำสั่ง `raise` บทความนี้จะอธิบายการทำงานของระบบนี้ ข้อผิดพลาดที่พบบ่อย และวิธีจัดการข้อผิดพลาดโดยไม่ไปซ่อนปัญหา

## Exception ใน Python คืออะไร

เมื่อการดำเนินการทำต่อไม่ได้ เช่น หารด้วยศูนย์ เข้าถึงคีย์ที่ไม่มีในดิกชันนารี หรือแปลงคำให้เป็นตัวเลข Python จะสร้างออบเจกต์ exception และ "โยน" ออกมา การทำงานของฟังก์ชันปัจจุบันจะหยุด exception จะถูกส่งต่อขึ้นไปตามลำดับการเรียกฟังก์ชัน จนกว่าจะเจอตัวจัดการที่เหมาะสม หากไม่เจอ โปรแกรมจะหยุด

```
>>> 10 / 0
Traceback (most recent call last):
  File "", line 1, in
ZeroDivisionError: division by zero
```

ตอบคำถามที่พบบ่อย: เมื่อหารด้วยศูนย์ใน Python จะเกิด exception ชนิด `ZeroDivisionError` ซึ่งรวมถึงตัวดำเนินการ `/` `//` และ `%` ด้วย

## วิธีอ่าน traceback

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

```
Traceback (most recent call last):
  File "tracker.py", line 9, in
    total = parse_amount(row)
  File "tracker.py", line 4, in parse_amount
    return float(raw.strip())
           ~~~~~^^^^^^^^^^^^^
ValueError: could not convert string to float: 'สิบ'
```

ตั้งแต่ Python 3.11 จะมีเครื่องหมาย `~~~^^^` วาดอยู่ใต้บรรทัดโค้ด (PEP 657) เพื่อชี้ว่านิพจน์ใดในบรรทัดนั้นที่เกิดข้อผิดพลาด ซึ่งมีประโยชน์มากเมื่อบรรทัดเดียวมีการเรียกฟังก์ชันหลายครั้งต่อกัน

### คำแนะนำแบบ "Did you mean"

ตั้งแต่ Python 3.10 เมื่อพิมพ์ชื่อผิด ข้อผิดพลาดจะแนะนำชื่อที่ใกล้เคียง ใน Python 3.15 คำแนะนำฉลาดขึ้น โดยรู้ถึงนิสัยจากภาษาอื่น เช่น เรียก `list.push` จะแนะนำ `append` และ `"กาแฟ".toUpperCase()` จะแนะนำเมธอด `upper` คำแนะนำยังดูลงไปถึงฟิลด์ซ้อนได้ เช่น หากดิกชันนารีอยู่ในแอตทริบิวต์ `totals` ของอ็อบเจกต์ แล้วคุณเรียก `items()` กับตัวอ็อบเจกต์ เอง ข้อผิดพลาดจะแนะนำ `.totals.items` นอกจากนี้ `delattr()` ก็แนะนำชื่อที่ใกล้เคียงได้แล้ว

## ชนิดของ exception ใน Python

มี exception ในตัวหลายสิบชนิด แต่ช่วงเริ่มต้นจะเจอชนิดเหล่านี้บ่อยที่สุด:

| Exception | เกิดขึ้นเมื่อ | ตัวอย่าง |
| --- | --- | --- |

| `NameError` | ไม่รู้จักชื่อนี้ มักเกิดจากพิมพ์ผิด | `print(totl)` |

| `TypeError` | การดำเนินการไม่เหมาะกับชนิดข้อมูลนี้ | `"5" + 3` |

| `ValueError` | ชนิดถูกต้องแต่ค่าไม่ถูกต้อง | `int("สิบ")` |

| `KeyError` | ไม่มีคีย์นี้ในดิกชันนารี | `d["missing"]` |

| `IndexError` | ดัชนีอยู่นอกขอบเขตของลิสต์ | `[1, 2][5]` |

| `AttributeError` | อ็อบเจกต์ไม่มีเมธอดหรือฟิลด์นี้ | `[].push(1)` |

| `ZeroDivisionError` | หารด้วยศูนย์ | `1 / 0` |

| `FileNotFoundError` | ไม่พบไฟล์ | `open("nope.txt")` |

ส่วน `SyntaxError` (และกรณีย่อย `IndentationError`) เกิดขึ้นตั้งแต่ตอนแปลไฟล์ ก่อนโค้ดจะรัน จึงดักจับด้วย `try` ในไฟล์เดียวกันไม่ได้

### ลำดับชั้นของ exception

Exception เป็นคลาส และมีการจัดเป็นลำดับชั้น ที่ยอดคือ `BaseException` ซึ่งมี `Exception` (ข้อผิดพลาดทั่วไปแทบทั้งหมด) และคลาสระบบบางตัวสืบทอดอยู่ เช่น `KeyboardInterrupt` `SystemExit` และ `GeneratorExit`

```
BaseException
 ├── KeyboardInterrupt
 ├── SystemExit
 └── Exception
      ├── ArithmeticError
      │    └── ZeroDivisionError
      ├── LookupError
      │    ├── KeyError
      │    └── IndexError
      ├── OSError
      │    └── FileNotFoundError
      ├── TypeError
      └── ValueError
```

ลำดับชั้นมีผลเมื่อดักจับ `except LookupError` จะดักได้ทั้ง `KeyError` และ `IndexError` ส่วน `KeyboardInterrupt` ไม่ใช่ข้อผิดพลาดในโค้ด แต่เป็นสัญญาณว่าผู้ใช้กด Ctrl+C มันไม่ได้สืบทอดจาก `Exception` โดยเจตนา เพื่อไม่ให้ `except Exception` ไปขัดขวางการหยุดโปรแกรม

## การจัดการ exception: try/except/else/finally

โค้ดที่อาจเกิดข้อผิดพลาดจะถูกวางไว้ในบล็อก `try` ส่วน `except` ระบุชนิดของข้อผิดพลาดและกำหนดสิ่งที่จะทำ `except` คือบล็อกที่ Python ใช้จัดการ exception

```
def parse_amount(raw):
    try:
        value = float(raw)
    except ValueError as e:
        print(f"ข้ามบรรทัดนี้: {e}")
        return None
    else:
        return value          # ทำงานเมื่อไม่มีข้อผิดพลาด
    finally:
        print("แยกข้อมูลเสร็จแล้ว")   # ทำงานเสมอ

print(parse_amount("250"))
```

ผลลัพธ์จะแสดง "แยกข้อมูลเสร็จแล้ว" ก่อน แล้วตามด้วย `250.0` คำสั่ง `return` ในบล็อก `else` จะจำค่าไว้ก่อน จากนั้น Python ทำ `finally` ให้เสร็จเสมอ แล้วจึงคืนค่า ดังนั้น `finally` จึงเชื่อถือได้สำหรับการเก็บกวาด เช่น ปิดไฟล์หรือคืนการเชื่อมต่อ

หากเขียน `return` ของตัวเองใน `finally` ค่านี้จะทับค่าเดิม และยังอาจ "กลืน" exception ได้ด้วย ตั้งแต่ Python 3.14 โค้ดแบบนี้จะแสดง `SyntaxWarning` (PEP 765) อย่าเขียน `return` ใน `finally`

### หลาย exception พร้อมกัน

สามารถเขียนหลายบล็อก `except` สำหรับชนิดต่างกัน หรือระบุหลายชนิดในทูเพิลเดียวได้:

```
try:
    amount = int(data["amount"])
except KeyError:
    amount = 0
except (ValueError, TypeError) as e:
    print("ตัวเลขผิดรูปแบบ:", e)
    amount = 0
```

บล็อกจะถูกตรวจจากบนลงล่าง และทำงานเฉพาะบล็อกแรกที่ตรง ดังนั้นชนิดที่เจาะจงกว่าต้องวางไว้เหนือชนิดทั่วไป ตั้งแต่ Python 3.14 (PEP 758) สามารถระบุหลายชนิดได้โดยไม่ต้องใส่วงเล็บ เช่น `except ValueError, TypeError:` แต่หากใช้ `as` ยังต้องมีวงเล็บเหมือนเดิม

### วิธีแสดงข้อความและดูชนิดของ exception

ออบเจกต์ exception เข้าถึงได้ผ่าน `as e` โดย `str(e)` จะได้ข้อความ และ `type(e).__name__` จะได้ชื่อชนิด:

```
try:
    int("สิบ")
except ValueError as e:
    print(type(e).__name__)   # ValueError
    print(e)                  # invalid literal for int() with base 10: 'สิบ'
```

## โมดูล traceback: ข้อความผิดพลาดแบบเต็ม

บางครั้งต้องดักข้อผิดพลาดไว้ แต่ยังต้องเก็บ traceback ทั้งหมดไว้ เช่น สำหรับบันทึกล็อกหรือรายงาน โมดูล `traceback` ทำหน้าที่นี้:

```
import traceback

try:
    1 / 0
except ZeroDivisionError:
    traceback.print_exc()             # พิมพ์ traceback ไปยัง stderr
    text = traceback.format_exc()     # ข้อความเดียวกันในรูปสตริง
```

หากใช้โมดูล `logging` เขียนล็อก ให้เรียก `logging.exception("คำนวณไม่สำเร็จ")` ภายใน `except` จะได้ traceback แนบไปกับข้อความโดยอัตโนมัติ

## raise ใน Python: เรียก exception เอง

คำสั่ง `raise` โยน exception ด้วยตัวเอง ฟังก์ชันใช้บอกว่าไม่สามารถทำงานกับข้อมูลแบบนี้ได้:

```
def add_expense(amount):
    if amount < 0:
        raise ValueError(f"จำนวนเงินติดลบไม่ได้: {amount}")
    ...

add_expense(-5)
# ValueError: จำนวนเงินติดลบไม่ได้: -5
```

`raise` เป็นคำสั่ง ไม่ใช่ฟังก์ชัน จึงไม่ต้องมีวงเล็บหลังคำนี้ อาร์กิวเมนต์ที่ใส่ในวงเล็บคือค่าที่ส่งเข้า constructor ของ exception เลือกชนิดให้ตรงความหมาย ค่าที่ไม่ถูกต้องใช้ `ValueError` ชนิดข้อมูลที่ไม่ถูกต้องใช้ `TypeError`

### raise เปล่า ๆ และ raise from

ภายใน `except` การเขียน `raise` เปล่า ๆ จะส่ง exception ปัจจุบันต่อไปโดยไม่เปลี่ยนแปลง ใช้เมื่อต้องการบันทึกบางอย่างแต่ให้โค้ดที่เรียกใช้เป็นผู้จัดการต่อ

หากต้องการแทนที่ exception หนึ่งด้วยอีกตัวหนึ่ง ใช้ `raise ... from e` เพื่อเก็บ exception เดิมไว้ในสายโซ่ และ traceback จะแสดงทั้งสองตัว:

```
def load_config(path):
    try:
        with open(path) as f:
            return f.read()
    except FileNotFoundError as e:
        raise RuntimeError(f"ไม่พบไฟล์คอนฟิก {path}") from e
```

### add_note: เพิ่มบริบทให้ข้อผิดพลาด

ตั้งแต่ Python 3.11 (PEP 678) สามารถเพิ่มหมายเหตุให้ exception ด้วยเมธอด `add_note()` แล้วส่งต่อได้เลย ชนิดข้อผิดพลาดไม่เปลี่ยน และหมายเหตุจะแสดงอยู่ใต้ข้อความ:

```
def parse(raw, line_no):
    try:
        return float(raw)
    except ValueError as e:
        e.add_note(f"ไฟล์ expenses.csv บรรทัดที่ {line_no}")
        raise

parse("สิบ", 17)
# ValueError: could not convert string to float: 'สิบ'
# ไฟล์ expenses.csv บรรทัดที่ 17
```

## สร้าง exception ของตัวเองใน Python

Exception ของตัวเองมีประโยชน์กับข้อผิดพลาดเฉพาะโดเมน เช่น เกินงบประมาณ ออเดอร์จ่ายเงินแล้ว หรือผู้ใช้ถูกบล็อก เพียงสร้างคลาสที่สืบทอดจาก `Exception`:

```
class BudgetExceeded(Exception):
    def __init__(self, category, limit, spent):
        super().__init__(f"{category}: วงเงิน {limit} ใช้ไป {spent}")
        self.category = category
        self.limit = limit
        self.spent = spent

def check_budget(category, limit, spent):
    if spent > limit:
        raise BudgetExceeded(category, limit, spent)

try:
    check_budget("อาหาร", 10000, 12500)
except BudgetExceeded as e:
    print("คำเตือน:", e)   # คำเตือน: อาหาร: วงเงิน 10000 ใช้ไป 12500
```

ตอนนี้โค้ดที่เรียกใช้จะดักเฉพาะกรณีเกินงบประมาณ และไม่สับสนกับการพิมพ์ชื่อผิด ในโปรเจกต์ขนาดใหญ่ควรสร้างคลาสฐานร่วม (เช่น `class TrackerError(Exception)`) แล้วให้ exception ทั้งหมดสืบทอดจากคลาสนี้ เพื่อดักทั้งหมดด้วย `except TrackerError` เพียงบล็อกเดียว และควรสืบทอดจาก `Exception` ไม่ใช่จาก `BaseException`

## กลุ่ม exception และ except*

บางครั้งเกิดข้อผิดพลาดหลายตัวพร้อมกัน เช่น ตรวจทั้งไฟล์ หรือในงานคู่ขนานของ `asyncio.TaskGroup` ตั้งแต่ Python 3.11 สามารถรวมไว้ใน `ExceptionGroup` และจัดการแยกตามชนิดด้วย `except*`:

```
def check_all(rows):
    errors = []
    for r in rows:
        try:
            float(r)
        except ValueError as e:
            errors.append(e)
    if errors:
        raise ExceptionGroup("ข้อผิดพลาดในไฟล์", errors)

try:
    check_all(["1", "a", "b"])
except* ValueError as eg:
    print(f"บรรทัดที่ผิดรูปแบบ: {len(eg.exceptions)}")   # บรรทัดที่ผิดรูปแบบ: 2
```

## try except ดักทุกข้อผิดพลาด: ทำไมไม่ควร

นิสัยที่เป็นอันตรายที่สุดคือดักทุกอย่าง เช่น `except: pass` โปรแกรมจะ "ไม่ล่ม" แต่จริง ๆ แล้วกลับไม่รายงานปัญหาใดเลย การใช้ `except` เปล่า ๆ จะดัก `KeyboardInterrupt` และ `SystemExit` ด้วย ทำให้กด Ctrl+C แล้วหยุดโปรแกรมไม่ได้ ส่วน `except Exception: pass` ดีกว่าเล็กน้อย แต่ก็ยังเงียบไปกับการพิมพ์ชื่อผิดและข้อผิดพลาดทางตรรกะ

วิธีจัดการ exception ที่ถูกต้องใน Python:

- ดักเฉพาะชนิดที่รู้วิธีรับมือ เช่น `except ValueError` ไม่ดักทั้งหมด
- ทำบล็อก `try` ให้สั้น ใส่เฉพาะบรรทัดที่อาจเกิดข้อผิดพลาดแบบนั้นจริง ๆ
- หากดักกว้าง (`except Exception`) ที่ระดับบนสุดของโปรแกรม ต้องบันทึก traceback ทุกครั้ง
- อย่าใช้ exception สำหรับการแตกแขนงธรรมดาเมื่อการตรวจสอบเพียงพอ เช่น ใช้ `dict.get()` แทน `try/except KeyError` ในกรณีง่าย ๆ
- สำหรับทรัพยากร ใช้ `with` เพราะมันจะปิดไฟล์ให้แม้เกิดข้อผิดพลาด

## สรุป

Exception ใน Python เป็นกลไกสำหรับแจ้งข้อผิดพลาด ไม่ใช่เหตุผลที่จะซ่อนมัน อ่าน traceback จากล่างขึ้นบน ดักชนิดที่เจาะจงด้วย `try/except` ใช้ `else` สำหรับโค้ด "เมื่อทุกอย่างผ่าน" และ `finally` สำหรับการเก็บกวาด `raise` ใช้แจ้งข้อผิดพลาดด้วยตัวเอง `raise from` และ `add_note` เก็บบริบทไว้ และ exception ของตัวเองควรสืบทอดจาก `Exception` หัวข้อนี้ถูกสอนแบบลงมือทำในคอร์สเรื่อง Python โดยโปรแกรมติดตามค่าใช้จ่ายจะไม่หยุดทำงานเพราะบรรทัด CSV ที่ผิดรูปแบบบรรทัดเดียว และแสดงการเกินงบประมาณด้วย exception ของตัวเอง

## FAQ

**ข้อผิดพลาดกับ exception ใน Python ต่างกันอย่างไร?**

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

**หลัง exception แล้วจะกลับไปทำต่อจากจุดที่เกิดได้หรือไม่?**

ไม่ได้ หลังจากดักจับแล้ว โปรแกรมจะทำงานต่อจากหลังบล็อก try/except ไม่ใช่จากบรรทัดที่เกิดข้อผิดพลาด หากต้องการประมวลผลทีละบรรทัดของไฟล์ ให้วาง try ไว้ภายในลูป

**หาก exception เกิดขึ้นภายในบล็อก except จะเป็นอย่างไร?**

exception ใหม่จะถูกส่งต่อขึ้นไป และ exception เดิมจะถูกเก็บไว้ในแอตทริบิวต์ __context__ ใน traceback จะปรากฏข้อความ During handling of the above exception, another exception occurred

**จะทดสอบว่าฟังก์ชันโยน exception จริงในเทสต์ได้อย่างไร?**

ใน pytest ใช้ with pytest.raises(ValueError): ใน unittest ใช้ self.assertRaises(ValueError) เทสต์จะผ่านเฉพาะเมื่อมี exception ชนิดที่ระบุเกิดขึ้นในบล็อก

**assert ต่างจาก raise อย่างไร?**

assert เงื่อนไข, "ข้อความ" จะโยน AssertionError เมื่อเงื่อนไขเป็นเท็จ เป็นเครื่องมือตรวจสอบระหว่างพัฒนา เมื่อรันด้วยแฟล็ก -O จะถูกปิด ดังนั้นการตรวจข้อมูลจากผู้ใช้ต้องใช้ if กับ raise


---
Source: Skillaria.Top (https://skillaria.top)
