Python装饰器的使用

内容绝大部分出自《Python高级编程》,Luke Sneeringer,清华大学出版社,Python版本2.7。
代码部分经修改可以完整运行,方便理解和直接测试。

理解语法

添加文档说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/usr/bin/env python
#_*_ coding:utf-8 _*_
#书写装饰器,为函数添加说明
def decorated_by(func):
#在函数文档说明后添加指定字段
func.__doc__ += '\nDecorated by decorated_by.'
return func
#书写函数
def add(x,y):
#文档说明,通过.__doc__方法查看
'''I'm add...'''
return x + y
@decorated_by
def add_(x,y):
'''I'm add_...'''
return x + y
#分别测试
#原始add
help(add)
#添加了装饰器的add_
help(add_)
#修改了名称为add0,分别查看add和add0
add0 = decorated_by(add)
help(add0)
help(add)

获取被装饰函数相关参数,详见Python官方文档-Code objects
这部分内容我也不是很理解。可以边看文档边用ipython之类的工具配合查看理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/env python
#_*_ coding:utf-8 _*_
#书写装饰器,为函数添加说明
def decorated_by(func):
#在函数文档说明后添加指定字段
print '函数文档:',func.func_doc
print '函数名称:',func.func_name
print '参数个数',func.func_code.co_argcount
print '函数code对象名称:',func.func_code.co_name
print 'code对象常量?',func.func_code.co_consts
print '参数元组:',func.func_code.co_varnames
#书写函数
@decorated_by
def add(x,y):
#文档说明,通过.__doc__方法查看
'''I'm add...'''
return x + y
add(4,5)

装饰器应用的顺序,自下而上(因为函数的解析是自内而外的)

1
2
3
4
5
6
7
8
9
10
#!/usr/bin/env python
#_*_ coding:utf-8 _*_
@also_decorated_by
@decorated_by
def add(x,y):
"""Return the sum of x and y."""
return x + y
#上面的表达方式与下面相同
add = also_decorated_by(decorated_by(add))

装饰器的使用

例如静态方法@staticmethod和类方法@classmethod,单元测试mock模块的@mock.path@mock.path.object。Web框架Django的@login_required(用户必须登录才能查看特定页面)和@permission_required(权限限制),Flask的@app.route,Celery的@task等。
装饰器显示的对函数进行前置和收尾工作,减少了代码的重复,并且增加了可读性。它主要应用在以下几个地方(主要 说的可能有些草率):

  • 在被装饰函数之前或之后追加功能,如合法性检查,权限验证,记录函数结果等
  • 预处理函数的参数(如改变格式),处理函数的返回值(如格式化输出JSON或XML等),附加参数等
  • 函数的注册

装饰器的编写

函数装饰器(这里说函数装饰器是因为还有类装饰器)通常接受一个callable函数作为参数,返回一个可调用函数(注意,返回的可能不是那个函数了呦)。
示例一:函数注册表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/env python
#_*_ coding:utf-8 _*_
registry = []
def register(decorated):
#注意列表添加的是函数对象,类型为fuction object
registry.append(decorated)
return decorated
@register
def add(x,y):
return x + y
@register
def multiply(x,y):
return x * y
#确认列表里的对象是函数不是函数返回结果
print registry
#结果列表
answers = []
#分别调用,批量执行注册表内的函数,并将结果追加到结果列表
for func in registry:
answers.append(func(5,3))
print answers

另一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/usr/bin/env python
#_*_ coding:utf-8 _*_
class Registry(object):
def __init__(self):
self._functions = []
def register(self,decorated):
self._functions.append(decorated)
return decorated
def run_all(self,*args,**kwargs):
return_values = []
for func in self._functions:
return_values.append(func(*args,**kwargs))
return return_values
#人机分离!
a = Registry()
b = Registry()
@a.register
def foo(x=3):
return x
@b.register
def bar(x=5):
return x
@a.register
@b.register
def sa(x=7):
return x
#查看各自结果
print a.run_all()
print b.run_all()
#赋值
print a.run_all(4)

示例二:类型检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/env python
#_*_ coding:utf-8 _*_
#装饰器检查函数参数是否为整形
def requires_ints(func):
def replace_of_func(*args,**kwargs):
#获取字典方式传入的参数
kwarg_values = [i for i in kwargs.values()]
#判断所有参数值是不是整数
for arg in list(args) + kwarg_values:
if not isinstance(arg,int):
raise TypeError('%s only accepts intergers as arguments.' % func.__name__)
#参数没问题的话要返回func的计算值
return func(*args,**kwargs)
#原始函数func已经被替换了
return replace_of_func
@requires_ints
def foo(x,y):
return x + y
#help命令已经暴露了,这个装饰器其实已经把原始func替换掉了,只不过是计算结果一致而已
help(foo)
#查看错误输入和正确输入
print foo(1,2)
foo('1','2')

另一个例子,在上述代码的基础上,追加了@functools.wraps(func),help(foo)返回结果则不同了,没有暴露出我们的狸猫换太子。

1
2
3
4
5
6
7
import functools
...
def requires_ints(func):
#functools.wraps(func)接受了一个参数即func,并保存了帮助和文档字符串等内容
@functools.wraps(func)
def replace_of_func(*args,**kwargs):
...

