文章目录
-
- 核心概念:FILO(First-In-Last-Out,先进后出)
- [第一部分:Yield Fixtures 的执行顺序](#第一部分:Yield Fixtures 的执行顺序)
- [第二部分:addfinalizer 的执行顺序](#第二部分:addfinalizer 的执行顺序)
- 第三部分:底层实现揭秘
- 多层嵌套示例
- 实际应用建议
-
- [1. **利用 FILO 特性设计资源清理**](#1. 利用 FILO 特性设计资源清理)
- [2. **避免顺序陷阱**](#2. 避免顺序陷阱)
- 总结
核心概念:FILO(First-In-Last-Out,先进后出)
Finalizers 遵循 栈式执行顺序,类似于:
- 函数调用栈
- 资源获取即初始化(RAII)模式
- 嵌套的
with
语句
原则:最后注册的 finalizer 最先执行
第一部分:Yield Fixtures 的执行顺序
代码示例
python
def test_bar(fix_w_yield1, fix_w_yield2):
print("test_bar")
@pytest.fixture
def fix_w_yield1():
yield
print("after_yield_1")
@pytest.fixture
def fix_w_yield2():
yield
print("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"
└─────────────────┘
关键理解点
python
def test_bar(fix_w_yield1, fix_w_yield2):
# ↑ ↑
# 左边 右边(最后一个参数)
- Setup 顺序 :左 → 右(
fix_w_yield1
→fix_w_yield2
) - Teardown 顺序 :右 → 左(
fix_w_yield2
→fix_w_yield1
)
为什么是这个顺序?
这是为了依赖关系的正确性:
python
@pytest.fixture
def database():
db = create_db()
yield db
db.close() # 最后关闭
@pytest.fixture
def user(database): # 依赖 database
u = database.create_user()
yield u
database.delete_user(u) # 先删除用户
def test_something(database, user):
pass
执行顺序:
- Setup:
database
→user
- Test 运行
- Teardown:
user
→database
✅(正确:先删用户,再关数据库)
如果反过来就会出错:
- ❌ 先关数据库,再删用户 → 失败!
第二部分:addfinalizer 的执行顺序
代码示例
python
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 │ ← 后出栈
└────────┘ └────────┘
↑
第一次注册 第二个执行
实际应用示例
python
@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 的底层实现原理
python
# 我们写的代码:
@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,获取资源
# 注册 finalizer
def resume_generator():
try:
next(gen) # 继续执行生成器,运行 yield 后的代码
except StopIteration:
pass
request.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 也会执行
多层嵌套示例
复杂场景
python
@pytest.fixture
def fix_a():
print("setup A")
yield
print("teardown A")
@pytest.fixture
def fix_b(fix_a): # 依赖 fix_a
print("setup B")
yield
print("teardown B")
@pytest.fixture
def fix_c(fix_b): # 依赖 fix_b
print("setup C")
yield
print("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 特性设计资源清理
python
@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. 避免顺序陷阱
python
# ❌ 错误:顺序会反过来
@pytest.fixture
def bad_example(request):
request.addfinalizer(cleanup_step_1) # 实际最后执行
request.addfinalizer(cleanup_step_2)
request.addfinalizer(cleanup_step_3) # 实际最先执行
python
# ✅ 正确:按想要的执行顺序反向注册
@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 实现 |
设计目的 | 保证依赖资源的正确清理顺序 |
最佳实践 | 按依赖关系顺序创建资源,自动逆序清理 |
这种设计模式在编程中非常常见,就像俄罗斯套娃:最外层的最先关闭,最内层的最后关闭,确保不会出现"还在用的资源已经被销毁"的问题。