Python Decorator 修饰器简介

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

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

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

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

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 的修饰器其实是实现了下面的一个简单功能:

@decorator
def func():
    pass

等价于

func = decorator(func)

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

具体功能 实现代码 如下:

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

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

#!/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()

输出结果如下:

➜  Desktop  python test_decorator.py
hello!
in main

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

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

#!/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()

输出结果为:

➜  Desktop  python test_decorator.py
Hello, Pythonic

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

#!/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()

输出结果为:

➜  Desktop  python test_decorator.py
Hello, Pythonic

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

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)时并不能取到正确的结果。不过这个问题可以通过使用反射来解决:

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 可以解决绝大多数的问题。

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

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,比如路由组织,异步处理等等都是通过修饰器实现的。

Licensed under CC BY-NC-SA 4.0
Built with Hugo
主题 StackJimmy 设计