示例三:用户验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#!/usr/bin/env python
#_*_ coding:utf-8 _*_
#区分普通用户和匿名用户,并用__nonzero__私有方法做标识(返回False)
class User(object):
def __init__(self,username,email):
self.username = username
self.email = email
class AnonymousUser(object):
def __init__(self):
self.username = None
self.email = None
def __nonzero__(self):
return False
#
import functools
def requires_user(func):
@functools.wraps(func)
def inner(user,*args,**kwargs):
#判断非零和用户类型
if user and isinstance(user,User):
#原书这里写的是func(user,*args,**kwargs),我认为应该是func(*args,**kwargs),
#因为原始func只接受两个参数,并不包含user。
return func(*args,**kwargs)
else:
return ValueError('A valid user is required to run this.')
return inner
#创建函数,并用help(foo)发现函数已经增加了一个参数
@requires_user
def foo(x,y):
return x + y
help(foo)
#创建类实例,来验证参数个数和异常
usr1 = User('name','email')
print foo(usr1,7,8)
print foo(7,8)

示例四:输出格式化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env python
#_*_ coding:utf-8 _*_
import functools
import json
def json_output(func):
#序列化输出函数结果
@functools.wraps(func)
def inner(*args,**kwargs):
result = func(*args,**kwargs)
return json.dumps(result)
return inner
@json_output
def do_nothing():
return {'name':'YangMingJun'}
#测试
print do_nothing(),type(do_nothing())

另一个例子:捕获特定异常并JSON输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#!/usr/bin/env python
#_*_ coding:utf-8 _*_
import functools
import json
#定义异常
class JSONOutputError(Exception):
def __init__(self,message):
self._message = message
def __str__(self):
return self._message
def json_output(func):
@functools.wraps(func)
def inner(*args,**kwargs):
try:
result = func(*args,**kwargs)
except JSONOutputError as ex:
result = {'status':'error','message':str(ex)}
return json.dumps(result)
return inner
#可以针对特定异常进行处理(本处为JSONOutputError)
@json_output
def error():
raise JSONOutputError('This function is erratic.')
#打印输出
print error()
#查看其他类型异常
@json_output
def other_error():
raise ValueError('What is this.')
print other_error()

示例五:日志管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#!/usr/bin/env python
#_*_ coding:utf-8 _*_
import functools
import logging
import time
import os
#配置默认日志输出目录和格式,日志位于/tmp/test.log
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s',
datefmt='%a, %d %b %Y %H:%M:%S',
filename='/tmp/test.log',
filemode='w')
#定义装饰器函数
def logged(func):
@functools.wraps(func)
def inner(*args,**kwargs):
#记录开始时间
start = time.time()
#记录结果
return_value = func(*args,**kwargs)
#记录结束时间并计算
end = time.time()
delta = end - start
#使用默认的日志设置
logger = logging.getLogger()
logger.warn('Called method %s at %.2f;execution time %.2f seconds;result %r.' % (func.__name__,start,delta,return_value))
#返回原始func的返回值
return return_value
return inner
#
@logged
def sleep_and_return(return_value):
time.sleep(2)
return return_value
#查看输出
print sleep_and_return(10)
os.system('cat /tmp/test.log')

带参数的装饰器

废话少说,放码过来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#!/usr/bin/env python
#_*_ coding:utf-8 _*_
import functools
import json
class JSONOutputError(Exception):
def __init__(self,message):
self._message = message
def __str__(self):
return self._message
#添加参数的装饰器就是比普通的装饰器多了一层,外层负责接收参数,内层接收函数作为参数
#我的理解是,外层函数(这里是json_output)接收参数后,定向的返回了
#一个装饰器(这里是actual_decorator),这里定向是指针对所给的
#参数(indent=4)生成一个缩进4格的装饰器,然后actual_decorator就和普通装饰器
#做的工作一样,用另一个函数(这里是inner)去替代原始函数func,从而实现了带参数的效果
def json_output(indent=None,sort_keys=False):
def actual_decorator(func):
@functools.wraps(func)
def inner(*args,**kwargs):
try:
result = func(*args,**kwargs)
except JSONOutputError as ex:
result = {'status':'error','message':str(ex)}
return json.dumps(result,indent=indent,sort_keys=sort_keys)
return inner
return actual_decorator
#注意,此时应用装饰器时,没有参数也必须调用(即@json_output()),前文说了,这里调用相当于生成了
#actual_decorator装饰器,否则@json_output会报错,附加参数的效果
#相当于 json_output(参数)(do_nothing),即actual_decorator(do_nothing)
@json_output(indent=4)
def do_nothing():
return {'name':'YangMingJun'}
print do_nothing(),type(do_nothing())

理想的装饰器应该是这样的:

  • @json_output
  • @json_output()
  • @json_output(参数)
    所以对上述代码作如下修改:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#!/usr/bin/env python
#_*_ coding:utf-8 _*_
import functools
import json
class JSONOutputError(Exception):
def __init__(self,message):
self._message = message
def __str__(self):
return self._message
#前面都一样
#
def json_output(decorated_=None,indent=None,sort_keys=False):
#加了一层判断,不应该同时函数func和参数都给json_output,即json_output(func,indent,sort_keys)
if decorated_ and (indent or sort_keys):
raise RuntimeError('Unexpected arguments.')
def actual_decorator(func):
@functools.wraps(func)
def inner(*args,**kwargs):
try:
result = func(*args,**kwargs)
except JSONOutputError as ex:
result = {'status':'error','message':str(ex)}
return json.dumps(result,indent=indent,sort_keys=sort_keys)
return inner
#加了一层判断,获得的参数是函数(即直接@json_output,相当于json_output(func)),
#是函数则直接返回内层装饰器inner,否则照常返回actual_decorator
if decorated_:
return actual_decorator(decorated_)
else:
return actual_decorator
#仅测试不带调用,注意@json_output无括号调用,相当于decorated_ = do_nothing
@json_output
def do_nothing():
return {'name':'YangMingJun'}
print do_nothing(),type(do_nothing())