在 Python 應用程式開發中,尤其在多模組架構下,有效管理日誌記錄至關重要。本文將介紹如何使用 dictConfig 和 logging API 實作多模組日誌記錄,並提供一個可捕捉異常的日誌裝飾器,提升程式碼的可維護性和除錯效率。多模組專案的日誌記錄常面臨整合和管理的挑戰,dictConfig 提供集中式的組態方案,透過設定檔定義不同模組的日誌級別、輸出格式和目標,簡化了日誌管理的複雜度。相較之下,logging API 則提供更細緻的程式化控制,適合需要動態調整日誌行為的場景。兩種方法各有優劣,開發者可依專案需求選擇合適的策略。此外,我們將示範如何利用 Python 裝飾器簡化日誌程式碼,並設計一個能捕捉並記錄函式異常的裝飾器,減少重複程式碼並提升程式碼的健壯性。

多模組日誌記錄的實作

在開發複雜的 Python 應用程式時,日誌記錄是一個不可或缺的功能。它可以幫助我們追蹤程式的執行過程、除錯問題以及監控應用程式的執行狀態。然而,當應用程式涉及多個模組時,如何有效地進行日誌記錄就變得至關重要。這裡,玄貓將帶領你學習如何使用 dictConfig 來組態多模組日誌記錄。

實作多模組日誌記錄

首先,我們需要建立一個範例 Python 包,這個包中包含多個模組。假設我們有一個簡單的計算工具包,其中包含基本的數學運算功能。以下是我們需要建立的目錄結構:

sample_package/
├── __init__.py
├── main.py
├── settings.py
└── utils/
    ├── __init__.py
    └── minimath.py

目錄結構解析

  • sample_package/:這是我們的主要包目錄。
  • __init__.py:這些空白的 __init__.py 檔案告訴 Python 這些目錄是一個包的一部分。
  • main.py:這是我們的主要執行檔案。
  • settings.py:這裡包含我們的日誌組態。
  • utils/:這是一個子目錄,用來存放輔助模組。
  • minimath.py:這裡包含一些基本的數學運算功能。

組態日誌

main.py 中,我們將使用 dictConfig 來組態日誌。以下是 main.py 的內容:

# main.py

import logging.config
import settings
from utils import minimath

logging.config.dictConfig(settings.LOG_CONFIG)

def main():
    log = logging.getLogger(__name__)
    log.debug("Logging is configured.")

    minimath.add(4, 5)

if __name__ == "__main__":
    main()

內容解密:

  • import logging.config:匯入日誌組態模組。
  • import settings:匯入我們的設定檔案,其中包含日誌組態。
  • from utils import minimath:匯入我們的數學運算模組。
  • logging.config.dictConfig(settings.LOG_CONFIG):使用字典組態來設定日誌。
  • log = logging.getLogger(__name__):取得當前模組的日誌器。
  • log.debug("Logging is configured."):記錄一條除錯資訊。
  • minimath.add(4, 5):呼叫數學運算模組中的加法函式。

日誌組態檔案

接下來,我們需要在 settings.py 中定義我們的日誌組態。以下是 settings.py 的內容:

# settings.py

LOG_CONFIG = {
    "version": 1,
    "loggers": {
        "": { # root logger
            "handlers": ["default"],
            "level": "WARNING",
            "propagate": False,
        },
        "utils.minimath": {
            "handlers": ["fileHandler", "default"],
            "level": "DEBUG",
            "propagate": False,
        },
        "__main__": { # if __name__ == '__main__'
            "handlers": ["default"],
            "level": "DEBUG",
            "propagate": False,
        },
    },
    "handlers": {
        "fileHandler": {
            "class": "logging.FileHandler",
            "formatter": "file_formatter",
            "filename": "settings.log",
        },
        "default": {
            "class": "logging.StreamHandler",
            "formatter": "stream_formatter",
        },
    },
    "formatters": {
        "file_formatter": {
            "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
        },
        "stream_formatter": {
            "format": "%(asctime)s - %(name)s - %(filename)s - %(lineno)s - %(message)s",
            "datefmt": "%a %d %b %Y",
        },
    },
}

