Python Decorator 修饰器简介

很久没有写技术存档了,太过于罪恶。最近在智能硬件创业公司担任架构师,推广一些更新更酷的技术应用在各个方面,包括 Golang/Python/Docker 等,如果你有兴趣,也欢迎加入我们: kevin[at yeeuu[dot]com。广告时间结束。

Python 的修饰器是比较常见的开发应用帮助工具,他可以实现一些批量的修饰工作,比如统一来添加一些小功能等等。但是这些功能对原有的函数不产生侵入,也就是说可以实现快速的修改和替换、移除。

如果你使用过 Python 的 Web 框架,相信你对修饰器应该并不陌生:DjangoFlaskTornado 等常见的框架中都包含了修饰器的使用。

那么 decorators 是怎么实现的呢?还是先从一个简单的例子开始。先看下 Tornado 中的 tornado.web.authenticated 使用。

1
2
3
4
class IndexHandler(tornado.web.RequestHandler):
@tornado.web.authenticated
def get(self):
self.render('index.html')

tornado.web.authenticated 的作用就是判断 self.current_user 是否为 None 或者为空,否则跳转到之前设置的 login_url 地址去。至于获取 current_user 的内容,可以通过重载 get_current_user 函数实现。

在查看修饰器的具体代码之前,我们先来了解一下 Python 修饰器的原理。Python 的修饰器其实是实现了下面的一个简单功能:

1
2
3
@decorator
def func():
pass

等价于

1
func = decorator(func)

多层的修饰器则是实现了多层的回调调用。同时在底层层面,提供了 functools 包 用于实现相关功能,注意,该包是 2.5 之后版本中引入,如果你还在使用古老的 Python 版本,则可以手工实现同等功能。

具体功能 实现代码 如下:

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
def authenticated(method):
"""Decorate methods with this to require that the user be logged in.

If the user is not logged in, they will be redirected to the configured
`login url <RequestHandler.get_login_url>`.

If you configure a login url with a query parameter, Tornado will
assume you know what you're doing and use it as-is. If not, it
will add a `next` parameter so the login page knows where to send
you once you're logged in."""
# method 作为参数传入,实际上为类中的 get 等函数。
@functools.wraps(method)
def wrapper(self, *args, **kwargs):
# 判断当前用户
if not self.current_user:
# 没有权限操作
if self.request.method in ("GET", "HEAD"):
# get、head 请求跳转到登录页面
url = self.get_login_url()
if "?" not in url:
if urlparse.urlsplit(url).scheme:
# if login url is absolute, make next absolute too
next_url = self.request.full_url()
else:
next_url = self.request.uri
url += "?" + urlencode(dict(next=next_url))
self.redirect(url)
return
# 403 无权限操作
raise HTTPError(403)
# 通过验证
return method(self, *args, **kwargs)
# 函数式编程的典型范例 :)
return wrapper

根据这个,我们也可以尝试写一个自己的修饰器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/python
# -*- coding: utf-8 -*-

import functools


def print_hello(method):
@functools.wraps(method)
def wrapper(*args, **kwargs):
print "hello!"
return method(*args, **kwargs)
return wrapper


@print_hello
def main():
print 'in main'


if __name__ == '__main__':
main()

输出结果如下:

1
2
3
➜  Desktop  python test_decorator.py
hello!
in main

现在看起来已经很不错了,但是不能修改参数看起来有些需求还是无法实现。那么能够通过修饰器传递修改一些参数么?答案是肯定的。

修改一下上面的例子,我们试着用修饰器向函数中传递参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/python
# -*- coding: utf-8 -*-

import functools


def print_hello(method):
@functools.wraps(method)
def wrapper(*args, **kwargs):
kwargs['name'] = 'Pythonic'
return method(*args, **kwargs)
return wrapper


@print_hello
def main(name):
print "Hello, {}".format(name)


if __name__ == '__main__':
main()

输出结果为:

1
2
➜  Desktop  python test_decorator.py
Hello, Pythonic

那么能不能向修饰器里面传递参数呢?当然也是可以的,不过相对来说更复杂一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/python
# -*- coding: utf-8 -*-

import functools


def print_hello(name):
def real_wrapper(method):
@functools.wraps(method)
def wrapper(*args, **kwargs):
kwargs['name'] = name
return method(*args, **kwargs)
return wrapper
return real_wrapper


@print_hello(name='Pythonic')
def main(name):
print "Hello, {}".format(name)


if __name__ == '__main__':
main()

输出结果为:

1
2
➜  Desktop  python test_decorator.py
Hello, Pythonic

上面的实现方法是函数式的实现,Python 同样支持类模式的修饰器支持,比如:

1
2
3
4
5
6
7
8
9
class function_wrapper(object):
def __init__(self, wrapped):
self.wrapped = wrapped
def __call__(self, *args, **kwargs):
return self.wrapped(*args, **kwargs)

@function_wrapper
def function():
pass

虽然修饰器很好,但是也是会存在一些问题的,使用这种方式之后,想要获取被包装函数的参数(argument)或源代码(source code)时并不能取到正确的结果。不过这个问题可以通过使用反射来解决:

1
2
3
4
5
6
7
8
9
10
11
12
def get_true_argspec(method):
argspec = inspect.getargspec(method)
args = argspec[0]
if args and args[0] == 'self':
return argspec
if hasattr(method,'__func__'):
method = method.__func__
if not hasattr(method,'func_closure') or method.func_closure is None:
raise Exception("No closure for method.")

method = method.func_closure[0].cell_contents
return get_true_argspec(method)

不过说实话,很少在项目中获取这些东西,只是提供一些解决方案,实际上 functools.wraps 可以解决绝大多数的问题。

最后介绍一些比较常用的修饰器使用方法,比如,进行性能测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import cProfile, pstats, StringIO

def run_profile(func):
def wrapper(*args, **kwargs):
datafn = func.__name__ + ".profile" # Name the data file
prof = cProfile.Profile()
retval = prof.runcall(func, *args, **kwargs)
s = StringIO.StringIO()
sortby = 'cumulative'
ps = pstats.Stats(prof, stream=s).sort_stats(sortby)
ps.print_stats()
print s.getvalue()
return retval

return wrapper

还有一些案例可以看之前提供的几个框架的 API,比如路由组织,异步处理等等都是通过修饰器实现的。