Skip to content

Commit 7650db4

Browse files
committed
add decorator.md
1 parent 9eb3ab6 commit 7650db4

1 file changed

Lines changed: 346 additions & 0 deletions

File tree

Functional/decorator.md

Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,348 @@
11
# 装饰器
22

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

Comments
 (0)