阿发聊测试 · 技术进阶系列
小白VS大佬:同样功能,Python代码差距就在一个@符号
从0到1彻底搞懂Python装饰器,面试被问到再也不怕
你好,我是阿发。
上周帮一个粉丝改代码,看到一段让我哭笑不得的实现。他要给5个函数加日志,结果每个函数里面都写了一遍logging代码。整整50行重复代码,就为了打印一句"函数XX开始执行"。我说:"你知道装饰器吗?一个@符号就能搞定。"他一脸茫然:"@不是邮箱符号吗?"说实话,这个场景我见过太多了。很多测试工程师写Python只会堆代码,遇到重复逻辑就复制粘贴,完全不知道装饰器这种优雅的解决方案。今天这篇文章,我用"小白VS大佬"的对比方式,把装饰器从概念到实战讲得明明白白。同样的功能,小白写100行,大佬只需要一个@符号。
SECTION 01
开场:同样的功能,两种写法对比
先来看一个真实场景:你需要给多个函数添加执行时间统计功能。比如有登录、注册、查询用户三个接口测试函数,你想知道每个函数执行了多久。
小白写法:重复代码堆砌
```python
import time
def login(username, password):
start_time = time.time() # 第1处:记录开始时间
print(f'登录函数开始执行')
# 模拟登录逻辑
time.sleep(0.5)
print(f'登录成功: {username}')
end_time = time.time() # 第2处:记录结束时间
print(f'登录函数执行耗时: {end_time - start_time:.2f}秒') # 第3处:计算耗时
return True
def register(email, password):
start_time = time.time() # 重复第1处
print(f'注册函数开始执行')
# 模拟注册逻辑
time.sleep(0.8)
print(f'注册成功: {email}')
end_time = time.time() # 重复第2处
print(f'注册函数执行耗时: {end_time - start_time:.2f}秒') # 重复第3处
return True
def query_user(user_id):
start_time = time.time() # 重复第1处
print(f'查询用户函数开始执行')
# 模拟查询逻辑
time.sleep(0.3)
print(f'查询用户成功: {user_id}')
end_time = time.time() # 重复第2处
print(f'查询用户函数执行耗时: {end_time - start_time:.2f}秒') # 重复第3处
return {'id': user_id, 'name': '张三'}
```
大佬写法:一个@符号搞定
```python
import time
def timer(func):
def wrapper(*args, **kwargs):
start_time = time.time()
print(f'{func.__name__}函数开始执行')
result = func(*args, **kwargs)
end_time = time.time()
print(f'{func.__name__}函数执行耗时: {end_time - start_time:.2f}秒')
return result
return wrapper
@timer
def login(username, password):
time.sleep(0.5)
print(f'登录成功: {username}')
return True
@timer
def register(email, password):
time.sleep(0.8)
print(f'注册成功: {email}')
return True
@timer
def query_user(user_id):
time.sleep(0.3)
print(f'查询用户成功: {user_id}')
return {'id': user_id, 'name': '张三'}
```
阿发说实话
同样的功能,小白写了45行,大佬只写了30行。如果有10个函数需要加计时,小白要写100行,大佬还是30行。这就是装饰器的威力——它把横切关注点(日志、计时、权限校验等)从业务逻辑中分离出来,实现代码复用。
要理解装饰器,首先要明白一个Python的核心概念:函数是一等公民。这意味着函数可以作为参数传递,可以作为返回值,可以赋值给变量。
函数可以作为参数传递
```python
def greet(name):
return f'Hello, {name}'
def call_function(func, arg):
return func(arg)
result = call_function(greet, '阿发')
print(result) # 输出: Hello, 阿发
```
函数可以作为返回值
```python
def outer():"""外层函数"""
def inner():"""内层函数"""
print('这是内层函数')
return inner # 返回内层函数,注意没有括号
my_func = outer()
my_func() # 输出: 这是内层函数
```
一句话理解装饰器:装饰器就是一个接收函数作为参数,并返回一个新函数的函数。这个新函数会在原函数执行前后添加额外的逻辑。
很多人觉得@符号很神秘,其实它只是一个语法糖(Syntactic Sugar),让代码更简洁易读而已。
```python
# 写法1:使用@语法糖(推荐)
@timer
def login(username, password):
time.sleep(0.5)
return True
# 写法2:等价的手动调用
def login(username, password):
time.sleep(0.5)
return True
login = timer(login) # 把login函数传给timer,再把返回值重新赋值给login
```
这两种写法完全等价!@timer 就是 login = timer(login) 的简写。理解了这一点,装饰器就不再神秘了。
SECTION 04
不带参数的装饰器:最基础也最常用
不带参数的装饰器是最基础的形式,我们在开场例子中已经看到了。让我们再深入剖析一下它的结构。
不带参数装饰器的标准结构
```python
def decorator(func): # 第1层:接收被装饰的函数
def wrapper(*args, **kwargs): # 第2层:包装函数,接收原函数的参数
# 执行前的逻辑(如日志、计时、权限校验等)
print('函数执行前')
# 调用原函数,保存返回值
result = func(*args, **kwargs)
# 执行后的逻辑(如日志、统计等)
print('函数执行后')
# 返回原函数的返回值
return result
# 返回包装函数
return wrapper
```
关键要点解析
· *args 和 **kwargs:这是Python的可变参数语法。*args接收任意数量的位置参数,**kwargs接收任意数量的关键字参数。这样装饰器就能适配任何函数的参数列表。
· 必须调用原函数:wrapper函数内部必须调用func(*args, **kwargs),否则原函数不会执行。
· 必须返回result:如果原函数有返回值,wrapper必须返回这个结果,否则调用者拿不到返回值。
· 必须返回wrapper:外层函数decorator必须返回wrapper函数对象(不带括号),而不是调用wrapper()。
有时候我们需要给装饰器本身传递参数。比如一个日志装饰器,我们想指定日志级别(info/warn/error)。这时候就需要带参数的装饰器。
带参数装饰器的标准结构
```python
def decorator_with_args(arg1, arg2): # 第1层:接收装饰器参数
def decorator(func): # 第2层:接收被装饰的函数
def wrapper(*args, **kwargs): # 第3层:包装函数
# 可以使用arg1, arg2这些参数
print(f'装饰器参数: {arg1}, {arg2}')
result = func(*args, **kwargs)
return result
return wrapper
return decorator
```
实际应用:日志级别装饰器
```python
def logger(level='info'):
def decorator(func):
def wrapper(*args, **kwargs):
if level == 'info':
print(f'[INFO] {func.__name__}开始执行')
elif level == 'warn':
print(f'[WARN] {func.__name__}需要注意')
elif level == 'error':
print(f'[ERROR] {func.__name__}可能出错')
result = func(*args, **kwargs)
if level == 'info':
print(f'[INFO] {func.__name__}执行完成')
return result
return wrapper
return decorator
# 使用方式:注意括号不能少
@logger(level='warn')
def risky_operation():
print('执行危险操作')
risky_operation()
# 输出: [WARN] risky_operation需要注意
# 执行危险操作
```
带参数vs不带参数的区别:不带参数的装饰器直接写@decorator,带参数的装饰器必须写@decorator(参数)。带参数的装饰器比不带参数的多一层函数嵌套——最外层接收装饰器参数,中间层接收被装饰函数,最内层是包装函数。
SECTION 06
functools.wraps:最容易踩的坑
这是装饰器最容易踩的坑,没有之一。让我先展示一下问题:
问题:函数元信息丢失
```python
def timer(func):
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f'{func.__name__}耗时: {end_time-start_time:.2f}秒')
return result
return wrapper
@timer
def login(username, password):
"""用户登录函数"""
time.sleep(0.5)
return True
# 查看函数元信息
print(login.__name__) # 输出: wrapper(期望是login)
print(login.__doc__) # 输出: None(期望是"用户登录函数")
```
看到了吗?被装饰后的函数,它的__name__变成了wrapper,__doc__变成了None。这是因为装饰器返回的是wrapper函数,所以原来函数的元信息都丢失了。
解决方案:functools.wraps
```python
import time
from functools import wraps # 必须导入wraps
def timer(func):
@wraps(func) # 在wrapper上应用wraps装饰器
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f'{func.__name__}耗时: {end_time-start_time:.2f}秒')
return result
return wrapper
@timer
def login(username, password):
"""用户登录函数"""
time.sleep(0.5)
return True
# 现在元信息正确了
print(login.__name__) # 输出: login
print(login.__doc__) # 输出: 用户登录函数
```
强制规则:写装饰器时,必须在wrapper函数上使用@functools.wraps(func)。这是Python开发者的共识,也是面试官最爱问的问题之一。
除了用函数实现装饰器,还可以用类来实现。类装饰器通过__call__方法实现可调用对象。
类装饰器的实现方式
```python
import time
from functools import wraps
class Timer:
def __init__(self, func):
self.func = func
wraps(func)(self) # 同样需要保留元信息
def __call__(self, *args, **kwargs):
start_time = time.time()
result = self.func(*args, **kwargs)
end_time = time.time()
print(f'{self.func.__name__}耗时: {end_time-start_time:.2f}秒')
return result
@Timer
def login(username, password):
"""用户登录函数"""
time.sleep(0.5)
return True
login('test', '123456')
# 输出: login耗时: 0.50秒
```
带参数的类装饰器
```python
class Logger:
def __init__(self, level='info'):
self.level = level # 保存装饰器参数
def __call__(self, func): # 接收被装饰的函数
@wraps(func)
def wrapper(*args, **kwargs):
if self.level == 'info':
print(f'[INFO] {func.__name__}开始执行')
elif self.level == 'error':
print(f'[ERROR] {func.__name__}可能出错')
result = func(*args, **kwargs)
return result
return wrapper
@Logger(level='error')
def risky_operation():
print('执行危险操作')
risky_operation()
# 输出: [ERROR] risky_operation可能出错
# 执行危险操作
```
日志记录是装饰器最常见的应用场景之一。在接口测试中,我们经常需要记录每个请求的详细信息。
```python
import logging
from functools import wraps
# 配置logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
def log_request(func):
@wraps(func)
def wrapper(*args, **kwargs):
# 记录请求信息
logger.info(f'开始调用 {func.__name__}')
logger.info(f'参数: args={args}, kwargs={kwargs}')
try:
result = func(*args, **kwargs)
logger.info(f'{func.__name__} 调用成功,返回值: {result}')
return result
except Exception as e:
logger.error(f'{func.__name__} 调用失败: {str(e)}')
raise # 重新抛出异常,不影响原有逻辑
return wrapper
@log_request
def api_login(username, password):
"""登录接口"""
if username == 'admin' and password == '123456':
return {'code': 200, 'token': 'abc123'}
else:
raise ValueError('用户名或密码错误')
# 调用测试
api_login('admin', '123456')
# 输出日志:
# INFO:__main__:开始调用 api_login
# INFO:__main__:参数: args=(), kwargs={'username': 'admin', 'password': '123456'}
# INFO:__main__:api_login 调用成功,返回值: {'code': 200, 'token': 'abc123'}
```
SECTION 09
实战2:重试装饰器(测试必备)
在接口测试中,网络请求偶尔会失败,这时候我们需要重试机制。一个好的重试装饰器可以大大简化测试代码。
```python
import time
from functools import wraps
def retry(max_retries=3, delay=1, backoff=2):
"""
重试装饰器
:param max_retries: 最大重试次数
:param delay: 初始延迟(秒)
:param backoff: 退避因子(每次重试延迟乘以backoff)
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
current_delay = delay
last_exception = None
for attempt in range(1, max_retries + 1):
try:
return func(*args, **kwargs)
except Exception as e:
last_exception = e
print(f'{func.__name__} 第{attempt}次调用失败: {e}')
if attempt < max_retries:
print(f'等待 {current_delay} 秒后重试...')
time.sleep(current_delay)
current_delay *= backoff # 指数退避
# 所有重试都失败了
print(f'{func.__name__} 重试{max_retries}次后仍然失败')
raise last_exception
return wrapper
return decorator
@retry(max_retries=3, delay=1, backoff=2)
def flaky_api():"""不稳定的API,模拟网络波动"""
import random
if random.random() < 0.7: # 70%概率失败
raise ConnectionError('网络超时')
return {'code': 200, 'data': 'success'}
# 调用测试
result = flaky_api()
print(result) # 最终会成功返回
```
指数退避策略:重试装饰器使用指数退避(backoff=2),即每次重试的等待时间是上一次的2倍。这样可以避免在服务繁忙时过度请求,给服务恢复的时间。这是生产环境中重试机制的最佳实践。
在测试平台或后台系统中,我们经常需要对接口进行权限校验。装饰器是实现权限控制的优雅方式。
```python
from functools import wraps
def require_permission(required_role):
"""权限校验装饰器"""
def decorator(func):
@wraps(func)
def wrapper(user, *args, **kwargs):
# 检查用户角色
if user.get('role') != required_role:
raise PermissionError(f'用户{user["name"]}没有{required_role}权限')
print(f'用户{user["name"]}通过权限校验')
return func(user, *args, **kwargs)
return wrapper
return decorator
@require_permission('admin')
def delete_user(user, user_id):
"""删除用户(需要管理员权限)"""
print(f'管理员{user["name"]}删除用户: {user_id}')
return True
@require_permission('user')
def view_profile(user):
"""查看个人资料(普通用户即可)"""
print(f'用户{user["name"]}查看个人资料')
return {'name': user['name'], 'email': user['email']}
# 测试
admin_user = {'name': 'admin', 'role': 'admin', 'email': 'admin@test.com'}
normal_user = {'name': 'user', 'role': 'user', 'email': 'user@test.com'}
delete_user(admin_user, 123) # 成功
# view_profile(normal_user) # 成功
# delete_user(normal_user, 123) # 抛出PermissionError
```
SECTION 11
内置装饰器盘点:@property/@staticmethod/@classmethod
Python内置了几个常用的装饰器,它们非常实用,也是面试的高频考点。
@property:把方法变成属性
```python
class User:
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
@property
def full_name(self):
"""计算全名"""
return f'{self.first_name} {self.last_name}'
user = User('张', '三')
print(user.full_name) # 输出: 张 三(注意没有括号!)
# user.full_name = '李四' # 会报错,因为@property默认只读
```
@staticmethod和@classmethod:静态方法和类方法
```python
class Calculator:
pi = 3.14159
@staticmethod
def add(a, b):
"""静态方法:不需要self或cls参数"""
return a + b
@classmethod
def circle_area(cls, radius):
"""类方法:第一个参数是cls,表示类本身"""
return cls.pi * radius ** 2
# 调用方式
print(Calculator.add(2, 3)) # 输出: 5
print(Calculator.circle_area(5)) # 输出: 78.53975
```
· @staticmethod:不需要实例,也不需要类。它只是一个挂在类上的普通函数。
· @classmethod:第一个参数是cls,可以访问和修改类属性。常用于创建工厂方法。
· @property:把方法变成属性访问,不需要括号调用。可以配合@property.setter实现读写控制。
· Q1:装饰器的本质是什么?
装饰器是一个接收函数作为参数并返回新函数的函数。@符号是语法糖,等价于func = decorator(func)。
· Q2:为什么要用functools.wraps?
因为装饰器返回的是wrapper函数,会导致原函数的__name__、__doc__等元信息丢失。@wraps(func)会把原函数的元信息复制到wrapper上。
· Q3:带参数和不带参数的装饰器有什么区别?
不带参数的装饰器直接接收被装饰函数;带参数的装饰器多一层嵌套,最外层接收装饰器参数,中间层接收被装饰函数。
· Q4:装饰器可以装饰类吗?
可以。类装饰器通过__call__方法实现。装饰类时,装饰器接收类作为参数,返回一个新类或修改原类。
· Q5:@staticmethod和@classmethod的区别?
staticmethod不需要self或cls参数,是挂在类上的普通函数;classmethod第一个参数是cls,可以访问类属性,常用于工厂方法。
· Q6:怎么实现一个带参数的装饰器?
需要三层嵌套:最外层接收装饰器参数,中间层接收被装饰函数,最内层是包装函数。或者用类装饰器,__init__接收装饰器参数,__call__接收被装饰函数。
最后
装饰器是Python进阶的核心知识点,也是区分小白和大佬的关键标志。同样的功能,小白写重复代码,大佬用装饰器实现优雅复用。掌握装饰器,不仅能让你的代码更简洁,还能在面试中加分。希望今天的内容能帮你彻底搞懂装饰器,以后遇到需要横切关注点的场景,第一时间想到用装饰器来解决。
关于Python进阶、接口测试、AI测试转型等问题,都可以通过公众号菜单栏添加我微信 testafa 私信交流。需要学习资料也可以私信!