本文 AI 產出,尚未審核

Python 課程 – 例外與錯誤處理

主題:logging 模組基礎


簡介

在實務開發中,程式錯誤執行狀態系統行為的記錄往往比單純的印出除錯訊息更為重要。當程式上線後,若只靠 print() 或 IDE 的除錯視窗,就很難在生產環境快速定位問題,甚至可能遺漏關鍵的安全或效能資訊。Python 內建的 logging 模組提供了一套彈性且可擴充的日誌機制,讓開發者可以在不同的「層級」記錄訊息、將日誌寫入檔案、甚至傳送到遠端服務。

本篇文章將從 基礎概念實作範例常見陷阱最佳實踐,一步步帶你了解如何在 Python 專案中正確使用 logging,讓除錯變得更有系統、維護更省力。


核心概念

1. 為何不直接使用 print()

print() logging
只能輸出到標準輸出(stdout) 支援多種 Handler(檔案、串流、SMTP、SysLog 等)
沒有層級(level)概念 提供 DEBUG / INFO / WARNING / ERROR / CRITICAL 五種層級
難以在程式執行時動態調整輸出 可在執行期間透過設定檔或程式碼改變層級
不支援格式化、時間戳記、檔案位置等資訊 自動加入時間、模組名稱、行號等豐富資訊

2. logging 的基本組件

  1. Logger:產生日誌訊息的「來源」物件。通常以模組名稱或自訂名稱建立。
  2. Handler:決定日誌訊息的「去向」——例如寫入檔案、輸出到終端、發送 Email。
  3. Formatter:定義日誌訊息的文字格式(時間、層級、訊息內容等)。
  4. 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.RotatingFileHandlerTimedRotatingFileHandler
在多執行緒/多程序環境下共享同一個 FileHandler 訊息交錯、檔案寫入衝突 為每個執行緒或程序使用獨立的 Handler,或使用 QueueHandler + QueueListener

推薦的最佳實踐

  1. 模組化 logger:每個 .py 檔案最上方放 logger = logging.getLogger(__name__)
  2. 使用設定檔:在 developmentstagingproduction 環境使用不同的 logging.conf,避免硬編碼。
  3. 啟用日誌輪替RotatingFileHandler(maxBytes=5*1024*1024, backupCount=5),每 5 MB 產生新檔,保留 5 份。
  4. 在例外捕獲時使用 logging.exception:自動加入 traceback,省去手寫 traceback.format_exc()
  5. 避免在庫 (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 專案在除錯、監控與維運上大幅提升效率與可靠性。祝你寫出乾淨、可追蹤的程式碼,日誌成為你的好幫手!