【基础篇】Python装饰器的妙用.md

2022/6/13 Python小技巧

大家好~我是米洛

我在从0到1打造一个开源平台, 也在编写一套完整的接口测试平台系列教程,希望大家能够多多支持。

欢迎关注我的公众号米洛的测开日记,获取最新文章教程!

本文讲述装饰器的一些运用场景,有兴趣的同学可以了解一下。

在此之前,我们先对一下上期作业的答案吧,已经做对的同学可以往下拉:

要求实现一个自定义的装饰器

这题的核心内容就是callable,我们可以这么写:

def run(times):
    if callable(times):
        def wrapper(*args, **kwargs):
            for i in range(5):
                times(*args, **kwargs)
                print(f"运行的第{i}次")

        return wrapper
    else:
        def decorator(func):
            def wrapper(*args, **kwargs):
                for i in range(times):
                    func(*args, **kwargs)
                    print(f"运行的第{i}次")

            return wrapper

        return decorator
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

这里用到了一个核心方法: callable

callable的意思是,这个变量是不是可被调用的,如果我们的run接受的是一个方法,那么它就会走if里面的逻辑,反之则会再包一层decorator。

# 怎么区分呢

@run
def wqrf():
    print("haha")
1
2
3

如果是这样,run后面没有接任何参数,甚至是括号,单纯的就是@run,那么我们看看times变量会是什么呢?

大家可以先猜想一下:

执行一下代码,可以看到这个times是function: wqrf

所以我们按照预先说的,如果run装饰器后面没有接参数,则调用5次。(这里有一些改动,因为如果我不循环调用的话,那加这个装饰器毫无意义,所以我这边改成了: 不带参数则调用5次)

那再看看带参数的时候,times是什么内容:

可以看到,这个times变成了23,也就是我们给装饰器传入的循环次数

而23自然是不会触发callable条件的。

搞懂什么时候if callable成立,那这个问题就好解决了。

如果它成立,那么它可以简写为:

def run(times):
    def wrapper(*args, **kwargs):
        for i in range(5):
            times(*args, **kwargs)
            print(f"运行的第{i}次")

    return wrapper
1
2
3
4
5
6
7

如果不成立,则是另一个装饰器:

def run(times):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for i in range(times):
                func(*args, **kwargs)
                print(f"运行的第{i}次")

        return wrapper

    return decorator
1
2
3
4
5
6
7
8
9
10

所以,看似复杂,实际上是根据接收的参数是否可调用,从而选择变成什么样的装饰器,这就完成了我们的作业了~

如果有疑惑的话,可以回忆一下上一篇装饰器的写法。文末会附上完整代码。

# 回到主题

我们说了那么多,该讲下装饰器的运用了。以下都是需求+例子,大家且看且珍惜~

  • 计时器

    当我们需要统计一些方法的执行时间,我们可以这么写。

def time_dec(func):
    def wrapper(*arg):
        t = time.perf_counter()
        res = func(*arg)
        print(func.__name__, time.perf_counter() - t)
        return res
    return wrapper
1
2
3
4
5
6
7

但你会发现装饰器的名字打印出来的时候,func.name 变成了wrapper,很正常,因为我们确实定义了个wrapper

只需要在装饰器def wrapper上面加上一个装饰器:

@functools.wraps(func)

  • 异常捕获器(来自测试开发笋货的思路)

    写try和except写厌烦了吧?你可以这么玩:

def exceptions(func):
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except Exception as e:
            raise Exception(e)

    return wrapper
1
2
3
4
5
6
7
8
  • web开发中判断用户是否登录
def login_required(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            try:
                headers = request.headers
                token = headers.get('token')
                if token is None:
                    return jsonify(dict(code=401, msg="用户信息认证失败, 请检查"))
                # 解析用户信息
                user_info = UserToken.parse_token(token)
                # 这里把user信息写入kwargs
                kwargs["user_info"] = user_info
            except Exception as e:
                return jsonify(dict(code=401, msg=str(e)))
            return func(*args, **kwargs)

        return wrapper
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  • 数据驱动
@parameters(
   (2, 4, 6),
   (5, 6, 11),
)
def test_add(a, b, expected):
    assert a + b == expected
1
2
3
4
5
6

类似于ddt库提供的数据驱动方法,用于测试用例。

  • 日志记录

  • 用例重跑

  • UI用例执行异常自动截图

    这个与笋货类似,捕获异常以后进行额外的处理。

  • 给变量加锁

import functools

def synchronized(lock):
    """ Synchronization decorator """
    def wrap(f):
        @functools.wraps(f)
        def newFunction(*args, **kw):
            with lock:
                return f(*args, **kw)
        return newFunction
    return wrap
1
2
3
4
5
6
7
8
9
10
11

今天的内容就简单介绍到这里了,收尾明显有点潦草,因为有个同事催我下班了,哈哈~

下次再会···