从print转向logging,会发现一个强大得多的工具。logging模块不仅能替代print的功能,还提供了分级输出、灵活配置和持久化等强大特性。
1. 为什么用logging替代print?
| 特性 | logging | |
|---|---|---|
| 输出控制 | 只能全部开启/关闭 | 可分级控制(DEBUG, INFO等) |
| 输出目的地 | 仅控制台 | 控制台、文件、网络等 |
| 信息丰富度 | 仅自定义内容 | 可包含时间、模块、行号等 |
| 性能影响 | 始终执行 | 可关闭低级别日志 |
| 生产环境适用性 | 差 | 优秀 |
为什么要用logging而不是print?
-
分级输出:logging有DEBUG, INFO, WARNING, ERROR, CRITICAL等不同级别,可以根据重要性分类日志。
-
控制输出:可以轻松地全局调整日志级别,只输出重要程度 above 某个级别的日志。
-
输出到多地方:可以同时将日志输出到控制台、文件、网络等。
-
包含更多信息:可以自动记录时间、模块名、行号等。
-
不会干扰正常输出:print会输出到标准输出,而logging可以输出到标准错误,并且可以分别控制。
如何使用logging?
基本步骤:
-
导入logging模块
-
配置logging(可选,但推荐)
-
使用logging来记录日志
2. logging的五个日志级别
import logging
# 从低到高五个级别
logging.debug("调试信息") # 最详细,用于调试
logging.info("普通信息") # 确认程序按预期运行
logging.warning("警告信息") # 表明意外情况,但程序仍运行
logging.error("错误信息") # 严重问题,影响部分功能
logging.critical("严重错误") # 致命错误,可能导致程序中断
- 日志级别从低到高:DEBUG < INFO < WARNING < ERROR < CRITICAL
- 设置一个级别后,只有该级别及以上的日志会被记录。所以,如果设置为INFO,那么DEBUG级别的日志不会输出,而INFO及以上的会输出。
- 开发时设置为DEBUG级别,看到所有信息
- 生产环境设置为WARNING级别,忽略调试信息
3. 基础使用方法
简单起步(替代print)
import logging
# 基本配置(替换print语句)
logging.basicConfig(level=logging.INFO)
# 原来的print("开始处理数据")
logging.info("开始处理数据")
# 原来的print(f"用户{user_id}登录成功")
logging.info("用户%s登录成功", user_id) # 更高效的格式化
# 原来的print("错误:文件不存在")
logging.error("文件不存在")
# 调试信息(原来可能用print来调试)
logging.debug("变量x的值:%s", x) # 该语句不会被输出
debug语句不输出是正确的、期望的行为!
这种设计的优势:
-
开发时 :设置
level=DEBUG,看到所有细节 -
生产环境 :设置
level=WARNING,只记录重要问题 -
无需修改代码:通过配置控制输出详细程度
4. 进阶配置:发挥logging真正威力
模块化使用(推荐)
# 在每个模块中这样使用
import logging
logger = logging.getLogger(__name__) # 使用模块名作为logger名称
class DataProcessor:
def __init__(self):
self.logger = logging.getLogger(__name__)
def process(self, data):
self.logger.debug("开始处理数据: %s", data[:100]) # 只记录前100字符
if not data:
self.logger.warning("接收到空数据")
return None
try:
result = self._complex_operation(data)
self.logger.info("数据处理成功,大小: %d", len(result))
return result
except ValueError as e:
self.logger.error("数据格式错误: %s", e)
raise
except Exception as e:
self.logger.critical("处理过程中发生意外错误", exc_info=True)
raise
logger的两种方式:
import logging
# 方式1:使用根logger(不推荐用于成熟项目)
logging.info("这是一条日志") # 使用根logger
# 方式2:使用自定义logger(推荐)
logger = logging.getLogger(__name__)
logger.info("这是一条日志") # 使用自定义logger
# 配置根logger(影响所有子logger)
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
import logging
# 简单脚本可以这样用
logging.basicConfig(level=logging.INFO)
logging.info("程序启动")
logging.error("发生错误")
import logging
# 创建logger(通常以模块名命名)
logger = logging.getLogger(__name__)
# 配置logger
def setup_logging():
handler = logging.StreamHandler()
formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(logging.INFO)
setup_logging()
# 使用自定义logger
logger.info("程序启动")
logger.error("发生错误")
为什么使用自定义logger更好?
1. 更好的日志追踪
# 使用根logger(难以追踪来源)
# 输出:INFO - 开始处理数据
# 使用自定义logger(清晰知道来自哪个模块)
# 输出:my_project.data_processor - INFO - 开始处理数据
# 输出:my_project.api - INFO - 接收API请求
2. 独立的配置控制
import logging
# 为不同模块设置不同级别
data_logger = logging.getLogger('my_project.data_processor')
data_logger.setLevel(logging.DEBUG) # 数据模块需要详细日志
api_logger = logging.getLogger('my_project.api')
api_logger.setLevel(logging.WARNING) # API模块只记录警告和错误
# 这样data_logger.debug()会输出,但api_logger.debug()不会
3. 模块化的日志管理
# 主程序
import logging
from my_module import my_function
from another_module import another_function
# 配置根logger
logging.basicConfig(level=logging.INFO)
def main():
logger = logging.getLogger(__name__)
logger.info("主程序启动")
# 这些函数内部使用它们自己的logger
my_function() # 使用my_module的logger
another_function() # 使用another_module的logger
logger.info("主程序结束")
if __name__ == "__main__":
main()
完整配置示例
import logging
import sys
def setup_logging():
# 创建logger
logger = logging.getLogger('my_app')
logger.setLevel(logging.DEBUG) # 捕获所有级别日志
# 避免重复添加handler
if logger.handlers:
return logger
# 创建formatter
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s'
)
# 控制台handler(输出INFO及以上)
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(formatter)
# 文件handler(输出DEBUG及以上,更详细)
file_handler = logging.FileHandler('app.log', encoding='utf-8')
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(formatter)
# 添加handler到logger
logger.addHandler(console_handler)
logger.addHandler(file_handler)
return logger
# 使用配置好的logger
logger = setup_logging()
处理器(handler)
我们使用logging模块时,可以设置多个处理器(handler),每个处理器可以设置不同的日志级别和输出目标(比如控制台、文件等)。这样,我们可以将不同级别的日志输出到不同的地方。
在上面的代码中,我们创建了两个处理器:
-
控制台处理器(StreamHandler):输出到标准输出(sys.stdout),级别为INFO,所以会处理INFO及以上的日志。
-
文件处理器(FileHandler):输出到文件'app.log',使用UTF-8编码,级别为DEBUG,所以会处理DEBUG及以上的日志。
然后,我们将这两个处理器添加到logger中。这样,当使用logger记录日志时,一条日志会同时被发送给这两个处理器(当然,每个处理器会根据自己设置的级别过滤,只有级别达到要求的日志才会被实际输出)。
例如,当我们调用logger.debug()时:
-
控制台处理器级别为INFO,所以不会在控制台输出。
-
文件处理器级别为DEBUG,所以会写入文件。
当我们调用logger.info()时:
-
控制台处理器会输出到控制台。
-
文件处理器会写入文件。
这样,我们就可以实现:
-
在控制台上看到INFO及以上的日志(相对简洁)
-
在文件中看到DEBUG及以上的日志(更详细,便于调试和问题追踪)
格式字符串
# 创建formatter(信件格式)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
这个格式字符串定义了日志记录的输出格式。其中每个百分号加括号的部分是一个占位符,会被具体的日志信息替换。
格式字符串:'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
各占位符含义:
-
%(asctime)s: 日志记录的时间,格式为字符串。默认格式是类似 '2024-01-15 10:30:00,000'。
-
%(name)s: 记录日志的logger的名称(通常是模块名,如
__name__)。 -
%(levelname)s: 日志级别的文本描述(如 'INFO', 'DEBUG'等)。
-
%(message)s: 日志消息内容。
例子:假设在模块 my_module中,有一条INFO级别的日志,消息是"用户登录成功",那么格式化后的日志可能像这样:
'2024-01-15 10:30:00,000 - my_module - INFO - 用户登录成功'
完整字段参考表
| 字段 | 说明 | 示例值 |
|---|---|---|
%(asctime)s |
日志创建时间 | 2024-01-15 10:30:25,123 |
%(name)s |
logger名称 | data_processor |
%(levelname)s |
日志级别 | INFO, ERROR |
%(message)s |
日志消息 | 开始处理数据 |
%(filename)s |
文件名 | data_processor.py |
%(pathname)s |
完整文件路径 | /home/user/project/data_processor.py |
%(funcName)s |
函数名 | process_data |
%(lineno)d |
行号 | 45 |
%(module)s |
模块名 | data_processor |
%(process)d |
进程ID | 1234 |
%(thread)d |
线程ID | 5678 |
%(threadName)s |
线程名 | MainThread |
5. 实用技巧和最佳实践
性能优化
python
# 使用判断避免不必要的字符串拼接
if logger.isEnabledFor(logging.DEBUG):
logger.debug("详细数据: %s", expensive_to_compute_value())
# 而不是(性能差):
# logger.debug("详细数据: %s", expensive_to_compute_value())
在Python的logging模块中,即使日志消息不会被输出(因为当前设置的日志级别高于该日志语句的级别),在调用日志语句时,如果使用了字符串格式化操作,那么这些格式化操作仍然会执行,这可能会带来不必要的性能开销。
python
import logging
# 假设这是一个计算成本很高的函数
def expensive_to_compute_value():
# 模拟耗时操作:大数据处理、复杂计算、数据库查询等
import time
time.sleep(2) # 模拟2秒的耗时计算
return "复杂计算结果"
# 不推荐的写法(性能差)
logging.basicConfig(level=logging.INFO) # 注意:级别是INFO,不是DEBUG
# 即使DEBUG日志不会输出,expensive_to_compute_value()也会被执行!
logger.debug("详细数据: %s", expensive_to_compute_value())
问题 :虽然日志级别是INFO,DEBUG消息不会输出,但expensive_to_compute_value()这个耗时函数仍然会被调用,浪费了2秒钟!
logger.isEnabledFor(level)方法用于检查当前logger是否会记录指定级别的日志。
python
import logging
logging.basicConfig(level=logging.INFO) # 设置为INFO级别
logger = logging.getLogger(__name__)
# 检查DEBUG级别是否启用
print(f"DEBUG是否启用: {logger.isEnabledFor(logging.DEBUG)}") # 输出: False
print(f"INFO是否启用: {logger.isEnabledFor(logging.INFO)}") # 输出: True
print(f"WARNING是否启用: {logger.isEnabledFor(logging.WARNING)}") # 输出: True
结构化日志记录(JSON格式)
import json
import logging
class StructuredLogger:
def __init__(self, name):
self.logger = logging.getLogger(name)
def log_transaction(self, user_id, action, status, details=None):
log_data = {
"timestamp": logging.getLogRecordFactory().time,
"user_id": user_id,
"action": action,
"status": status,
"details": details
}
self.logger.info(json.dumps(log_data))
# 使用
structured_logger = StructuredLogger(__name__)
structured_logger.log_transaction("user123", "login", "success")
根据不同环境配置
def setup_environment_logging(env='development'):
logger = logging.getLogger()
if env == 'development':
logging.basicConfig(level=logging.DEBUG, format='%(levelname)s - %(message)s')
elif env == 'production':
logging.basicConfig(
level=logging.WARNING,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('app.log'),
logging.StreamHandler()
]
)
6. 从print迁移的实用建议
-
逐步替换:不要一次性替换所有print,先替换重要的输出
-
保持习惯:继续用print做快速调试,但正式代码用logging
-
利用级别:
-
调试信息 →
logging.debug() -
普通信息 →
logging.info() -
警告信息 →
logging.warning() -
错误信息 →
logging.error()
-