解决Python logging重复打印问题:多线程与多模块共用Logger的陷阱

远山来客
远山来客
发布时间:2026-10-14| 2人浏览过
原创

本文深入解析Python logging模块在多线程、多进程及Web框架中常见的日志重复打印与内存溢出问题。通过重写日志类清空Handler以及为每个模块指定唯一Logger名称两种方案,彻底解决多文件共用Logger时的继承干扰。文章提供完整代码示例与执行结果对比,帮助开发者构建稳定高效的日志记录系统。

在Python开发中,使用logging模块时若涉及多线程、多进程处理,或在Web框架环境下(每个请求独立线程)自定义日志配置,极易出现日志重复打印甚至导致内存崩溃的情况。

针对上述问题,以下是具体的解决策略。

核心思路是重写日志处理逻辑,通过封装类来严格控制日志输出。


 class Log():
 import logging
 def __init__(self):
 self.logger = logging.getLogger(__name__)
 # 以下三行为清空上次文件
 # 这为清空当前文件的logging 因为logging会包含所有的文件的logging
 logging.Logger.manager.loggerDict.pop(__name__)
 # 将当前文件的handlers 清空 
 self.logger.handlers = []
 # 然后再次移除当前文件logging配置
 self.logger.removeHandler(self.logger.handlers)
 # 这里进行判断,如果logger.handlers列表为空,则添加,否则,直接去写日志
 if not self.logger.handlers:
 # loggger 文件配置路径
 self.handler = logging.FileHandler(os.getcwd() + '/logger/%s_log/%s_score.log' % (str(dt.date.today()), str(dt.date.today())))
 # logger 配置等级
 self.logger.setLevel(logging.DEBUG)
 # logger 输出格式
 formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(name)s - %(message)s')
 # 添加输出格式进入handler
 self.handler.setFormatter(formatter)
 # 添加文件设置金如handler
 self.logger.addHandler(self.handler)
 
 # 以下皆为重写方法 并且每次记录后清除logger
 def info(self,message=None):
 self.__init__()
 self.logger.info(message)
 self.logger.removeHandler(self.logger.handlers)
 
 def debug(self,message=None):
 self.__init__()
 self.logger.debug(message)
 self.logger.removeHandler(self.logger.handlers)
 
 def warning(self,message=None):
 self.__init__()
 self.logger.warning(message)
 self.logger.removeHandler(self.logger.handlers)
 
 def error(self,message=None):
 self.__init__()
 self.logger.error(message)
 self.logger.removeHandler(self.logger.handlers)
 
 def critical(self, message=None):
 self.__init__()
 self.logger.critical(message)
 self.logger.removeHandler(self.logger.handlers)

经实际测试,该方法有效可行。

此外需特别注意模块调用规范,例如在Web接口处调用日志模块并传递参数时,严禁使用全局变量,以免引发状态混乱。

补充说明:针对Python中多个文件共用Logger导致的重复打印问题,还有另一种解决方案。

问题背景与现象描述。

近期项目中,需利用Python的logging库将日志输出至文件,并将脚本纳入crontab定时执行。为此,编写了一个简单的Logger封装库。

具体实现代码如下。


#!/usr/bin/python
# -*- coding:utf-8 -*- 
import logging
import time
import os 
class Log(object):
 '''
封装后的logging
 ''' 
 def __init__(self, logger=None, log_cate='search'):
  '''
   指定保存日志的文件路径,日志级别,以及调用文件
   将日志存入到指定的文件中
  ''' 
  # 创建一个logger
  self.logger = logging.getLogger(logger)
  self.logger.setLevel(logging.DEBUG)
  # 创建一个handler,用于写入日志文件
  self.log_time = time.strftime("%Y_%m_%d")
  file_dir = os.getcwd() + '/../log'
  if not os.path.exists(file_dir):
   os.mkdir(file_dir)
  self.log_path = file_dir
  self.log_name = self.log_path + "/" + log_cate + "." + self.log_time + '.log'
  # print(self.log_name)
 
  fh = logging.FileHandler(self.log_name, 'a') # 追加模式 这个是python2的
  # fh = logging.FileHandler(self.log_name, 'a', encoding='utf-8') # 这个是python3的
  fh.setLevel(logging.INFO)
 
  # 再创建一个handler,用于输出到控制台
  ch = logging.StreamHandler()
  ch.setLevel(logging.INFO)
 
  # 定义handler的输出格式
  formatter = logging.Formatter(
   '[%(asctime)s] %(filename)s->%(funcName)s line:%(lineno)d [%(levelname)s]%(message)s')
  fh.setFormatter(formatter)
  ch.setFormatter(formatter)
 
  # 给logger添加handler
  self.logger.addHandler(fh)
  self.logger.addHandler(ch)
 
  # 添加下面一句,在记录日志之后移除句柄
  # self.logger.removeHandler(ch)
  # self.logger.removeHandler(fh)
  # 关闭打开的文件
  fh.close()
  ch.close()
 
 def getlog(self):
  return self.logger

