Skip to content

日志、配置文件、装饰器✅

一个项目,无论多么小,都应该包含日志、配置文件这两个模块。日志用于分析和优化,配置文件让程序使用更加灵活。

Python 日志

在 Python 中,可以使用内置的 logging 模块来记录日志。以下是一个简单的示例,说明如何使用 logging 模块创建并记录日志:

python
import logging

# 配置日志记录器
logging.basicConfig(filename='app.log', level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

def main():
    # 记录不同级别的日志信息
    logging.debug(' 这是一个调试信息 ')
    logging.info(' 这是一个信息性的日志 ')
    logging.warning(' 这是一个警告信息 ')
    logging.error(' 这是一个错误信息 ')
    logging.critical(' 这是一个严重错误信息 ')

if __name__ == "__main__":
    main()

在这个例子中,basicConfig() 方法用于配置日志记录器,它指定了日志文件名 (filename='app.log')、日志级别 (level=logging.INFO) 和日志记录格式 (format='%(asctime)s - %(levelname)s - %(message)s')。

然后,在 main() 函数中,通过调用不同级别的日志记录方法来记录不同类型的日志信息,例如 logging.debug()logging.info()logging.warning()logging.error()logging.critical()

运行此代码后,日志信息将被记录到指定的文件 app.log 中,并根据其级别进行标识。你可以根据需要修改日志文件名、级别和格式等参数。

运行后,我们发现当前目录下多了一个 app.log ,内容如下所示:

log_demo

虽然有了时间,有了日志级别,但是,还不够使用,因为我们还需要:

  1. 缺少打印的代码行信息,方便快速定位。
  2. 如果有异常发生,也需要在日志中看到异常发生。
  3. 我希望日志按天记录。
  4. 日志不仅记录到文件中,也要输出到终端。

完整代码如下:

python
import logging
from logging.handlers import TimedRotatingFileHandler
import sys
import traceback

# 日志格式配置
log_format = "%(asctime)s - %(levelname)s - %(filename)s:%(lineno)s - %(message)s"

# 配置文件处理器,每天分割一次日志
file_handler = TimedRotatingFileHandler("my_app.log", when="midnight", interval=1)
file_handler.suffix = "%Y-%m-%d"
file_handler.setFormatter(logging.Formatter(log_format))

# 配置流处理器(控制台输出)
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(logging.Formatter(log_format))

# 配置日志记录器
logger = logging.getLogger()
logger.setLevel(logging.INFO)
logger.addHandler(file_handler)
logger.addHandler(stream_handler)  # 添加流处理器

# 函数捕获未处理的异常
def handle_exception(exc_type, exc_value, exc_traceback):
    """
    Handle uncaught exceptions.
    """
    if issubclass(exc_type, KeyboardInterrupt):
        sys.__excepthook__(exc_type, exc_value, exc_traceback)
        return

    logger.error("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback))

# 设置为默认的异常捕获方法
sys.excepthook = handle_exception

# 示例:记录一个信息日志和一个故意制造的异常
# logger.info("This is an info message.")

# raise RuntimeError("This is an example error")

这样,当发生异常时,我们可以通过日志看到是哪一行代码发生了异常,从而方便查找异常原因:

假如,把上述代码保存为 log.py,这样在其他文件打印日志的时候,就可以这样来使用。

python
from log import logger
logger.info("record a log")

Python 配置文件

配置文件,最常用的格式是 json 和 yaml,我比较推荐 yaml,因为可以添加注释,从而更容易理解和修改。这里我给出一个即支持 json 又支持 yaml 的代码,可以复用在任何项目中。

文件 config.py,关键部分我都做了注释,你会很容易看懂。

注意,yaml 可能还未加入到 Python 的标准库中,使用时如果报 Module Not Found,可以执行 pip install pyyaml 安装一下。

python
import json
import yaml
import os
import copy

class ConfigManager:
    # 类变量,用于存储类的单例实例
    _instance = None

    def __new__(cls):
        # 重写__new__方法以实现单例模式
        if cls._instance is None:
            cls._instance = super(ConfigManager, cls).__new__(cls)
            cls._instance._config = {}  # 初始化配置字典
        return cls._instance

    def load_config(self, filepath):
        # 从指定文件加载配置

        # 检查文件是否存在
        if not os.path.exists(filepath):
            raise FileNotFoundError(f"Configuration file '{filepath}' not found.")

        # 获取文件扩展名
        file_ext = os.path.splitext(filepath)[1]
        with open(filepath, 'r', encoding="utf-8") as file:
            # 根据文件类型加载配置
            if file_ext == '.json':
                self._config = json.load(file)
            elif file_ext in ['.yml', '.yaml']:
                self._config = yaml.safe_load(file)
            else:
                raise ValueError("Unsupported file format. Please use JSON or YAML.")

    def get(self, key):
        # 获取单个配置项,返回深拷贝以防外部修改
        return copy.deepcopy(self._instance._config.get(key))

    def get_all(self):
        # 获取所有配置,返回深拷贝以防外部修改
        return copy.deepcopy(self._instance._config)

# 示例用法
if __name__ == "__main__":
    config_manager = ConfigManager()
    # 加载并访问配置值
    config_manager.load_config("config.yml")
    config_data = config_manager.get_all()
    print(config_data)

config.py 说明:

  1. 单例模式:ConfigManager 使用单例模式,确保程序中只有一个配置管理器实例。因此,如果不需要实时获取配置信息,config_manager.load_config("config.yml") 可以仅执行一次。

  2. 支持的格式:支持从JSON和YAML文件加载配置。

  3. 安全的数据访问:通过get和get_all方法提供对配置数据的访问,返回配置项的深拷贝,防止外部代码意外修改配置数据。

  4. 异常处理:如果配置文件不存在或文件格式不支持,会抛出异常。

python 装饰器

这一小节,我们先认识一下装饰器,然后分享一下我自己写的装饰器和简单用法。

1. 什么是装饰器

装饰器是 Python 中一种非常有用的特性,它允许用户在不修改原有函数代码的情况下,增加额外的功能。装饰器本质上是一个 Python 函数,它可以包裹另一个函数或方法,并可以在被包裹的函数执行前后执行一些额外的代码。

2. 基本的装饰器

让我们从一个简单的例子开始,了解装饰器是如何工作的。

2.1 创建一个基本装饰器

一个基本的装饰器通常有以下结构:

python
def my_decorator(func):
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper

# 使用装饰器
@my_decorator
def say_hello():
    print("Hello!")

say_hello()

在这个例子中,my_decorator 是一个装饰器,它接受一个函数 func 作为参数。wrapper 是一个内部函数,它包裹了 func 的调用。当你调用 say_hello() 时,实际上是在调用 wrapper()

2.2 输出

Something is happening before the function is called.
Hello!
Something is happening after the function is called.

3. 带参数的装饰器

有时候你需要一个能够接受参数的装饰器。

3.1 创建一个带参数的装饰器

python
def decorator_with_args(arg1, arg2):
    def my_decorator(func):
        def wrapper(*args, **kwargs):
            print(f"Arguments for decorator: {arg1}, {arg2}")
            func(*args, **kwargs)
        return wrapper
    return my_decorator

@decorator_with_args("arg1 value", "arg2 value")
def print_args(*args):
    for arg in args:
        print(arg)

print_args(1, 2, 3)

在这个例子中,decorator_with_args 是一个接受参数的装饰器工厂函数。它返回一个装饰器 my_decorator,该装饰器再返回实际的包装函数 wrapper

3.2 输出

Arguments for decorator: arg1 value, arg2 value
1
2
3

4. 类装饰器

除了普通函数,装饰器也可以用于类。

4.1 创建一个类装饰器

python
class MyDecorator:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print("Something is happening before the function is called.")
        self.func(*args, **kwargs)
        print("Something is happening after the function is called.")

@MyDecorator
def say_hello():
    print("Hello!")

say_hello()

类装饰器利用了类的 __call__ 特殊方法。当装饰函数时,实际上是创建了该类的一个实例,并传入被装饰的函数。

4.2 输出

Something is happening before the function is called.
Hello!
Something is happening after the function is called.

装饰器是 Python 中一个强大的工具,可以在不修改原有函数定义的情况下增加额外的功能。它们可以用于日志记录、性能测试、事务处理、缓存等多种场景。通过本教程,您应该对如何创建和使用装饰器有了基本的了解。

5个非常实用的装饰器

因为自己经常用得到,就写了一个装饰器发布在了 pypi。

这里介绍一下它的用法。使用前先安装:

sh
pip install somedecorators

1、 timeit

耗时统计装饰器,单位是秒,保留 4 位小数

使用方法:

python
from somedecorators import timeit
@timeit()
def test_timeit():
    time.sleep(1)

#test_timeit cost 1.0026 seconds

@timeit(logger = your_logger)
def test_timeit():
    time.sleep(1)

2、 timeout

超时装饰器,单位是秒,函数运行超过指定的时间会抛出 TimeOutError 异常。

使用方法:

python
import time
from somedecorators import timeout
@timeout(2)
def test_timeit():
    time.sleep(3)

#somedecorators.timeit.TimeoutError: Operation did not finish within 2 seconds

3、 retry

重试装饰器

  • 当被装饰的函数调用抛出指定的异常时,函数会被重新调用。
  • 直到达到指定的最大调用次数才重新抛出指定的异常,可以指定时间间隔,默认 5 秒后重试。
  • traced_exceptions 为监控的异常,可以为 None(默认)、异常类、或者一个异常类的列表或元组 tuple。
  • traced_exceptions 如果为 None,则监控所有的异常;如果指定了异常类,则若函数调用抛出指定的异常时,重新调用函数,直至成功返回结果。
  • 未出现监控的异常时,如果指定定了 reraised_exception 则抛出 reraised_exception,否则抛出原来的异常。
python
from somedecorators import retry 

@retry(
    times=2,
    wait_seconds=1,
    traced_exceptions=myException,
    reraised_exception=CustomException,
)
def test_retry():
    # time.sleep(1)
    raise myException


test_retry()

4、 email_on_exception

报错发邮件装饰器。当被装饰的函数调用抛出指定的异常时,函数发送邮件给指定的人员,使用独立的 djangomail 发邮件模块,非常好用。

  • recipient_list: 一个字符串列表,每项都是一个邮箱地址。recipient_list 中的每个成员都可以在邮件的 "收件人:" 中看到其他的收件人。
  • traced_exceptions 为监控的异常,可以为 None(默认)、异常类、或者一个异常类的元组。 traced_exceptions 如果为 None,则监控所有的异常;如果指定了异常类,则若函数调用抛出指定的异常时,发送邮件。

使用方法

首先在项目目录新建 settings.py,配置邮件服务器或企业微信,内容如下:

python
EMAIL_USE_LOCALTIME = True

#for unitest
#EMAIL_BACKEND = 'djangomail.backends.console.EmailBackend'
#EMAIL_BACKEND = 'djangomail.backends.smtp.EmailBackend'
EMAIL_USE_SSL = True
EMAIL_HOST = 'smtp.163.com' #可以换其他邮箱
EMAIL_PORT = 465
EMAIL_HOST_USER = 'your-username'
EMAIL_HOST_PASSWORD = '********'
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
SERVER_EMAIL = EMAIL_HOST_USER


# 用于发送企业微信
CORPID="**********************"  # 企业 ID
APPID="*******"  # 企业应用 ID
CORPSECRET="************************" # 企业应用 Secret

如果你的文件名不是 settings.py,假如是 mysettings.py 则需要修改环境变量:

python
os.environ.setdefault("SETTINGS_MODULE", "mysettings")

然后主程序中这样使用:

监控所有的异常
python
from somedecorators import email_on_exception 
#import os
#os.environ.setdefault("SETTINGS_MODULE", "settings") #默认配置,可以不写此行代码

@email_on_exception(['somenzz@163.com'])
def myfunc(arg):
    1/arg

myfunc(0)

你会收到如下的邮件信息,非常便于排查错误。

sh
Subject: myfunc(arg=0) raise Exception
From: your-username
To: somenzz@163.com
Date: Fri, 11 Jun 2021 20:55:01 -0500
Message-ID: 
 <162346290112.13869.15957310483971819045@1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa>

myfunc(arg=0) raise Exception: division by zero 
 traceback:
 Traceback (most recent call last):
  File "/Users/aaron/github/somenzz/somedecorators/somedecorators/email.py", line 35, in wrapper
    return func(*args, **kwargs)
  File "/Users/aaron/github/somenzz/somedecorators/tests/tests.py", line 55, in myfunc
    return 1/arg
ZeroDivisionError: division by zero

extra_msg = 严重错误
监控指定的异常
python

from somedecorators import email_on_exception
import os
os.environ.setdefault("SETTINGS_MODULE", "settings")

class Exception1(Exception):
    pass

class Exception2(Exception):
    pass

class Exception3(Exception):
    pass

@email_on_exception(['somenzz@163.com'],traced_exceptions = Exception2)
def myfunc(args):
    if args == 1:
        raise Exception1
    elif args == 2:
        raise Exception2
    else:
        raise Exception3

myfunc(2)

上述代码只有在 raise Exception2 时才会发送邮件:

不同的异常发给不同的人
python
@email_on_exception(['somenzz@163.com'],traced_exceptions = Exception2)
@email_on_exception(['others@163.com'],traced_exceptions = (Exception1, Exception3))
def myfunc(args):
    if args == 1:
        raise Exception1
    elif args == 2:
        raise Exception2
    else:
        raise Exception3

是不是非常方便?

5、 wechat_on_exception

异常信息发送企业微信,发送前需要在 settings.py 文件企业微信相关信息

settings.py 示例:

python

CORPID="**********************"  # 企业 ID
APPID="*******"  # 企业应用 ID
CORPSECRET="************************" # 企业应用 Secret

调用代码

python
@wechat_on_exception(['企业微信接收者ID'],traced_exceptions = Exception2)
def myfunc(args):
    if args == 1:
        raise Exception1
    elif args == 2:
        raise Exception2
    else:
        raise Exception3

练习题目

以下是一些关于 Python 日志、配置文件和装饰器的编程题目。请先尝试自己编写代码,然后对比答案,看看有没有不同的思路。

问题一:

题目: 编写一个 Python 程序,使用内置的 logging 模块设置日志,将日志同时输出到控制台和一个名为 log.txt 的文件中。日志级别应为 INFO

问题二:

题目: 创建一个 Python 配置文件,包含数据库连接信息和其他配置参数。编写一个程序,读取配置文件并输出其中的配置信息。

问题三:

题目: 编写一个装饰器函数,用于测量其他函数的执行时间,并将结果输出到日志中。

问题四:

题目: 编写一个 Python 程序,使用 argparse 模块解析命令行参数,其中包括一个日志级别参数,允许用户指定日志级别。

问题五:

题目: 创建一个 Python 类,具有一个装饰器方法,该装饰器方法用于记录类方法的调用,并将日志输出到文件。

以上五个关于 Python 日志、配置文件和装饰器的编程题目的参考答案