Python 課程 – 例外與錯誤處理
主題:logging 模組基礎
簡介
在實務開發中,程式錯誤、執行狀態與系統行為的記錄往往比單純的印出除錯訊息更為重要。當程式上線後,若只靠 print() 或 IDE 的除錯視窗,就很難在生產環境快速定位問題,甚至可能遺漏關鍵的安全或效能資訊。Python 內建的 logging 模組提供了一套彈性且可擴充的日誌機制,讓開發者可以在不同的「層級」記錄訊息、將日誌寫入檔案、甚至傳送到遠端服務。
本篇文章將從 基礎概念、實作範例、常見陷阱與最佳實踐,一步步帶你了解如何在 Python 專案中正確使用 logging,讓除錯變得更有系統、維護更省力。
核心概念
1. 為何不直接使用 print()?
print() |
logging |
|---|---|
| 只能輸出到標準輸出(stdout) | 支援多種 Handler(檔案、串流、SMTP、SysLog 等) |
| 沒有層級(level)概念 | 提供 DEBUG / INFO / WARNING / ERROR / CRITICAL 五種層級 |
| 難以在程式執行時動態調整輸出 | 可在執行期間透過設定檔或程式碼改變層級 |
| 不支援格式化、時間戳記、檔案位置等資訊 | 自動加入時間、模組名稱、行號等豐富資訊 |
2. logging 的基本組件
- Logger:產生日誌訊息的「來源」物件。通常以模組名稱或自訂名稱建立。
- Handler:決定日誌訊息的「去向」——例如寫入檔案、輸出到終端、發送 Email。
- Formatter:定義日誌訊息的文字格式(時間、層級、訊息內容等)。
- Level:訊息的重要性層級,決定哪些訊息會被處理。
小技巧:
logging.getLogger(__name__)可以自動取得當前模組的名稱,方便在大型專案中追蹤來源。
3. 設定層級(Level)
import logging
logging.basicConfig(level=logging.DEBUG) # 全部層級都會被顯示
logging.debug("除錯訊息")
logging.info("一般資訊")
logging.warning("警告訊息")
logging.error("錯誤訊息")
logging.critical("嚴重錯誤")
- DEBUG:最詳細的訊息,僅在開發或除錯時使用。
- INFO:正常運作的關鍵步驟。
- WARNING:非致命但值得注意的情況。
- ERROR:造成功能失敗的錯誤。
- CRITICAL:系統無法繼續執行的嚴重錯誤。
程式碼範例
以下示範 5 個常見且實用的 logging 用法,皆附上說明註解。
範例 1:最簡單的檔案日誌
import logging
# 設定根 logger,將訊息寫入 app.log,層級為 INFO
logging.basicConfig(
filename='app.log',
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
logging.info("程式啟動")
logging.warning("磁碟空間即將不足")
logging.error("無法連接資料庫")
說明:
basicConfig只會在第一次呼叫時生效,適合小型腳本或單一入口的程式。
範例 2:同時寫檔與輸出到終端
import logging
logger = logging.getLogger('my_app')
logger.setLevel(logging.DEBUG) # 設定 logger 本身的層級
# 建立檔案 handler(只接受 INFO 以上)
fh = logging.FileHandler('my_app.log')
fh.setLevel(logging.INFO)
# 建立 console handler(接受 DEBUG 以上)
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
# 定義 formatter
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%H:%M:%S'
)
fh.setFormatter(formatter)
ch.setFormatter(formatter)
# 加入 handler
logger.addHandler(fh)
logger.addHandler(ch)
logger.debug("這是除錯訊息,只會出現在終端")
logger.info("這是資訊訊息,兩邊皆會看到")
logger.error("發生錯誤!")
說明:透過不同的
Handler,可以細緻控制哪些層級寫入檔案、哪些層級顯示在螢幕。
範例 3:使用設定檔(INI 格式)管理
logging.conf
[loggers]
keys=root,exampleLogger
[handlers]
keys=consoleHandler,fileHandler
[formatters]
keys=defaultFormatter
[logger_root]
level=WARNING
handlers=consoleHandler
[logger_exampleLogger]
level=DEBUG
handlers=consoleHandler,fileHandler
qualname=exampleLogger
propagate=0
[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=defaultFormatter
args=(sys.stdout,)
[handler_fileHandler]
class=FileHandler
level=INFO
formatter=defaultFormatter
args=('example.log', 'a')
[formatter_defaultFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
datefmt=%Y-%m-%d %H:%M:%S
Python 程式碼
import logging
import logging.config
logging.config.fileConfig('logging.conf')
logger = logging.getLogger('exampleLogger')
logger.debug('除錯訊息 – 只會出現在終端')
logger.info('資訊訊息 – 兩邊皆會看到')
logger.error('錯誤訊息 – 同上')
說明:將設定抽離到外部檔案,可在不改程式碼的情況下調整層級、輸出位置,適合部署環境的差異化需求。
範例 4:自訂 Filter 只記錄特定模組訊息
import logging
class ModuleFilter(logging.Filter):
def __init__(self, module_name):
super().__init__()
self.module_name = module_name
def filter(self, record):
return record.name.startswith(self.module_name)
logger_a = logging.getLogger('module_a')
logger_b = logging.getLogger('module_b')
handler = logging.FileHandler('module_a.log')
handler.setLevel(logging.INFO)
handler.addFilter(ModuleFilter('module_a')) # 只保留 module_a 的訊息
formatter = logging.Formatter('%(asctime)s - %(name)s - %(message)s')
handler.setFormatter(formatter)
logger_a.addHandler(handler)
logger_b.addHandler(handler) # 仍會接收到訊息,但會被 filter 拒絕
logger_a.info('module_a 產生的訊息')
logger_b.info('module_b 產生的訊息')
說明:
Filter可以根據自訂條件過濾日誌,常用於多模組大型專案的分層記錄。
範例 5:結合例外資訊的完整日誌
import logging
logging.basicConfig(
filename='exception.log',
level=logging.ERROR,
format='%(asctime)s - %(levelname)s - %(message)s\n%(exc_info)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
def divide(a, b):
try:
return a / b
except ZeroDivisionError:
logging.exception("除以零錯誤") # logging.exception 會自動加入 traceback
return None
result = divide(10, 0)
說明:
logging.exception()只在例外上下文中有效,會自動把 traceback(堆疊資訊)寫入日誌,對除錯非常有幫助。
常見陷阱與最佳實踐
| 陷阱 | 可能的後果 | 解決方案 |
|---|---|---|
重複呼叫 basicConfig |
只會套用第一次的設定,後續設定被忽略,導致日誌不如預期 | 只在程式入口(如 if __name__ == '__main__':)呼叫一次,或改用 logging.config |
| 忘記設定 Handler 的層級 | 低層級訊息被意外過濾,或高層級訊息寫入錯誤檔案 | 明確為每個 Handler 設定 setLevel() |
| 使用全域 logger,無法區分模組 | 日誌難以追蹤來源,維護成本上升 | 為每個模組使用 logging.getLogger(__name__) |
| 日誌檔案未輪替 (log rotation) | 檔案持續增長,最終耗盡磁碟空間 | 使用 logging.handlers.RotatingFileHandler 或 TimedRotatingFileHandler |
| 在多執行緒/多程序環境下共享同一個 FileHandler | 訊息交錯、檔案寫入衝突 | 為每個執行緒或程序使用獨立的 Handler,或使用 QueueHandler + QueueListener |
推薦的最佳實踐
- 模組化 logger:每個
.py檔案最上方放logger = logging.getLogger(__name__)。 - 使用設定檔:在
development、staging、production環境使用不同的logging.conf,避免硬編碼。 - 啟用日誌輪替:
RotatingFileHandler(maxBytes=5*1024*1024, backupCount=5),每 5 MB 產生新檔,保留 5 份。 - 在例外捕獲時使用
logging.exception:自動加入 traceback,省去手寫traceback.format_exc()。 - 避免在庫 (library) 中設定根 logger:只提供 logger,讓使用者自行決定輸出方式。
實際應用場景
| 場景 | 為何需要 logging |
設定範例 |
|---|---|---|
| Web API (Flask / Django) | 記錄每一次請求、回應狀態、錯誤堆疊,便於日後分析流量與問題 | RotatingFileHandler + JSONFormatter(方便 ELK) |
| 資料處理批次 | 大量資料轉換時需追蹤每筆失敗紀錄,避免整批失敗 | FileHandler + WARN/ERROR 只寫入失敗資料行號 |
| IoT 裝置 | 裝置資源有限,只能保留最近幾筆重要日誌 | TimedRotatingFileHandler('log', when='D', backupCount=7) |
| CI/CD pipeline | 在自動化測試階段收集測試輸出與例外 | StreamHandler(輸出到標準輸出),CI 系統自動收集 |
| 安全審計 | 必須記錄使用者操作、權限變更等敏感行為 | FileHandler + CRITICAL 層級,並加上 audit filter |
總結
logging 是 Python 生態系中最成熟且彈性的日誌解決方案。透過 Logger、Handler、Formatter、Level 四大核心概念,我們可以:
- 彈性輸出:同時寫檔、印在終端、發送 Email。
- 層級控制:在開發、測試、上線環境以不同的訊息量切換。
- 易於維護:使用設定檔或程式碼集中管理,避免散落的
print()。 - 支援例外追蹤:
logging.exception自動加入 traceback,快速定位錯誤根源。
只要遵守 模組化 logger、適當的層級設定、日誌輪替 以及 在例外時使用 exception 的最佳實踐,就能讓你的 Python 專案在除錯、監控與維運上大幅提升效率與可靠性。祝你寫出乾淨、可追蹤的程式碼,日誌成為你的好幫手!