【pytest】finalizer 执行顺序:FILO 原则
文章目录
- 核心概念:FILO(First-In-Last-Out,先进后出)
- 第一部分:Yield Fixtures 的执行顺序
- 代码示例
- 执行结果
- 详细执行流程分析
- 关键理解点
- 为什么是这个顺序?
- 第二部分:addfinalizer 的执行顺序
- 代码示例
- 执行结果
- 详细执行流程
- 栈的可视化
- 实际应用示例
- 第三部分:底层实现揭秘
- Yield Fixture 的底层实现原理
- 完整执行流程图
- 为什么要这样设计?
- 多层嵌套示例
- 复杂场景
- 执行结果
- 调用栈可视化
- 实际应用建议
- 1. **利用 FILO 特性设计资源清理**
- 2. **避免顺序陷阱**
- 总结
核心概念:FILO(First-In-Last-Out,先进后出)
Finalizers 遵循 栈式执行顺序,类似于:
- 函数调用栈
- 资源获取即初始化(RAII)模式
- 嵌套的
with
语句
原则:最后注册的 finalizer 最先执行
第一部分:Yield Fixtures 的执行顺序
代码示例
def test_bar(fix_w_yield1, fix_w_yield2):print("test_bar")@pytest.fixture
def fix_w_yield1():yieldprint("after_yield_1")@pytest.fixture
def fix_w_yield2():yieldprint("after_yield_2")
执行结果
test_bar
.after_yield_2
after_yield_1
详细执行流程分析
时间轴 →1. Setup 阶段(从左到右)┌─────────────────┐│ fix_w_yield1 │ ← 第一个参数,先执行 setup│ 执行到 yield │└─────────────────┘↓┌─────────────────┐│ fix_w_yield2 │ ← 第二个参数,后执行 setup│ 执行到 yield │└─────────────────┘2. 测试执行┌─────────────────┐│ test_bar() │ ← 打印 "test_bar"└─────────────────┘3. Teardown 阶段(从右到左,栈式弹出)┌─────────────────┐│ fix_w_yield2 │ ← 最后 setup 的,最先 teardown│ after_yield_2 │ 打印 "after_yield_2"└─────────────────┘↓┌─────────────────┐│ fix_w_yield1 │ ← 最先 setup 的,最后 teardown│ after_yield_1 │ 打印 "after_yield_1"└─────────────────┘
关键理解点
def test_bar(fix_w_yield1, fix_w_yield2):# ↑ ↑# 左边 右边(最后一个参数)
- Setup 顺序:左 → 右(
fix_w_yield1
→fix_w_yield2
) - Teardown 顺序:右 → 左(
fix_w_yield2
→fix_w_yield1
)
为什么是这个顺序?
这是为了依赖关系的正确性:
@pytest.fixture
def database():db = create_db()yield dbdb.close() # 最后关闭@pytest.fixture
def user(database): # 依赖 databaseu = database.create_user()yield udatabase.delete_user(u) # 先删除用户def test_something(database, user):pass
执行顺序:
- Setup:
database
→user
- Test 运行
- Teardown:
user
→database
✅(正确:先删用户,再关数据库)
如果反过来就会出错:
- ❌ 先关数据库,再删用户 → 失败!
第二部分:addfinalizer 的执行顺序
代码示例
from functools import partial
import pytest@pytest.fixture
def fix_w_finalizers(request):request.addfinalizer(partial(print, "finalizer_2")) # 第一个注册request.addfinalizer(partial(print, "finalizer_1")) # 第二个注册def test_bar(fix_w_finalizers):print("test_bar")
执行结果
test_bar
.finalizer_1
finalizer_2
详细执行流程
注册顺序(时间从上到下):
┌────────────────────────────┐
│ request.addfinalizer(f2) │ ← 第一个注册(进栈)
├────────────────────────────┤
│ request.addfinalizer(f1) │ ← 第二个注册(进栈)
└────────────────────────────┘执行顺序(FILO,从栈顶弹出):
┌────────────────────────────┐
│ finalizer_1 执行 │ ← 最后注册的,最先执行
├────────────────────────────┤
│ finalizer_2 执行 │ ← 最先注册的,最后执行
└────────────────────────────┘
栈的可视化
栈结构注册时: 执行时:第二次注册 第一个执行↓ ↑
┌────────┐ ┌────────┐
│ f1 │ ← 栈顶 │ f1 │ ← 先出栈
├────────┤ ├────────┤
│ f2 │ │ f2 │ ← 后出栈
└────────┘ └────────┘↑
第一次注册 第二个执行
实际应用示例
@pytest.fixture
def complex_setup(request):# 步骤1:分配内存memory = allocate_memory()request.addfinalizer(lambda: free_memory(memory))# 步骤2:创建数据库连接(依赖内存)db = create_db_connection(memory)request.addfinalizer(lambda: db.close())# 步骤3:创建用户(依赖数据库)user = db.create_user()request.addfinalizer(lambda: db.delete_user(user))return user
清理顺序(FILO):
- ✅ 删除用户(最后注册,最先执行)
- ✅ 关闭数据库
- ✅ 释放内存(最先注册,最后执行)
这保证了依赖关系的正确性!
第三部分:底层实现揭秘
Yield Fixture 的底层实现原理
# 我们写的代码:
@pytest.fixture
def my_fixture():print("setup")yield "resource"print("teardown")# pytest 内部实际做的事(伪代码):
@pytest.fixture
def my_fixture(request):print("setup")# 创建生成器对象gen = generator_function()resource = next(gen) # 执行到 yield,获取资源# 注册 finalizerdef resume_generator():try:next(gen) # 继续执行生成器,运行 yield 后的代码except StopIteration:passrequest.addfinalizer(resume_generator)return resource
完整执行流程图
┌─────────────────────────────────────────────────────┐
│ @pytest.fixture │
│ def my_fixture(): │
│ print("setup") ← 1. 执行到这里 │
│ yield "resource" ← 2. 暂停,返回资源 │
│ print("teardown") ← 4. finalizer 触发执行 │
└─────────────────────────────────────────────────────┘↓3. 测试运行完成↓
┌─────────────────────────────────────────────────────┐
│ request.addfinalizer(resume_generator) │
│ ↓ │
│ resume_generator() 被调用 │
│ ↓ │
│ next(gen) → 继续执行 yield 之后的代码 │
└─────────────────────────────────────────────────────┘
为什么要这样设计?
- 统一机制:所有清理逻辑都通过
addfinalizer
管理 - 顺序保证:利用栈结构自动保证正确的清理顺序
- 异常安全:即使测试失败,finalizers 也会执行
多层嵌套示例
复杂场景
@pytest.fixture
def fix_a():print("setup A")yieldprint("teardown A")@pytest.fixture
def fix_b(fix_a): # 依赖 fix_aprint("setup B")yieldprint("teardown B")@pytest.fixture
def fix_c(fix_b): # 依赖 fix_bprint("setup C")yieldprint("teardown C")def test_example(fix_c):print("TEST")
执行结果
setup A ← 最底层依赖,先执行
setup B ← 中间层
setup C ← 最上层
TEST ← 测试运行
teardown C ← 最上层,先清理(FILO)
teardown B ← 中间层
teardown A ← 最底层,最后清理
调用栈可视化
Setup(入栈): Teardown(出栈):┌─────┐ ┌─────┐│ C │ ← 最后 │ C │ ← 最先├─────┤ ├─────┤│ B │ │ B │├─────┤ ├─────┤│ A │ ← 最先 │ A │ ← 最后└─────┘ └─────┘
实际应用建议
1. 利用 FILO 特性设计资源清理
@pytest.fixture
def app_environment(request):# 按依赖顺序注册config = load_config()request.addfinalizer(lambda: config.cleanup())db = init_database(config)request.addfinalizer(lambda: db.shutdown())cache = init_cache(db)request.addfinalizer(lambda: cache.clear())return {'config': config, 'db': db, 'cache': cache}
清理顺序自动正确:cache → db → config ✅
2. 避免顺序陷阱
# ❌ 错误:顺序会反过来
@pytest.fixture
def bad_example(request):request.addfinalizer(cleanup_step_1) # 实际最后执行request.addfinalizer(cleanup_step_2)request.addfinalizer(cleanup_step_3) # 实际最先执行
# ✅ 正确:按想要的执行顺序反向注册
@pytest.fixture
def good_example(request):request.addfinalizer(cleanup_step_3) # 想最后执行,先注册request.addfinalizer(cleanup_step_2)request.addfinalizer(cleanup_step_1) # 想最先执行,最后注册
总结
方面 | 说明 |
---|---|
核心原则 | FILO(先进后出,栈式结构) |
Yield fixtures | 最右边的参数最先清理 |
addfinalizer | 最后注册的最先执行 |
底层实现 | yield 通过 addfinalizer 实现 |
设计目的 | 保证依赖资源的正确清理顺序 |
最佳实践 | 按依赖关系顺序创建资源,自动逆序清理 |
这种设计模式在编程中非常常见,就像俄罗斯套娃:最外层的最先关闭,最内层的最后关闭,确保不会出现"还在用的资源已经被销毁"的问题。