Python 例外處理 (Exception Handling)

例外(Exception)是程式執行時發生的錯誤。Python 提供 try...except 語法來處理例外,讓程式不會因為錯誤而中斷。

常見的例外類型

例外說明
SyntaxError語法錯誤
NameError變數名稱不存在
TypeError型別錯誤
ValueError值錯誤
IndexError索引超出範圍
KeyError字典 key 不存在
FileNotFoundError檔案不存在
ZeroDivisionError除以零
AttributeError屬性不存在
ImportError匯入錯誤
# 一些會產生例外的程式碼
print(10 / 0)              # ZeroDivisionError
print(int("hello"))        # ValueError
print(my_var)              # NameError
print([1, 2, 3][10])       # IndexError
print({"a": 1}["b"])       # KeyError

try...except

使用 try...except 捕捉例外:

try:
    result = 10 / 0
except ZeroDivisionError:
    print("Cannot divide by zero!")

輸出:

Cannot divide by zero!

捕捉多種例外

try:
    # 可能發生錯誤的程式碼
    value = int(input("Enter a number: "))
    result = 10 / value
except ValueError:
    print("Please enter a valid number!")
except ZeroDivisionError:
    print("Cannot divide by zero!")

一次捕捉多種例外

try:
    value = int(input("Enter a number: "))
    result = 10 / value
except (ValueError, ZeroDivisionError):
    print("Invalid input!")

捕捉所有例外

try:
    # 可能發生錯誤的程式碼
    result = 10 / 0
except Exception as e:
    print(f"An error occurred: {e}")
捕捉所有例外可能會隱藏真正的問題,建議只捕捉你知道如何處理的特定例外。

取得例外資訊

try:
    result = 10 / 0
except ZeroDivisionError as e:
    print(f"Error type: {type(e).__name__}")
    print(f"Error message: {e}")

輸出:

Error type: ZeroDivisionError
Error message: division by zero

else 子句

else 在沒有例外時執行:

try:
    result = 10 / 2
except ZeroDivisionError:
    print("Cannot divide by zero!")
else:
    print(f"Result: {result}")  # 沒有例外時執行

輸出:

Result: 5.0

finally 子句

finally 無論是否發生例外都會執行,常用於清理資源:

try:
    f = open("file.txt", "r")
    content = f.read()
except FileNotFoundError:
    print("File not found!")
finally:
    print("Cleanup...")
    # 確保檔案被關閉
    if 'f' in locals() and not f.closed:
        f.close()

完整的 try 語法

try:
    # 可能發生錯誤的程式碼
    pass
except ExceptionType1:
    # 處理 ExceptionType1
    pass
except ExceptionType2 as e:
    # 處理 ExceptionType2,並取得例外資訊
    pass
except Exception as e:
    # 處理其他所有例外
    pass
else:
    # 沒有例外時執行
    pass
finally:
    # 無論如何都會執行
    pass

raise 拋出例外

使用 raise 主動拋出例外:

def divide(a, b):
    if b == 0:
        raise ValueError("Divisor cannot be zero!")
    return a / b

try:
    result = divide(10, 0)
except ValueError as e:
    print(e)  # Divisor cannot be zero!

重新拋出例外

try:
    result = 10 / 0
except ZeroDivisionError:
    print("Logging error...")
    raise  # 重新拋出同一個例外

從另一個例外拋出

try:
    result = 10 / 0
except ZeroDivisionError as e:
    raise ValueError("Invalid operation") from e

例外階層

Python 的例外是有階層的:

BaseException
 ├── SystemExit
 ├── KeyboardInterrupt
 ├── GeneratorExit
 └── Exception
      ├── StopIteration
      ├── ArithmeticError
      │    ├── ZeroDivisionError
      │    └── OverflowError
      ├── LookupError
      │    ├── IndexError
      │    └── KeyError
      ├── ValueError
      ├── TypeError
      └── ...
# LookupError 是 IndexError 和 KeyError 的父類別
try:
    my_list = [1, 2, 3]
    print(my_list[10])
except LookupError:
    print("Lookup failed!")  # 會捕捉 IndexError 和 KeyError

實際範例

檔案處理

def read_file(filename):
    try:
        with open(filename, 'r') as f:
            return f.read()
    except FileNotFoundError:
        print(f"File '{filename}' not found")
        return None
    except PermissionError:
        print(f"No permission to read '{filename}'")
        return None
    except Exception as e:
        print(f"Error reading file: {e}")
        return None

content = read_file("data.txt")
if content:
    print(content)

使用者輸入

def get_positive_number():
    while True:
        try:
            value = int(input("Enter a positive number: "))
            if value <= 0:
                raise ValueError("Number must be positive")
            return value
        except ValueError as e:
            print(f"Invalid input: {e}")

number = get_positive_number()
print(f"You entered: {number}")

API 請求

import requests

def fetch_data(url):
    try:
        response = requests.get(url, timeout=5)
        response.raise_for_status()  # 如果狀態碼不是 2xx,拋出例外
        return response.json()
    except requests.exceptions.Timeout:
        print("Request timed out")
    except requests.exceptions.HTTPError as e:
        print(f"HTTP error: {e}")
    except requests.exceptions.RequestException as e:
        print(f"Request failed: {e}")
    return None

assert 斷言

assert 用於測試條件,如果條件為 False 會拋出 AssertionError

def divide(a, b):
    assert b != 0, "Divisor cannot be zero"
    return a / b

divide(10, 0)  # AssertionError: Divisor cannot be zero
assert 主要用於開發和測試階段,在生產環境可以用 python -O 選項停用。

最佳實踐

  1. 只捕捉你知道如何處理的例外
  2. 不要使用空的 except
  3. 使用 finally 確保資源被清理
  4. 提供有意義的錯誤訊息
  5. 記錄例外資訊以便除錯
# 不好的做法
try:
    do_something()
except:  # 捕捉所有例外,包括 KeyboardInterrupt
    pass  # 完全忽略錯誤

# 好的做法
try:
    do_something()
except SpecificException as e:
    logger.error(f"Operation failed: {e}")
    handle_error()