设计目标是让所有需要日志功能的模块只需导入该封装库并直接调用即可。例如,在模块中调用logger的特定方法。

文件 a.py 的内容如下。


#!/usr/bin/python
# -*- coding:utf-8 -*-
 
from common.log import Log
log = Log().getlog()
log.info("I am a.py")

文件 b.py 的内容如下。


#!/usr/bin/python
# -*- coding:utf-8 -*-
 
from common.log import Log
log = Log().getlog()
log.info("I am b.py")

文件 c.py 的内容如下。


#!/usr/bin/python
# -*- coding:utf-8 -*-
 
import a
import b
from common.log import Log 
log = Log().getlog()
log.info("I am c.py")

此时执行 c.py 脚本,观察到的输出结果如下。

终端执行命令:python c.py

[2019-01-14 15:58:35,807] a.py-><module> line:6 [INFO]I am a.py

[2019-01-14 15:58:35,808] b.py-><module> line:6 [INFO]I am b.py

[2019-01-14 15:58:35,808] b.py-><module> line:6 [INFO]I am b.py

[2019-01-14 15:58:35,809] c.py-><module> line:8 [INFO]I am c.py

[2019-01-14 15:58:35,809] c.py-><module> line:8 [INFO]I am c.py

[2019-01-14 15:58:35,809] c.py-><module> line:8 [INFO]I am c.py

从输出可见,a.py、b.py 和 c.py 共用了同一个Logger实例,导致日志被重复打印多次。

问题原因深度分析。

根据现象推断,不同文件间的日志系统存在相互影响。在 a.py、b.py 和 c.py 中,调用方式为 log = Log().getlog(),即 self.logger = logging.getLogger(logger),由于未传递 logger 参数,获取的 self.logger 实际上是 RootLogger。

RootLogger 是 Python 程序中全局唯一的 Logger 对象祖先。因此,对 RootLogger 的配置会影响所有日志输出。简而言之,先加载的文件中的日志设置会影响后加载的文件,后者会遍历 Logger 的继承关系。在此示例中,b.py 在 a.py 之后被 import,因此 b.py 会执行一次自身的 Logger,再执行一次 a.py 中打开的 RootLogger,依此类推,导致重复输出。

解决方案详解。

避免使用默认的 RootLogger,改为为每个 Logger 指定唯一的名称。

修改后的 a.py 代码如下。


from common.log import Log
log = Log(__name__).getlog()
log.info("I am a.py")

修改后的 b.py 代码如下。


from common.log import Log
log = Log(__name__).getlog()
log.info("I am b.py")

修改后的 c.py 代码如下。


import b
import a 
from common.log import Log 
log = Log(__name__).getlog()
log.info("I am c.py")

再次执行 c.py,最新结果如下。

终端执行命令:python c.py

[2019-01-14 16:24:12,008] b.py-><module> line:6 [INFO]I am b.py

[2019-01-14 16:24:12,009] a.py-><module> line:6 [INFO]I am a.py

[2019-01-14 16:24:12,009] c.py-><module> line:10 [INFO]I am c.py

此时日志不再重复,符合预期,问题得到解决。

以上为个人经验分享,旨在提供参考。如有错误或未考虑周全之处,欢迎指正。

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。