|
1 | 1 | # 装饰器 |
2 | 2 |
|
| 3 | +我们知道,在 Python 中,我们可以像使用变量一样使用函数: |
| 4 | + |
| 5 | +- 函数可以被赋值给其他变量 |
| 6 | +- 函数可以被删除 |
| 7 | +- 可以在函数里面再定义函数 |
| 8 | +- 函数可以作为参数传递给另外一个函数 |
| 9 | +- 函数可以作为另一个函数的返回 |
| 10 | + |
| 11 | +**简而言之,函数就是一个对象**。 |
| 12 | + |
| 13 | +# 对一个简单的函数进行装饰 |
| 14 | + |
| 15 | +为了更好地理解装饰器,我们先从一个简单的例子开始,假设有下面的函数: |
| 16 | + |
| 17 | +```python |
| 18 | +def hello(): |
| 19 | + return 'hello world' |
| 20 | +``` |
| 21 | + |
| 22 | +现在我们想增强 `hello()` 函数的功能,希望给返回加上 HTML 标签,比如 `<i>hello world</i>`,但是有一个要求,不改变原来 `hello()` 函数的定义。这里当然有很多种方法,下面给出一种跟本文相关的方法: |
| 23 | + |
| 24 | +```python |
| 25 | +def makeitalic(func): |
| 26 | + def wrapped(): |
| 27 | + return "<i>" + func() + "</i>" |
| 28 | + return wrapped |
| 29 | +``` |
| 30 | + |
| 31 | +在上面的代码中,我们定义了一个函数 `makeitalic`,该函数有一个参数 `func`,它是一个函数;在 `makeitalic` 函数里面我们又定义了一个内部函数 `wrapped`,并将该函数作为返回。 |
| 32 | + |
| 33 | +现在,我们就可以不改变 `hello()` 函数的定义,给返回加上 HTML 标签了: |
| 34 | + |
| 35 | +```python |
| 36 | +>>> hello = makeitalic(hello) # 将 hello 函数传给 makeitalic |
| 37 | +>>> hello() |
| 38 | +'<i>hello world</i>' |
| 39 | +``` |
| 40 | + |
| 41 | +在上面,我们将 `hello` 函数传给 `makeitalic`,再将返回赋给 `hello`,此时调用 `hello()` 就得到了我们想要的结果。 |
| 42 | + |
| 43 | +不过要注意的是,由于我们将 `makeitalic` 的返回赋给了 `hello`,此时 `hello()` 函数仍然存在,但是它的函数名不再是 hello 了,而是 wrapped,正是 `makeitalic` 返回函数的名称,可以验证一下: |
| 44 | + |
| 45 | +```python |
| 46 | +>>> hello.__name__ |
| 47 | +'wrapped' |
| 48 | +``` |
| 49 | + |
| 50 | +对于这个小瑕疵,后文将会给出解决方法。 |
| 51 | + |
| 52 | +现在,我们梳理一下上面的例子,为了增强原函数 `hello` 的功能,我们定义了一个函数,它接收原函数作为参数,并返回一个新的函数,完整的代码如下: |
| 53 | + |
| 54 | +```python |
| 55 | +def makeitalic(func): |
| 56 | + def wrapped(): |
| 57 | + return "<i>" + func() + "</i>" |
| 58 | + return wrapped |
| 59 | + |
| 60 | +def hello(): |
| 61 | + return 'hello world' |
| 62 | + |
| 63 | +hello = makeitalic(hello) |
| 64 | +``` |
| 65 | + |
| 66 | +事实上,`makeitalic` 就是一个**装饰器(decorator)**,它『装饰』了函数 `hello`,并返回一个函数,将其赋给 `hello`。 |
| 67 | + |
| 68 | +一般情况下,我们使用装饰器提供的 `@` 语法糖(Syntactic Sugar),来简化上面的写法: |
| 69 | + |
| 70 | +```python |
| 71 | +def makeitalic(func): |
| 72 | + def wrapped(): |
| 73 | + return "<i>" + func() + "</i>" |
| 74 | + return wrapped |
| 75 | + |
| 76 | +@makeitalic |
| 77 | +def hello(): |
| 78 | + return 'hello world' |
| 79 | +``` |
| 80 | + |
| 81 | +像上面的情况,可以动态修改函数(或类)功能的函数就是装饰器。**本质上,它是一个高阶函数,以被装饰的函数(比如上面的 hello)为参数,并返回一个包装后的函数(比如上面的 wrapped)给被装饰函数(hello)**。 |
| 82 | + |
| 83 | +# 装饰器的使用形式 |
| 84 | + |
| 85 | +- 装饰器的一般使用形式如下: |
| 86 | + |
| 87 | +```python |
| 88 | +@decorator |
| 89 | +def func(): |
| 90 | + pass |
| 91 | +``` |
| 92 | + |
| 93 | +等价于下面的形式: |
| 94 | + |
| 95 | +```python |
| 96 | +def func(): |
| 97 | + pass |
| 98 | +func = decorator(func) |
| 99 | +``` |
| 100 | + |
| 101 | +- 装饰器可以定义多个,离函数定义最近的装饰器先被调用,比如: |
| 102 | + |
| 103 | +```python |
| 104 | +@decorator_one |
| 105 | +@decorator_two |
| 106 | +def func(): |
| 107 | + pass |
| 108 | +``` |
| 109 | + |
| 110 | +等价于: |
| 111 | + |
| 112 | +```python |
| 113 | +def func(): |
| 114 | + pass |
| 115 | + |
| 116 | +func = decorator_one(decorator_two(func)) |
| 117 | +``` |
| 118 | + |
| 119 | +- 装饰器还可以带参数,比如: |
| 120 | + |
| 121 | +```python |
| 122 | +@decorator(arg1, arg2) |
| 123 | +def func(): |
| 124 | + pass |
| 125 | +``` |
| 126 | + |
| 127 | +等价于: |
| 128 | + |
| 129 | +```python |
| 130 | +def func(): |
| 131 | + pass |
| 132 | + |
| 133 | +func = decorator(arg1, arg2)(func) |
| 134 | +``` |
| 135 | + |
| 136 | +下面我们再看一些具体的例子,以加深对它的理解。 |
| 137 | + |
| 138 | +# 对带参数的函数进行装饰 |
| 139 | + |
| 140 | +前面的例子中,被装饰的函数 `hello()` 是没有带参数的,我们看看被装饰函数带参数的情况。对前面例子中的 `hello()` 函数进行改写,使其带参数,如下: |
| 141 | + |
| 142 | +```python |
| 143 | +def makeitalic(func): |
| 144 | + def wrapped(*args, **kwargs): |
| 145 | + ret = func(*args, **kwargs) |
| 146 | + return '<i>' + ret + '</i>' |
| 147 | + return wrapped |
| 148 | + |
| 149 | +@makeitalic |
| 150 | +def hello(name): |
| 151 | + return 'hello %s' % name |
| 152 | + |
| 153 | +@makeitalic |
| 154 | +def hello2(name1, name2): |
| 155 | + return 'hello %s, %s' % (name1, name2) |
| 156 | +``` |
| 157 | + |
| 158 | +由于函数 `hello` 带参数,因此内嵌包装函数 `wrapped` 也做了一点改变: |
| 159 | + |
| 160 | +- 内嵌包装函数的参数传给了 `func`,即被装饰函数,也就是说内嵌包装函数的参数跟被装饰函数的参数对应,这里使用了 `(*args, **kwargs)`,是为了适应可变参数。 |
| 161 | + |
| 162 | +看看使用: |
| 163 | + |
| 164 | +```python |
| 165 | +>>> hello('python') |
| 166 | +'<i>hello python</i>' |
| 167 | +>>> hello2('python', 'java') |
| 168 | +'<i>hello python, java</i>' |
| 169 | +``` |
| 170 | + |
| 171 | +# 带参数的装饰器 |
| 172 | + |
| 173 | +上面的例子,我们增强了函数 `hello` 的功能,给它的返回加上了标签 `<i>...</i>`,现在,我们想改用标签 `<b>...</b>` 或 `<p>...</p>`。是不是要像前面一样,再定义一个类似 `makeitalic` 的装饰器呢?其实,我们可以定义一个函数,将标签作为参数,返回一个装饰器,比如: |
| 174 | + |
| 175 | +```python |
| 176 | +def wrap_in_tag(tag): |
| 177 | + def decorator(func): |
| 178 | + def wrapped(*args, **kwargs): |
| 179 | + ret = func(*args, **kwargs) |
| 180 | + return '<' + tag + '>' + ret + '</' + tag + '>' |
| 181 | + return wrapped |
| 182 | + |
| 183 | + return decorator |
| 184 | +``` |
| 185 | + |
| 186 | +现在,我们可以根据需要生成想要的装饰器了: |
| 187 | + |
| 188 | +```python |
| 189 | +makebold = wrap_in_tag('b') # 根据 'b' 返回 makebold 生成器 |
| 190 | + |
| 191 | +@makebold |
| 192 | +def hello(name): |
| 193 | + return 'hello %s' % name |
| 194 | + |
| 195 | +>>> hello('world') |
| 196 | +'<b>hello world</b>' |
| 197 | +``` |
| 198 | + |
| 199 | +上面的形式也可以写得更加简洁: |
| 200 | + |
| 201 | +```python |
| 202 | +@wrap_in_tag('b') |
| 203 | +def hello(name): |
| 204 | + return 'hello %s' % name |
| 205 | +``` |
| 206 | + |
| 207 | +这就是带参数的装饰器,其实就是在装饰器外面多了一层包装,根据不同的参数返回不同的装饰器。 |
| 208 | + |
| 209 | +# 多个装饰器 |
| 210 | + |
| 211 | +现在,让我们来看看多个装饰器的例子,为了简单起见,下面的例子就不使用带参数的装饰器。 |
| 212 | + |
| 213 | +```python |
| 214 | +def makebold(func): |
| 215 | + def wrapped(): |
| 216 | + return '<b>' + func() + '</b>' |
| 217 | + |
| 218 | + return wrapped |
| 219 | + |
| 220 | +def makeitalic(func): |
| 221 | + def wrapped(): |
| 222 | + return '<i>' + func() + '</i>' |
| 223 | + |
| 224 | + return wrapped |
| 225 | + |
| 226 | +@makebold |
| 227 | +@makeitalic |
| 228 | +def hello(): |
| 229 | + return 'hello world' |
| 230 | +``` |
| 231 | + |
| 232 | +上面定义了两个装饰器,对 `hello` 进行装饰,上面的最后几行代码相当于: |
| 233 | + |
| 234 | +```python |
| 235 | +def hello(): |
| 236 | + return 'hello world' |
| 237 | + |
| 238 | +hello = makebold(makeitalic(hello)) |
| 239 | +``` |
| 240 | + |
| 241 | +调用函数 `hello`: |
| 242 | + |
| 243 | +```python |
| 244 | +>>> hello() |
| 245 | +'<b><i>hello world</i></b>' |
| 246 | +``` |
| 247 | + |
| 248 | +# 基于类的装饰器 |
| 249 | + |
| 250 | +前面的装饰器都是一个函数,其实也可以基于类定义装饰器,看下面的例子: |
| 251 | + |
| 252 | +```python |
| 253 | +class Bold(object): |
| 254 | + def __init__(self, func): |
| 255 | + self.func = func |
| 256 | + |
| 257 | + def __call__(self, *args, **kwargs): |
| 258 | + return '<b>' + self.func(*args, **kwargs) + '</b>' |
| 259 | + |
| 260 | +@Bold |
| 261 | +def hello(name): |
| 262 | + return 'hello %s' % name |
| 263 | + |
| 264 | +>>> hello('world') |
| 265 | +'<b>hello world</b>' |
| 266 | +``` |
| 267 | + |
| 268 | +可以看到,类 `Bold` 有两个方法: |
| 269 | + |
| 270 | +- `__init__()`:它接收一个函数作为参数,也就是被装饰的函数 |
| 271 | +- `__call__()`:让类对象可调用,就像函数调用一样,在调用被装饰函数时被调用 |
| 272 | + |
| 273 | +还可以让类装饰器带参数: |
| 274 | + |
| 275 | +```python |
| 276 | +class Tag(object): |
| 277 | + def __init__(self, tag): |
| 278 | + self.tag = tag |
| 279 | + |
| 280 | + def __call__(self, func): |
| 281 | + def wrapped(*args, **kwargs): |
| 282 | + return "<{tag}>{res}</{tag}>".format( |
| 283 | + res=func(*args, **kwargs), tag=self.tag |
| 284 | + ) |
| 285 | + return wrapped |
| 286 | + |
| 287 | +@Tag('b') |
| 288 | +def hello(name): |
| 289 | + return 'hello %s' % name |
| 290 | +``` |
| 291 | + |
| 292 | +需要注意的是,如果类装饰器有参数,则 `__init__` 接收参数,而 `__call__` 接收 `func`。 |
| 293 | + |
| 294 | +# 装饰器的副作用 |
| 295 | + |
| 296 | +前面提到,使用装饰器有一个瑕疵,就是被装饰的函数,它的函数名称已经不是原来的名称了,回到最开始的例子: |
| 297 | + |
| 298 | +```python |
| 299 | +def makeitalic(func): |
| 300 | + def wrapped(): |
| 301 | + return "<i>" + func() + "</i>" |
| 302 | + return wrapped |
| 303 | + |
| 304 | +@makeitalic |
| 305 | +def hello(): |
| 306 | + return 'hello world' |
| 307 | +``` |
| 308 | + |
| 309 | +函数 `hello` 被 `makeitalic` 装饰后,它的函数名称已经改变了: |
| 310 | + |
| 311 | +```python |
| 312 | +>>> hello.__name__ |
| 313 | +'wrapped' |
| 314 | +``` |
| 315 | + |
| 316 | +为了消除这样的副作用,Python 中的 functool 包提供了一个 wraps 的装饰器: |
| 317 | + |
| 318 | +```python |
| 319 | +from functools import wraps |
| 320 | + |
| 321 | +def makeitalic(func): |
| 322 | + @wraps(func) # 加上 wraps 装饰器 |
| 323 | + def wrapped(): |
| 324 | + return "<i>" + func() + "</i>" |
| 325 | + return wrapped |
| 326 | + |
| 327 | +@makeitalic |
| 328 | +def hello(): |
| 329 | + return 'hello world' |
| 330 | + |
| 331 | +>>> hello.__name__ |
| 332 | +'hello' |
| 333 | +``` |
| 334 | + |
| 335 | +# 小结 |
| 336 | + |
| 337 | +- 本质上,装饰器就是一个返回函数的高阶函数。 |
| 338 | +- 装饰器可以动态地修改一个类或函数的功能,通过在原有的类或者函数上包裹一层修饰类或修饰函数实现。 |
| 339 | +- 事实上,装饰器就是闭包的一种应用,但它比较特别,接收被装饰函数为参数,并返回一个函数,赋给被装饰函数,闭包则没这种限制。 |
| 340 | + |
| 341 | +# 参考资料 |
| 342 | + |
| 343 | +- [Python修饰器的函数式编程 - coolshell](http://coolshell.cn/articles/11265.html) |
| 344 | +- [How can I make a chain of function decorators in Python? - Stack Overflow](http://stackoverflow.com/questions/739654/how-can-i-make-a-chain-of-function-decorators-in-python) |
| 345 | +- [Python中的装饰器介绍 – 思诚之道](http://www.bjhee.com/python-decorator.html) |
| 346 | +- [python装饰器入门与提高 | 赖明星](http://mingxinglai.com/cn/2015/08/python-decorator/) |
| 347 | + |
| 348 | + |
0 commit comments