专栏:FastAPI 从零到后端接口实战 标签:Python、装饰器、日志封装、colorlog、FastAPI 工程化、代码规范 前置知识:Day1 async/await 异步基础、aiohttp 异步请求
前言
Day1 我们完成了异步语法与 aiohttp 网络请求编写,但代码调试只能依靠原生 print 打印信息。
原生 print 无法区分日志等级、没有时间戳、不能持久化保存日志文件,项目体量变大后排查 BUG 极其麻烦。同时 FastAPI 内部大量使用装饰器做路由注册、参数校验、依赖注入,不懂装饰器很难看懂 FastAPI 源码写法。
本章两大核心目标:
- 基于 colorlog 封装彩色分级日志(控制台彩色打印 + 本地文件落盘)
- 由浅入深吃透装饰器(基础语法、传参装饰器、wraps 元信息修复、异步函数装饰器)
产出可复用的日志工具类,后续所有 FastAPI 项目直接导入使用。
一、Python 原生 logging 模块基础认知
1.1 日志 5 个等级(由低 → 高)
| 等级 | 方法 | 适用场景 |
|---|---|---|
| DEBUG | logging.debug() |
开发调试、打印变量值、流程节点 |
| INFO | logging.info() |
正常业务运行记录,接口调用、请求进入 |
| WARNING | logging.warning() |
非报错异常,配置缺失、请求超时等警告 |
| ERROR | logging.error() |
程序发生异常、接口执行报错 |
| CRITICAL | logging.critical() |
致命错误,服务无法正常运行 |
日志默认只会输出 WARNING 及以上级别 ,日常开发一般配置全局等级为
DEBUG。
1.2 原生 logging 缺陷
- 控制台文字全部单色,不同级别日志肉眼无法快速区分;
- 配置代码冗余,每个文件都要重复编写 handler、formatter;
- 无法同时实现「控制台打印」+「文件写入」两种输出。
二、colorlog 彩色日志库安装与基础使用
colorlog 对原生 logging 进行封装,给不同日志等级配置区分颜色,大幅提升调试效率。
安装依赖
shell
pip install colorlog
最简彩色日志测试代码
python
import logging
import colorlog
# 配置日志格式
log_format = colorlog.ColoredFormatter(
"%(log_color)s%(asctime)s - %(name)s - %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
log_colors={
"DEBUG": "cyan",
"INFO": "green",
"WARNING": "yellow",
"ERROR": "red",
"CRITICAL": "bold_red",
}
)
# 创建控制台处理器
console_handler = colorlog.StreamHandler()
console_handler.setFormatter(log_format)
# 初始化 logger 对象
logger = colorlog.getLogger("fastapi_project")
logger.addHandler(console_handler)
logger.setLevel(logging.DEBUG)
# 分级打印测试
logger.debug("DEBUG:接口入参数据打印")
logger.info("INFO:服务正常启动成功")
logger.warning("WARNING:请求响应缓慢,存在超时风险")
logger.error("ERROR:数据库查询抛出异常")
logger.critical("CRITICAL:Redis 连接失败,服务不可用")
运行后在控制台可以看到不同颜色的日志输出,一目了然。
三、封装通用日志工具类(工程化标准写法)
实现能力
- ✅ 控制台彩色输出
- ✅ 日志自动写入本地
logs/文件夹,按日期分割日志文件 - ✅ 全局统一配置,项目任意文件
from ... import即可使用
logger_utils.py 完整代码
python
import os
import logging
from logging.handlers import TimedRotatingFileHandler
import colorlog
# 1、定义日志根目录(相对当前脚本所在目录)
LOG_ROOT = os.path.join(os.path.dirname(__file__), "logs")
if not os.path.exists(LOG_ROOT):
os.makedirs(LOG_ROOT)
# 2、日志文件名称,按日期划分
LOG_FILE = os.path.join(LOG_ROOT, "fastapi_run.log")
# 3、控制台彩色格式
console_formatter = colorlog.ColoredFormatter(
fmt="%(log_color)s%(asctime)s | %(filename)s:%(lineno)d | %(levelname)s | %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
log_colors={
"DEBUG": "cyan",
"INFO": "green",
"WARNING": "yellow",
"ERROR": "red",
"CRITICAL": "bold_red,bg_white",
},
)
# 4、文件日志普通格式(文件不需要颜色代码)
file_formatter = logging.Formatter(
fmt="%(asctime)s | %(filename)s:%(lineno)d | %(levelname)s | %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
# 5、文件处理器:按天切割日志,保留 30 天日志记录
file_handler = TimedRotatingFileHandler(
filename=LOG_FILE,
when="D",
interval=1,
backupCount=30,
encoding="utf-8"
)
file_handler.setFormatter(file_formatter)
# 6、控制台处理器
console_handler = colorlog.StreamHandler()
console_handler.setFormatter(console_formatter)
# 7、组装 logger
def get_logger(name="fastapi_log"):
log = logging.getLogger(name)
log.setLevel(logging.DEBUG)
# 避免重复添加 handler
if not log.handlers:
log.addHandler(file_handler)
log.addHandler(console_handler)
return log
# 全局实例化导出
logger = get_logger()
提示 :
if not log.handlers的判断能避免多次调用get_logger()时重复添加 handler,导致日志重复打印。
项目中调用示例
python
from logger_utils import logger
def test_log():
logger.info("项目日志工具加载完成")
logger.debug("测试 debug 日志")
try:
1 / 0
except Exception as e:
logger.error("程序运算异常", exc_info=True)
if __name__ == "__main__":
test_log()
exc_info=True 会自动打印异常堆栈信息,排查报错必备参数。
四、Python 装饰器核心知识(FastAPI 重中之重)
4.1 装饰器本质
装饰器 = 接收函数为参数,返回新函数的高阶函数
作用:在不修改原有函数内部代码的前提下,对函数执行前后进行功能增强(统计耗时、打印日志、权限校验、异常捕获等)。
4.2 基础无参装饰器模板
python
import time
# 定义装饰器
def timer_decorator(func):
def wrapper(*args, **kwargs):
start = time.time()
# 执行原函数
res = func(*args, **kwargs)
cost = round(time.time() - start, 3)
print(f"函数 {func.__name__} 执行耗时:{cost} s")
return res
return wrapper
# 使用装饰器语法糖 @
@timer_decorator
def demo_func(sleep_time):
time.sleep(sleep_time)
return "函数执行完毕"
if __name__ == "__main__":
print(demo_func(1.2))
执行流程拆解 :demo_func(1.2) → 实际调用的是 wrapper(1.2) → wrapper 内部计时 + 调用原函数 + 打印耗时 → 返回原函数结果。
4.3 带参数的装饰器写法
外层再包裹一层函数,用来接收装饰器自身参数:
python
import time
def timer_param(threshold: float):
"""threshold: 超时阈值,单位秒"""
def outer(func):
def wrapper(*args, **kwargs):
s = time.time()
ret = func(*args, **kwargs)
c = time.time() - s
if c > threshold:
print(f"函数 {func.__name__} 执行超时,耗时 {c:.3f}s,阈值 {threshold}s")
else:
print(f"函数 {func.__name__} 正常执行,耗时 {c:.3f}s")
return ret
return wrapper
return outer
# 传入阈值参数 0.5 秒
@timer_param(threshold=0.5)
def test_sleep(t):
time.sleep(t)
test_sleep(0.8) # 输出:执行超时
test_sleep(0.2) # 输出:正常执行
三层嵌套结构 :timer_param(参数) → 返回 outer → @outer 修饰 func → 返回 wrapper。
4.4 装饰器修复函数元信息(wraps)
被装饰后的函数会丢失原函数名称、注释文档,使用 functools.wraps 修复:
python
from functools import wraps
def log_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"开始执行函数:{func.__name__}")
return func(*args, **kwargs)
return wrapper
@log_decorator
def hello():
"""测试函数文档注释"""
pass
print(hello.__name__) # 输出:hello(不加 wraps 会输出 wrapper)
print(hello.__doc__) # 输出:测试函数文档注释(不加 wraps 会输出 None)
记忆口诀 :定义装饰器时,
@wraps(func)写在最内层wrapper上面,养成习惯。
4.5 装饰器修饰异步函数(适配 Day1 异步代码)
异步函数必须在内部 await 调用原函数,普通同步装饰器无法直接修饰 async 函数:
python
import asyncio
import time
from functools import wraps
def async_timer(func):
@wraps(func)
async def wrapper(*args, **kwargs):
st = time.time()
result = await func(*args, **kwargs) # 注意:await 调用原函数
cost = time.time() - st
print(f"异步函数 {func.__name__} 耗时:{cost:.3f}s")
return result
return wrapper
# 修饰 aiohttp 异步请求函数
@async_timer
async def async_test():
await asyncio.sleep(1)
print("异步任务执行完成")
asyncio.run(async_test())
同步装饰器 vs 异步装饰器对比:
| 同步装饰器 | 异步装饰器 | |
|---|---|---|
wrapper 定义 |
def wrapper(...) |
async def wrapper(...) |
| 调用原函数 | func(*args, **kwargs) |
await func(*args, **kwargs) |
| 适用场景 | 同步函数 | async def 异步函数 |
五、结合日志 + 装饰器实战:统一接口耗时 & 异常捕获装饰器
日常 FastAPI 开发高频通用装饰器,直接放入工具类复用:
python
import time
from functools import wraps
from logger_utils import logger
def api_wrapper(func):
"""统一接口日志 + 耗时统计 + 异常捕获装饰器"""
@wraps(func)
async def wrapper(*args, **kwargs):
start = time.time()
try:
resp = await func(*args, **kwargs)
cost = round(time.time() - start, 4)
logger.info(f"接口 {func.__name__} 请求正常,耗时:{cost}s")
return resp
except Exception:
cost = round(time.time() - start, 4)
logger.error(f"接口 {func.__name__} 执行异常,耗时:{cost}s", exc_info=True)
raise # 原样抛出异常,保留原始调用栈
return wrapper
要点说明:
exc_info=True会自动将异常堆栈写入日志,无需手动拼接错误信息。raise(不带异常对象)会原样抛出,保留原始 traceback,比raise e更规范。
六、Day2 知识点总结
| 序号 | 知识点 | 掌握程度 |
|---|---|---|
| 1 | logging 5 级分级规则 | 理解并会使用 |
| 2 | colorlog 控制台彩色日志 | 能独立配置 |
| 3 | 可复用日志工具类(控制台 + 文件) | 能直接导入使用 |
| 4 | 基础无参装饰器 | 能手写模板 |
| 5 | 带参数装饰器(三层嵌套) | 理解原理 |
| 6 | functools.wraps 元信息修复 |
养成习惯 |
| 7 | 异步函数装饰器编写 | 适配 FastAPI 异步接口 |
| 8 | 日志 + 装饰器联合实战 | 产出通用工具 |
下期预告
Day3:FastAPI 第一个 HelloWorld 接口、路径参数、查询参数、请求体 POST 参数详解,正式开始编写后端接口。