Python 函数装饰器(Decorators)

背景

最近在维护一个比较旧的web站点, 前端是比较熟悉的angularjs, 而后端是和js有点类似的python+Flask. 最近碰到一个需求, 需要对部分接口加上用户操作日志, 这种情况就是典型的AOP方法了, 研究了一下, python也有装饰器Decorators, 使用起来非常简单

原理

Python的装饰器本质上也是对函数的二次封装, 但是由于FlaskthreadLocal支持线程/协程的本地全局变量, 所以在装饰器里直接读写全局变量即可

手动实现装饰器

下面是通用教程里的相关方法, 和js的高阶函数实现基本一致, 但是js不支持对函数使用装饰器, 因为会存在变量提升

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
def a_new_decorator(a_func):

def wrapTheFunction():
print("I am doing some boring work before executing a_func()")

a_func()

print("I am doing some boring work after executing a_func()")

return wrapTheFunction

def a_function_requiring_decoration():
print("I am the function which needs some decoration to remove my foul smell")

a_function_requiring_decoration()
#outputs: "I am the function which needs some decoration to remove my foul smell"

a_function_requiring_decoration = a_new_decorator(a_function_requiring_decoration)
#now a_function_requiring_decoration is wrapped by wrapTheFunction()

a_function_requiring_decoration()
#outputs:I am doing some boring work before executing a_func()
# I am the function which needs some decoration to remove my foul smell
# I am doing some boring work after executing a_func()

@a_new_decorator
def a_function_requiring_decoration():
"""Hey you! Decorate me!"""
print("I am the function which needs some decoration to "
"remove my foul smell")

a_function_requiring_decoration()
#outputs: I am doing some boring work before executing a_func()
# I am the function which needs some decoration to remove my foul smell
# I am doing some boring work after executing a_func()

#the @a_new_decorator is just a short way of saying:
a_function_requiring_decoration = a_new_decorator(a_function_requiring_decoration)

这样实现的装饰器可以用, 但是如果调用获取函数信息的一些方法就会存在异常, 会返回装饰器自己的函数相关信息

1
2
print(a_function_requiring_decoration.__name__)
# Output: wrapTheFunction

解决这个问题也很简单, 在装饰器函数返回的时候, 将返回的函数信息从原始函数信息里复制过来再返回, 比如functools里面的update_wrapper实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
'__annotations__')
WRAPPER_UPDATES = ('__dict__',)
def update_wrapper(wrapper,
wrapped,
assigned = WRAPPER_ASSIGNMENTS,
updated = WRAPPER_UPDATES):
for attr in assigned:
try:
value = getattr(wrapped, attr)
except AttributeError:
pass
else:
setattr(wrapper, attr, value)
for attr in updated:
getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
wrapper.__wrapped__ = wrapped
return wrapper

wraps

上面描述了一个装饰器函数的功能和流程, 而为了避免自己实现, 我们可以直接使用functools里提供的wraps修饰器, 而且wraps里依赖的partial函数是使用C实现的, 不过官方也给出了python实现的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from functools import wraps

def wrapper(f):
@wraps(f)
def wrapper_function(*args, **kwargs):
"""这个是修饰函数"""
return f(*args, **kwargs)
return wrapper_function

@wrapper
def wrapped():
"""这个是被修饰的函数
"""
print('wrapped')

print(wrapped.__doc__) # 输出`这个是被修饰的函数`
print(wrapped.__name__) # 输出`wrapped`

操作日志记录

装饰器实现

根据上面的原理, 实现我们需要的装饰器

  • 利用闭包将模块传入到运行时
  • 利用flaskthreadLocal提供的g变量作为全局变量保存空间
1
2
3
4
5
6
7
8
9
10
11
12
from flask import g
from functools import wraps

def operation_log(module=''):
def deco_log(f):
@wraps(f)
def f_log(*args, **kwargs):
g._operation_log['module'] = module
g._need_operation_log = True
return f(*args, **kwargs)
return f_log
return deco_log

装饰器使用

1
2
3
4
5
6
7
from decorators import operation_log

class Config(restful.Resource):

@operation_log(module='config')
def post(self):
# ...

请求日志初始化

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
40
41
42
43
44
def register_site(app):
# ...
@app.before_request
def add_operation_log():
g._need_operation_log = False
g._operation_log = {
'address': '%s:%s'%(request.headers.environ.get('REMOTE_ADDR', ''), request.headers.environ.get('REMOTE_PORT', '')),
'time': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'module': '',
'path': request.base_url,
'ref': request.headers.environ.get('HTTP_REFERER', '').split('?')[0],
'method': request.headers.environ.get('REQUEST_METHOD', ''),
'user_agent': request.headers.environ.get('HTTP_USER_AGENT', ''),
'args': json.dumps(request.args),
'body': '',
'request_time': time.time(),
'status_code': 200,
'error': None,
# ... 其他值
}
if g._operation_log['method'] == 'post' and len(request.data) < 2000:
g._operation_log['body'] = request.data

@app.after_request
def put_operation_log(response=None):
g._operation_log['request_time'] = time.time() - g._operation_log['request_time']
g._operation_log['status_code'] = response.status_code
save_operation_log(g._operation_log)
return response

def error_handler(error):
g._operation_log['request_time'] = time.time() - g._operation_log['request_time']
g._operation_log['status_code'] = 500
g._operation_log['error'] = str(error)
save_operation_log(g._operation_log)
return error

app.handle_exception = error_handler

def save_operation_log(log):
# 有error一定会写日志
if g._need_operation_log or g._operation_log.get('error') is not None:
# 将log落地
# saveLog(g._operation_log)

其他

JavaScript 的装饰器

  • JavaScript的装饰器无法用于函数, 只能用于class, 因为存在变量提升
  • 以下代码基于第一版装饰器提案, 已经过时, 不一定适用于最新提案
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
// 声明
var counter = 0;
var add = function () {
counter++;
};
@add
function foo() {
}

// 实际执行
@add
function foo() {
}
var counter;
var add;
counter = 0;
add = function () {
counter++;
};

// 声明
var readOnly = require("some-decorator");
@readOnly
function foo() {
}

// 实际执行
var readOnly;
@readOnly
function foo() {
}
readOnly = require("some-decorator");

如果有类似的需求, 可以直接采用高阶函数实现, 类似 redux 的做法

1
2
3
4
5
6
7
8
9
10
11
12
function doSomething(name) {
console.log('Hello, ' + name);
}
function loggingDecorator(wrapped) {
return function () {
console.log('Starting');
const result = wrapped.apply(this, arguments);
console.log('Finished');
return result;
};
}
const wrapped = loggingDecorator(doSomething);
作者

Mosby

发布于

2018-08-06

许可协议

评论