內容解密:

  • "version":日誌組態的版本號,目前為 1。
  • "loggers":定義了三個日誌器:
    • " ":根日誌器,處理所有未明確指定的日誌資訊。
    • "utils.minimath":專門處理 minimath 模組的日誌資訊。
    • "__main__":專門處理直接執行模組時的日誌資訊。
  • "handlers":定義了兩個處理器:
    • "fileHandler":將日誌資訊寫入到 settings.log 檔案中。
    • "default":將日誌資訊輸出到標準輸出流中。
  • "formatters":定義了兩個格式化器:
    • "file_formatter":用於檔案處理器的格式化器。
    • "stream_formatter":用於標準輸出流處理器的格式化器。

模組實作

接下來,我們需要在 minimath.py 中實作一些基本的數學運算功能,並新增日誌記錄。以下是 minimath.py 的內容:

# minimath.py

import logging

module_logger = logging.getLogger(__name__)

def add(x, y):
    module_logger.info("Adding %s and %s", x, y)
    return x + y

def subtract(x, y):
    return x - y

def multiply(x, y):
    return x * y

def divide(x, y):
    return x / y

內容解密:

  • module_logger = logging.getLogger(__name__):取得當前模組的日誌器。
  • add 函式中,使用 module_logger.info 認為了一條資訊級別的日誌資訊。

執行結果

現在,我們可以執行 main.py 來檢視結果。以下是預期的輸出:

2023-10-10 - __main__ - main.py - 11 - Logging is configured.
2023-10-10 - utils.minimath - Adding 4 and 5

日誌多模組記錄總結

透過上述步驟,我們成功地為多個模組組態了日誌記錄。透過使用 __name__ 動態建立不同模組的日誌器,我們可以更精細地控制每個模組的日誌輸出。這種方法不僅使得程式碼更加清晰和可維護,還能幫助我們在除錯和監控應用程式時更加高效。

希望這篇文章能幫助你更好地理解和實作多模組日誌記錄。如果你有任何問題或需要進一步的幫助,請隨時聯絡玄貓。

日誌記錄從多個模組中提取的重要技巧

在複雜的Python專案中,日誌記錄從多個模組中提取資訊是一個常見的需求。Python的日誌模組提供了靈活的功能來實作這一點。本文將探討如何從多個模組中進行日誌記錄,並且介紹使用日誌API和dictConfig兩種常見方法。

日誌API的基本概念

在開始之前,我們需要了解一些基本概念。首先,我們可以將Python程式碼分為多個模組,每個模組都可以獨立執行。當我們在某個模組中增加了__init__.py指令碼後,這個資料夾就變成了一個可以匯入的套件,這也允許我們匯入該資料夾內的任何Python檔案。

設定檔案的分析

讓我們來看一下設定檔案。當你仔細研究設定檔案時,你會發現「主」記錄器僅連線到StreamHandler。而「utils.minimath」則同時連線到StreamHandler和FileHandler,這就是為什麼你會在控制檯和檔案中看到相同的輸出。

假設你在mini-math.py檔案中增加了if __name__ == "__main__"這行程式碼,那麼這個檔案可以直接執行,但輸出會有所不同。你可以自行嘗試這個練習來確認。

日誌API與dictConfig的比較

Python的日誌模組提供了兩種主要方法來從多個模組中記錄日誌:使用日誌API和使用dictConfig。這兩種方法都有其優缺點。

使用日誌API

日誌API是最直接的方法,它允許你在程式碼中直接組態記錄器。以下是一個簡單的範例:

import logging

# 建立記錄器
logger = logging.getLogger('my_logger')
logger.setLevel(logging.DEBUG)

# 建立控制檯處理器
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)

# 建立格式化器並將其加入處理器
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)

# 將處理器加入記錄器
logger.addHandler(ch)

# 記錄訊息
logger.debug('This is a debug message')
logger.info('This is an info message')

使用dictConfig

dictConfig則是更靈活的方法,它允許你透過組態檔案來組態記錄器。以下是一個範例:

import logging
import logging.config

# 組態字典
logging_config = {
    'version': 1,
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
            'level': 'DEBUG',
            'formatter': 'simple'
        }
    },
    'formatters': {
        'simple': {
            'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
        }
    },
    'loggers': {
        'my_logger': {
            'handlers': ['console'],
            'level': 'DEBUG',
        }
    }
}

# 組態日誌
logging.config.dictConfig(logging_config)

# 建立記錄器
logger = logging.getLogger('my_logger')

