|
| 1 | +# 7.10 让你的装饰器写得更轻松的神库 |
| 2 | + |
| 3 | + |
| 4 | + |
| 5 | +本篇文章会为你介绍的是一个已经存在十三年,但是依旧不红的库 decorator,好像很少有人知道他的存在一样。 |
| 6 | + |
| 7 | +这个库可以帮你做什么呢 ? |
| 8 | + |
| 9 | +其实很简单,就是可以帮你更方便地写python装饰器代码,更重要的是,它让 Python 中被装饰器装饰后的方法长得更像装饰前的方法。 |
| 10 | + |
| 11 | +本篇文章不会过多的向你介绍装饰器的基本知识,我会默认你知道什么是装饰器,并且懂得如何写一个简单的装饰器。 |
| 12 | + |
| 13 | +不了解装饰器的可以先去阅读我之前写的文章,非常全且详细的介绍了装饰器的各种实现方法。 |
| 14 | + |
| 15 | +## 1. 常规的装饰器 |
| 16 | + |
| 17 | +下面这是一个最简单的装饰器示例,在运行 `myfunc` 函数的前后都会打印一条日志。 |
| 18 | + |
| 19 | +```python |
| 20 | +def deco(func): |
| 21 | + def wrapper(*args, **kw): |
| 22 | + print("Ready to run task") |
| 23 | + func(*args, **kw) |
| 24 | + print("Successful to run task") |
| 25 | + return wrapper |
| 26 | + |
| 27 | +@deco |
| 28 | +def myfunc(): |
| 29 | + print("Running the task") |
| 30 | + |
| 31 | +myfunc() |
| 32 | +``` |
| 33 | + |
| 34 | +装饰器使用起来,似乎有些高端和魔幻,对于一些重复性的功能,往往我们会封装成一个装饰器函数。 |
| 35 | + |
| 36 | +在定义一个装饰器的时候,我们都需要像上面一样机械性的写一个嵌套的函数,对装饰器原理理解不深的初学者,往往过段时间就会忘记如何定义装饰器。 |
| 37 | + |
| 38 | +有一些比较聪明的同学,会利用 PyCharm 来自动生成装饰器模板 |
| 39 | + |
| 40 | + |
| 41 | + |
| 42 | +然后要使用的时候,直接敲入 `deco` 就会生成一个简单的生成器代码,提高编码的准备效率 |
| 43 | + |
| 44 | + |
| 45 | + |
| 46 | +## 2. 使用神库 |
| 47 | + |
| 48 | +使用 PyCharm 的 Live Template ,虽然能降低编写装饰器的难度,但却要依赖 PyCharm 这一专业的代码编辑器。 |
| 49 | + |
| 50 | +这里,明哥要教你一个更加简单的方法,使用这个方法呢,你需要先安装一个库 : `decorator`,使用 pip 可以很轻易地去安装它 |
| 51 | + |
| 52 | +```shell |
| 53 | +$ python3 -m pip install decorator |
| 54 | +``` |
| 55 | + |
| 56 | +从库的名称不难看出,这是一个专门用来解决装饰器问题的第三方库。 |
| 57 | + |
| 58 | +有了它之后,你会惊奇的发现,以后自己定义的装饰器,就再也不需要写嵌套的函数了 |
| 59 | + |
| 60 | +```python |
| 61 | +from decorator import decorator |
| 62 | + |
| 63 | +@decorator |
| 64 | +def deco(func, *args, **kw): |
| 65 | + print("Ready to run task") |
| 66 | + func(*args, **kw) |
| 67 | + print("Successful to run task") |
| 68 | + |
| 69 | +@deco |
| 70 | +def myfunc(): |
| 71 | + print("Running the task") |
| 72 | + |
| 73 | +myfunc() |
| 74 | +``` |
| 75 | + |
| 76 | +deco 作为装饰函数,第一个参数是固定的,都是指被装饰函数,而后面的参数都固定使用 可变参数 `*args` 和 `**kw` 的写法,代码被装饰函数的原参数。 |
| 77 | + |
| 78 | +这种写法,不得不说,更加符合直觉,代码的逻辑也更容易理解。 |
| 79 | + |
| 80 | +## 3. 带参数的装饰器可用? |
| 81 | + |
| 82 | +装饰器根据有没有携带参数,可以分为两种 |
| 83 | + |
| 84 | +**第一种**:不带参数,最简单的示例,上面已经举例 |
| 85 | + |
| 86 | +```python |
| 87 | +def decorator(func): |
| 88 | + def wrapper(*args, **kw): |
| 89 | + func(*args, **kw) |
| 90 | + return wrapper |
| 91 | +``` |
| 92 | + |
| 93 | +**第二种**:带参数,这就相对复杂了,理解起来了也不是那么容易。 |
| 94 | + |
| 95 | +```python |
| 96 | +def decorator(arg1, arg2): |
| 97 | + def wrapper(func): |
| 98 | + def deco(*args, **kwargs) |
| 99 | + func(*args, **kwargs) |
| 100 | + return deco |
| 101 | + return wrapper |
| 102 | +``` |
| 103 | + |
| 104 | +那么对于需要带参数的装饰器,`decorator` 是否也一样能很好的支持呢? |
| 105 | + |
| 106 | +下面是一个官方的示例 |
| 107 | + |
| 108 | +```python |
| 109 | +from decorator import decorator |
| 110 | + |
| 111 | +@decorator |
| 112 | +def warn_slow(func, timelimit=60, *args, **kw): |
| 113 | + t0 = time.time() |
| 114 | + result = func(*args, **kw) |
| 115 | + dt = time.time() - t0 |
| 116 | + if dt > timelimit: |
| 117 | + logging.warn('%s took %d seconds', func.__name__, dt) |
| 118 | + else: |
| 119 | + logging.info('%s took %d seconds', func.__name__, dt) |
| 120 | + return result |
| 121 | + |
| 122 | +@warn_slow(timelimit=600) # warn if it takes more than 10 minutes |
| 123 | +def run_calculation(tempdir, outdir): |
| 124 | + pass |
| 125 | +``` |
| 126 | + |
| 127 | +可以看到 |
| 128 | + |
| 129 | +- 装饰函数的第一个参数,还是被装饰器 func ,这个跟之前一样 |
| 130 | +- 而第二个参数 timelimit 写成了位置参数的写法,并且有默认值 |
| 131 | +- 再往后,就还是跟原来一样使用了可变参数的写法 |
| 132 | + |
| 133 | +不难推断,只要你在装饰函数中第二个参数开始,使用了非可变参数的写法,这些参数就可以做为装饰器调用时的参数。 |
| 134 | + |
| 135 | +## 4. 签名问题有解决? |
| 136 | + |
| 137 | +我们在自己写装饰器的时候,通常都会顺手加上一个叫 `functools.wraps` 的装饰器,我想你应该也经常见过,那他有啥用呢? |
| 138 | + |
| 139 | +先来看一个例子 |
| 140 | + |
| 141 | +```python |
| 142 | +def wrapper(func): |
| 143 | + def inner_function(): |
| 144 | + pass |
| 145 | + return inner_function |
| 146 | + |
| 147 | +@wrapper |
| 148 | +def wrapped(): |
| 149 | + pass |
| 150 | + |
| 151 | +print(wrapped.__name__) |
| 152 | +#inner_function |
| 153 | +``` |
| 154 | + |
| 155 | +为什么会这样子?不是应该返回 `func ` 吗? |
| 156 | + |
| 157 | +这也不难理解,因为上边执行`func` 和下边 `decorator(func)` 是等价的,所以上面 `func.__name__` 是等价于下面`decorator(func).__name__` 的,那当然名字是 `inner_function` |
| 158 | + |
| 159 | +```python |
| 160 | +def wrapper(func): |
| 161 | + def inner_function(): |
| 162 | + pass |
| 163 | + return inner_function |
| 164 | + |
| 165 | +def wrapped(): |
| 166 | + pass |
| 167 | + |
| 168 | +print(wrapper(wrapped).__name__) |
| 169 | +#inner_function |
| 170 | +``` |
| 171 | + |
| 172 | +目前,我们可以看到当一个函数被装饰器装饰过后,它的签名信息会发生变化(譬如上面看到的函数名) |
| 173 | + |
| 174 | +那如何避免这种情况的产生? |
| 175 | + |
| 176 | +**解决方案就是使用我们前面所说的 functools .wraps 装饰器。** |
| 177 | + |
| 178 | +它的作用就是将 被修饰的函数(wrapped) 的一些属性值赋值给 修饰器函数(wrapper) ,最终让属性的显示更符合我们的直觉。 |
| 179 | + |
| 180 | +```python |
| 181 | +from functools import wraps |
| 182 | + |
| 183 | +def wrapper(func): |
| 184 | + @wraps(func) |
| 185 | + def inner_function(): |
| 186 | + pass |
| 187 | + return inner_function |
| 188 | + |
| 189 | +@wrapper |
| 190 | +def wrapped(): |
| 191 | + pass |
| 192 | + |
| 193 | +print(wrapped.__name__) |
| 194 | +# wrapped |
| 195 | +``` |
| 196 | + |
| 197 | +那么问题就来了,我们使用了 decorator 之后,是否还会存在这种签名的问题呢? |
| 198 | + |
| 199 | +写个例子来验证一下就知道啦 |
| 200 | + |
| 201 | +```python |
| 202 | +from decorator import decorator |
| 203 | + |
| 204 | +@decorator |
| 205 | +def deco(func, *args, **kw): |
| 206 | + print("Ready to run task") |
| 207 | + func(*args, **kw) |
| 208 | + print("Successful to run task") |
| 209 | + |
| 210 | +@deco |
| 211 | +def myfunc(): |
| 212 | + print("Running the task") |
| 213 | + |
| 214 | +print(myfunc.__name__) |
| 215 | +``` |
| 216 | + |
| 217 | +输出的结果是 `myfunc`,说明 `decorator` 已经默认帮我们处理了一切可预见的问题。 |
| 218 | + |
| 219 | +## 5. 总结一下 |
| 220 | + |
| 221 | +`decorator` 是一个提高装饰器编码效率的第三方库,它适用于对装饰器原理感到困惑的新手,可以让你很轻易的写出更符合人类直觉的代码。对于带参数装饰器的定义,是非常复杂的,它需要要写多层的嵌套函数,并且需要你熟悉各个参数的传递路径,才能保证你写出来的装饰器可以正常使用。这时候,只要用上 `decorator` 这个库,你就可以很轻松的写出一个带参数的装饰器。同时你也不用担心他会出现签名问题,这些它都为你妥善的处理好了。 |
| 222 | + |
| 223 | +这么棒的一个库,推荐你使用起来。 |
| 224 | + |
| 225 | + |
0 commit comments