一、装饰器的本质:给函数 “加功能” 的语法糖
装饰器(Decorator)是 Python 中一种高阶函数的语法糖(本身是一种语法糖,即和labbda一样是一种简化的写法),核心作用是:
在不修改原函数代码、不改变原函数调用方式的前提下,通过定义一个新的装饰器函数,并通过装饰器函数与原函数绑定,给函数添加额外功能(比如日志、计时、权限校验、pytest 夹具能力等)。
@ 就是装饰器的语法符号,用来简化装饰器的调用写法(注意这里的@是在绑定时使用,而不是在定义时使用)。
二、第一步:先懂装饰器的基础语法(3 个核心层级)
2.1、层级 1:无参数装饰器(最基础)
1. 定义装饰器(本质是函数)
装饰器本身是一个函数,这个函数的形参是接收另外一个函数名、装饰器函数的返回值也是一个函数:(重点中的重点就是理解这句话)
python
# 定义装饰器:给函数加“执行日志”功能
def log_decorator(func): # 接收被装饰的函数作为参数
# 定义包装函数(真正执行额外逻辑+原函数逻辑)
def wrapper():
print(f"【日志】函数 {func.__name__} 开始执行") # 额外功能:打印日志
func() # 执行原函数的核心逻辑
print(f"【日志】函数 {func.__name__} 执行结束") # 额外功能:打印日志
return wrapper # 返回包装后的新函数
这里在函数中定义另外一个函数,是python特有的性质。
从结构上区分,最外层定义的就是装饰器函数,特征就是形参是必须是接收函数名
第二层嵌套了一个包装函数,这个包装函数中就实现了,我们需要加入的步骤,也就是printf语句实现打印函数日志的功能。
此外第二层,还执行了一个很重要的功能就是,调用一次我们需要执行的函数。
最后1步是,先退出包装函数,返回包装函数名称,这一步不理解没关系,我们后续慢慢看最后1步到底起了什么作用
2. 使用装饰器(@符号)
用 @装饰器名 放在需要被修饰器修饰函数(说人话就是这个函数需要实现修饰器函数中的功能)定义上方,等价于 原函数 = 装饰器(原函数):
python
# 方式1:用@语法(推荐,简洁)
@log_decorator #log_decorator就是前个章节中定义的解释器函数
def test_func():
print("我是原函数的核心逻辑")
# 方式2:等价的手动调用(不用@,但麻烦)
# def test_func():
# print("我是原函数的核心逻辑")
# test_func = log_decorator(test_func) #这里就相当于直接调用了
#无论方式1还是方式2是实现一个功能,就是将被修饰函数与修饰器函数绑定
# 调用函数(和普通函数调用方式完全一样,调用上和普通函数一模一样)
test_func()
我认为,第二种更加有利于,我们理解
# 方式2:等价的手动调用(不用@,但麻烦)
# def test_func(): #正常定义
# print("我是原函数的核心逻辑")
# test_func = log_decorator(test_func) #这里,先看=左边,相当与直接调用了修饰器函数
执行结果:
【日志】函数 test_func 开始执行
我是原函数的核心逻辑
【日志】函数 test_func 执行结束
从执行结果中可以看出,如果以上代码中,我们没有定义修饰器函数,并将修饰器函数和test_fun这个函数绑定,执行的结果只会有一句“我是原函数的核心逻辑”,对不对。
这样的性质下,我们可以将一些常用的操作,打包为修饰器函数,然后将修饰器函数和需要使用修饰器函数中功能的函数执行绑定操作
2.2、层级 2:带参数的装饰器(比如 @pytest.fixture (scope="function"))
当装饰器需要传参数(如 scope="function"),需要在基础装饰器外层再包一层函数:
python
# 定义带参数的装饰器:可指定日志级别
def log_decorator_with_level(level): # 外层:接收装饰器参数
def decorator(func): # 中层:接收被装饰函数
def wrapper(): # 内层:包装函数
print(f"【{level}日志】函数 {func.__name__} 开始执行")
func()
print(f"【{level}日志】函数 {func.__name__} 执行结束")
return wrapper
return decorator
# 使用带参数的装饰器
@log_decorator_with_level(level="INFO") # 对应你看到的@pytest.fixture(scope="function")
def test_func2():
print("我是带参数装饰器的测试函数")
test_func2()
执行结果:
plaintext
【INFO日志】函数 test_func2 开始执行
我是带参数装饰器的测试函数
【INFO日志】函数 test_func2 执行结束
2.3、层级 3:装饰器适配带参数 / 返回值的原函数
实际开发中函数常带参数、有返回值,装饰器需要兼容:
python
# 通用装饰器:支持原函数传参、返回值
def universal_decorator(func):
def wrapper(*args, **kwargs): # *args接收位置参数,**kwargs接收关键字参数
print(f"【通用日志】函数 {func.__name__} 开始执行,参数:{args}, {kwargs}")
result = func(*args, **kwargs) # 给原函数传参并执行
print(f"【通用日志】函数 {func.__name__} 执行结束,返回值:{result}")
return result # 返回原函数的返回值
return wrapper
# 被装饰的函数带参数、有返回值
@universal_decorator
def add(a, b):
return a + b
# 调用
print(add(2, 3))
执行结果:
plaintext
【通用日志】函数 add 开始执行,参数:(2, 3), {}
【通用日志】函数 add 执行结束,返回值:5
5
三、回到你的场景:@pytest.fixture 解析
你看到的 @pytest.fixture(scope="function") 就是带参数的装饰器:
pytest.fixture是 pytest 库定义的装饰器函数;scope="function"是传给装饰器的参数,指定 “夹具的作用域为函数级”;- 装饰器的作用:把下面的函数标记为「pytest 测试夹具」,让 pytest 自动管理它的执行时机(比如每个测试函数执行前 / 后运行)。
完整实战示例(贴合你的测试场景):
python
import pytest
# 带参数的装饰器:定义函数级夹具
@pytest.fixture(scope="function")
def excel_test_data():
# 前置操作:准备测试数据
print("\n【夹具】准备Excel测试数据")
data = {"path": "test.xlsx", "sheet": "Sheet1"}
yield data # 把数据传给测试用例
# 后置操作:清理数据
print("\n【夹具】清理Excel测试数据")
# 测试用例使用夹具
def test_parse_excel(excel_test_data):
print(f"测试解析Excel:{excel_test_data['path']}")
assert excel_test_data["sheet"] == "Sheet1"
def test_check_sheet(excel_test_data):
print(f"测试检查Sheet:{excel_test_data['sheet']}")
assert len(excel_test_data["path"]) > 0
执行结果(每个测试函数都会触发夹具):
plaintext
【夹具】准备Excel测试数据
测试解析Excel:test.xlsx
【夹具】清理Excel测试数据
【夹具】准备Excel测试数据
测试检查Sheet:Sheet1
【夹具】清理Excel测试数据
四、装饰器的核心特性(必懂)
- 语法糖:
@装饰器是原函数 = 装饰器(原函数)的简化写法,本质是函数替换; - 无侵入:不修改原函数代码,只扩展功能(符合 “开闭原则”);
- 复用性:一个装饰器可以给多个函数加相同功能(比如所有测试函数都用同一个夹具);
- 链式调用:多个装饰器可叠加使用(比如
@decor1+@decor2)。
总结
@是 Python 装饰器的语法符号,不是函数名的一部分;- 装饰器核心是 “给函数加功能不修改原代码”,
@pytest.fixture(scope="function")就是给函数添加「pytest 夹具」功能; - 装饰器分 3 类:无参数、带参数、通用型(适配原函数传参 / 返回值),实际开发中通用型最常用。

8486

被折叠的 条评论
为什么被折叠?