# 記錄訊息
logger.debug('This is a debug message')
logger.info('This is an info message')

日誌處理器與格式化器

無論使用哪種方法,你都可以建立強大的日誌處理器和格式化器。你還可以新增篩選器來進一步控制日誌輸出。

實務應用

在實際應用中,你幾乎總是會將日誌API程式碼放在一個單獨的模組中,而不是放在main.py中。使用dictConfig、檔案組態或日誌API,你可以建立非常強大的日誌處理器和格式化器。

建立一個日誌裝飾器

在Python開發中,日誌記錄是監控程式執行情況的一個重要工具。然而,當我們需要為多個函式新增日誌時,重複編寫相同的程式碼會變得非常繁瑣。為瞭解決這一問題,我們可以使用裝飾器來封裝日誌程式碼,從而簡化開發流程。

本章將介紹如何建立一個異常日誌裝飾器,並解釋裝飾器的基本概念。

裝飾器的基本概念

裝飾器是一種高階函式(higher-order function),它接受另一個函式作為引數並傳回一個新函式。新函式通常會包含原始函式的一些額外功能。在本章中,我們將建立一個異常日誌裝飾器來捕捉並記錄函式執行過程中的異常。

建構物件說明圖示

  flowchart TD;
    A[Decorator] --> B[Original Function];
    B --> C[Wrapper Function];
    C --> D[Exception Handling];
    D --> E[Logging];

內容解密:

上圖示顯示了裝飾器的基本結構。

  • A(Decorator):接受原始函式作為引數並傳回包裝函式。
  • B(Original Function):被裝飾的原始函式。
  • C(Wrapper Function):包裝函式,包含原始函式及額外功能。
  • D(Exception Handling):例外處理邏輯。
  • E(Logging):記錄異常資訊。

建立異常日誌裝飾器

接下來,我們將建立一個簡單的異常日誌裝飾器。這個裝飾器會捕捉並記錄被裝飾函式中的異常資訊。

import functools
import logging

def create_logger():
    """
    建立一個logging物件並傳回它
    """
    logger = logging.getLogger("example_logger")
    logger.setLevel(logging.INFO)

    # 建立檔案處理程式
    fh = logging.FileHandler("test.log")

    fmt = ("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
    formatter = logging.Formatter(fmt)
    fh.setFormatter(formatter)

    # 新增處理程式到logging物件
    logger.addHandler(fh)
    return logger

def exception(function):
    """
    一個裝飾者, 包住傳遞進來之function並紀錄例外狀況發生時之log
    """

    @functools.wraps(function)
    def wrapper(*args, **kwargs):
        logger = create_logger()
        try:
            return function(*args, **kwargs)
        except:
            # 紀錄例外狀況
            err = "There was an exception in "
            err += function.__name__
            logger.exception(err)

            # 再次丟擲例外狀況
            raise

    return wrapper

內容解密:

上述程式碼展示了一個簡單的異常日誌裝飾器。

  • create_logger():使用logging API建立了一個名為"example_logger"物件,並設定其輸出到test.log檔案。
  • exception():這是我們定義之裝飾者, 接受function作為引數。
  • wrapper():包覆住function, 並在執行時捕捉其可能丟擲之例外狀況, 若有則寫入log之test.log。
  • functools.wraps():確保包覆後之function保持原本之名稱及docstring, 提供更好的可讀性與除錯經驗。

測試裝飾器

接下來我們建立測試程式以驗證我們定義之異常log機制.

@exception
def divide(a, b):
    """
    整除a / b, 傳回結果 (若b == 0, 則丟擲ZeroDivisionError)
    """
    return a / b

try:
   print(divide(4,2)) # 預期正確輸出2.0, 無例外狀況產生.
   print(divide(4,0)) # 預期丟擲ZeroDivisionError.
except ZeroDivisionError as e:
   print("Caught ZeroDivisionError: ", e)

內容解密:

上述測試程式碼展示如何使用我們定義之exception()裝飾者.

  • divide(a,b):整除a / b, 若b == 0, 則丟擲ZeroDivisionError.
  • 在測試時可觀察到:
    • divide(4,2) 的正確輸出值2.0.
    • divide(4,0) 丟擲ZeroDivisionError.
    • 丟擲之例外狀況被捕捉並在test.log登入.

這樣便完成了異常log機制之實作.