Python装饰器的函数式编程详解


Posted in Python onFebruary 27, 2015

Python的装饰器的英文名叫Decorator,当你看到这个英文名的时候,你可能会把其跟Design Pattern里的Decorator搞混了,其实这是完全不同的两个东西。虽然好像,他们要干的事都很相似——都是想要对一个已有的模块做一些“修饰工作”,所谓修饰工作就是想给现有的模块加上一些小装饰(一些小功能,这些小功能可能好多模块都会用到),但又不让这个小装饰(小功能)侵入到原有的模块中的代码里去。但是OO的Decorator简直就是一场恶梦,不信你就去看看wikipedia上的词条(Decorator Pattern)里的UML图和那些代码,这就是我在《 从面向对象的设计模式看软件设计》“餐后甜点”一节中说的,OO鼓励了——“厚重地胶合和复杂层次”,也是《 如此理解面向对象编程》中所说的“OO的狂热者们非常害怕处理数据”,Decorator Pattern搞出来的代码简直就是OO的反面教程。

Python 的 Decorator在使用上和Java/C#的Annotation很相似,就是在方法名前面加一个@XXX注解来为这个方法装饰一些东西。但是,Java/C#的Annotation也很让人望而却步,太TMD的复杂了,你要玩它,你需要了解一堆Annotation的类库文档,让人感觉就是在学另外一门语言。

而Python使用了一种相对于Decorator Pattern和Annotation来说非常优雅的方法,这种方法不需要你去掌握什么复杂的OO模型或是Annotation的各种类库规定,完全就是语言层面的玩法:一种函数式编程的技巧。如果你看过本站的《函数式编程》,你一定会为函数式编程的那种“描述你想干什么,而不是描述你要怎么去实现”的编程方式感到畅快。(如果你不了解函数式编程,那在读本文之前,还请你移步去看看《函数式编程》) 好了,我们先来点感性认识,看一个Python修饰器的Hello World的代码。

Hello World

下面是代码:

文件名:hello.py

def hello(fn):

    def wrapper():

        print "hello, %s" % fn.__name__

        fn()

        print "goodby, %s" % fn.__name__

    return wrapper

@hello

def foo():

    print "i am foo"

foo()

当你运行代码,你会看到如下输出:

[chenaho@chenhao-air]$ python hello.py

hello, foo

i am foo

goodby, foo

你可以看到如下的东西:

1)函数foo前面有个@hello的“注解”,hello就是我们前面定义的函数hello

2)在hello函数中,其需要一个fn的参数(这就用来做回调的函数)

3)hello函数中返回了一个inner函数wrapper,这个wrapper函数回调了传进来的fn,并在回调前后加了两条语句。

Decorator 的本质
对于Python的这个@注解语法糖- Syntactic Sugar 来说,当你在用某个@decorator来修饰某个函数func时,如下所示:

@decorator

def func():

    pass

其解释器会解释成下面这样的语句:

func = decorator(func)

尼玛,这不就是把一个函数当参数传到另一个函数中,然后再回调吗?是的,但是,我们需要注意,那里还有一个赋值语句,把decorator这个函数的返回值赋值回了原来的func。 根据《函数式编程》中的first class functions中的定义的,你可以把函数当成变量来使用,所以,decorator必需得返回了一个函数出来给func,这就是所谓的higher order function 高阶函数,不然,后面当func()调用的时候就会出错。 就我们上面那个hello.py里的例子来说,

@hello

def foo():

    print "i am foo"

被解释成了:

foo = hello(foo)

是的,这是一条语句,而且还被执行了。你如果不信的话,你可以写这样的程序来试试看:

def fuck(fn):

    print "fuck %s!" % fn.__name__[::-1].upper()

@fuck

def wfg():

    pass

没了,就上面这段代码,没有调用wfg()的语句,你会发现, fuck函数被调用了,而且还很NB地输出了我们每个人的心声!

再回到我们hello.py的那个例子,我们可以看到,hello(foo)返回了wrapper()函数,所以,foo其实变成了wrapper的一个变量,而后面的foo()执行其实变成了wrapper()。

知道这点本质,当你看到有多个decorator或是带参数的decorator,你也就不会害怕了。

比如:多个decorator

@decorator_one

@decorator_two

def func():

    pass

相当于:

func = decorator_one(decorator_two(func))

比如:带参数的decorator:

@decorator(arg1, arg2)

def func():

    pass

相当于:

func = decorator(arg1,arg2)(func)

这意味着decorator(arg1, arg2)这个函数需要返回一个“真正的decorator”。

带参数及多个Decrorator
我们来看一个有点意义的例子:

html.py

def makeHtmlTag(tag, *args, **kwds):

    def real_decorator(fn):

        css_class = " class='{0}'".format(kwds["css_class"]) \

                                     if "css_class" in kwds else ""

        def wrapped(*args, **kwds):

            return "<"+tag+css_class+">" + fn(*args, **kwds) + "</"+tag+">"

        return wrapped

    return real_decorator

@makeHtmlTag(tag="b", css_class="bold_css")

@makeHtmlTag(tag="i", css_class="italic_css")

def hello():

    return "hello world"

print hello()

# 输出:

# <b class='bold_css'><i class='italic_css'>hello world</i></b>

在上面这个例子中,我们可以看到:makeHtmlTag有两个参数。所以,为了让 hello = makeHtmlTag(arg1, arg2)(hello) 成功,makeHtmlTag 必需返回一个decorator(这就是为什么我们在makeHtmlTag中加入了real_decorator()的原因),这样一来,我们就可以进入到 decorator 的逻辑中去了—— decorator得返回一个wrapper,wrapper里回调hello。看似那个makeHtmlTag() 写得层层叠叠,但是,已经了解了本质的我们觉得写得很自然。

你看,Python的Decorator就是这么简单,没有什么复杂的东西,你也不需要了解过多的东西,使用起来就是那么自然、体贴、干爽、透气,独有的速效凹道和完美的吸收轨迹,让你再也不用为每个月的那几天感到焦虑和不安,再加上贴心的护翼设计,量多也不用当心。对不起,我调皮了。

什么,你觉得上面那个带参数的Decorator的函数嵌套太多了,你受不了。好吧,没事,我们看看下面的方法。

class式的 Decorator
首先,先得说一下,decorator的class方式,还是看个示例:

class myDecorator(object):

    def __init__(self, fn):

        print "inside myDecorator.__init__()"

        self.fn = fn

    def __call__(self):

        self.fn()

        print "inside myDecorator.__call__()"

@myDecorator

def aFunction():

    print "inside aFunction()"

print "Finished decorating aFunction()"

aFunction()

# 输出:

# inside myDecorator.__init__()

# Finished decorating aFunction()

# inside aFunction()

# inside myDecorator.__call__()

上面这个示例展示了,用类的方式声明一个decorator。我们可以看到这个类中有两个成员:
1)一个是__init__(),这个方法是在我们给某个函数decorator时被调用,所以,需要有一个fn的参数,也就是被decorator的函数。
2)一个是__call__(),这个方法是在我们调用被decorator函数时被调用的。
上面输出可以看到整个程序的执行顺序。

这看上去要比“函数式”的方式更易读一些。

下面,我们来看看用类的方式来重写上面的html.py的代码:

html.py

class makeHtmlTagClass(object):

    def __init__(self, tag, css_class=""):

        self._tag = tag

        self._css_class = " class='{0}'".format(css_class) \

                                       if css_class !="" else ""

    def __call__(self, fn):

        def wrapped(*args, **kwargs):

            return "<" + self._tag + self._css_class+">"  \

                       + fn(*args, **kwargs) + "</" + self._tag + ">"

        return wrapped

@makeHtmlTagClass(tag="b", css_class="bold_css")

@makeHtmlTagClass(tag="i", css_class="italic_css")

def hello(name):

    return "Hello, {}".format(name)

print hello("Hao Chen")

上面这段代码中,我们需要注意这几点:
1)如果decorator有参数的话,__init__() 成员就不能传入fn了,而fn是在__call__的时候传入的。
2)这段代码还展示了 wrapped(*args, **kwargs) 这种方式来传递被decorator函数的参数。(其中:args是一个参数列表,kwargs是参数dict,具体的细节,请参考Python的文档或是StackOverflow的这个问题,这里就不展开了)

用Decorator设置函数的调用参数
你有三种方法可以干这个事:

第一种,通过 **kwargs,这种方法decorator会在kwargs中注入参数。

def decorate_A(function):

    def wrap_function(*args, **kwargs):

        kwargs['str'] = 'Hello!'

        return function(*args, **kwargs)

    return wrap_function

@decorate_A

def print_message_A(*args, **kwargs):

    print(kwargs['str'])

print_message_A()

第二种,约定好参数,直接修改参数

def decorate_B(function):

    def wrap_function(*args, **kwargs):

        str = 'Hello!'

        return function(str, *args, **kwargs)

    return wrap_function

@decorate_B

def print_message_B(str, *args, **kwargs):

    print(str)

print_message_B()

第三种,通过 *args 注入

def decorate_C(function):

    def wrap_function(*args, **kwargs):

        str = 'Hello!'

        #args.insert(1, str)

        args = args +(str,)

        return function(*args, **kwargs)

    return wrap_function

class Printer:

    @decorate_C

    def print_message(self, str, *args, **kwargs):

        print(str)

p = Printer()

p.print_message()

Decorator的副作用
到这里,我相信你应该了解了整个Python的decorator的原理了。

相信你也会发现,被decorator的函数其实已经是另外一个函数了,对于最前面那个hello.py的例子来说,如果你查询一下foo.__name__的话,你会发现其输出的是“wrapper”,而不是我们期望的“foo”,这会给我们的程序埋一些坑。所以,Python的functool包中提供了一个叫wrap的decorator来消除这样的副作用。下面是我们新版本的hello.py。

文件名:hello.py

from functools import wraps

def hello(fn):

    @wraps(fn)

    def wrapper():

        print "hello, %s" % fn.__name__

        fn()

        print "goodby, %s" % fn.__name__

    return wrapper

@hello

def foo():

    '''foo help doc'''

    print "i am foo"

    pass

foo()

print foo.__name__ #输出 foo

print foo.__doc__  #输出 foo help doc

当然,即使是你用了functools的wraps,也不能完全消除这样的副作用。

来看下面这个示例:

from inspect import getmembers, getargspec

from functools import wraps

def wraps_decorator(f):

    @wraps(f)

    def wraps_wrapper(*args, **kwargs):

        return f(*args, **kwargs)

    return wraps_wrapper

class SomeClass(object):

    @wraps_decorator

    def method(self, x, y):

        pass

obj = SomeClass()

for name, func in getmembers(obj, predicate=inspect.ismethod):

    print "Member Name: %s" % name

    print "Func Name: %s" % func.func_name

    print "Args: %s" % getargspec(func)[0]

# 输出:

# Member Name: method

# Func Name: method

# Args: []

你会发现,即使是你你用了functools的wraps,你在用getargspec时,参数也不见了。

要修正这一问,我们还得用Python的反射来解决,下面是相关的代码:

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)

当然,我相信大多数人的程序都不会去getargspec。所以,用functools的wraps应该够用了。

一些decorator的示例
好了,现在我们来看一下各种decorator的例子:

给函数调用做缓存
这个例实在是太经典了,整个网上都用这个例子做decorator的经典范例,因为太经典了,所以,我这篇文章也不能免俗。

from functools import wraps

def memo(fn):

    cache = {}

    miss = object()

    @wraps(fn)

    def wrapper(*args):

        result = cache.get(args, miss)

        if result is miss:

            result = fn(*args)

            cache[args] = result

        return result

    return wrapper

@memo

def fib(n):

    if n < 2:

        return n

    return fib(n - 1) + fib(n - 2)

上面这个例子中,是一个斐波拉契数例的递归算法。我们知道,这个递归是相当没有效率的,因为会重复调用。比如:我们要计算fib(5),于是其分解成fib(4) + fib(3),而fib(4)分解成fib(3)+fib(2),fib(3)又分解成fib(2)+fib(1)…… 你可看到,基本上来说,fib(3), fib(2), fib(1)在整个递归过程中被调用了两次。

而我们用decorator,在调用函数前查询一下缓存,如果没有才调用了,有了就从缓存中返回值。一下子,这个递归从二叉树式的递归成了线性的递归。

Profiler的例子
这个例子没什么高深的,就是实用一些。

import cProfile, pstats, StringIO

def profiler(func):

    def wrapper(*args, **kwargs):

        datafn = func.__name__ + ".profile" # Name the data file

        prof = cProfile.Profile()

        retval = prof.runcall(func, *args, **kwargs)

        #prof.dump_stats(datafn)

        s = StringIO.StringIO()

        sortby = 'cumulative'

        ps = pstats.Stats(prof, stream=s).sort_stats(sortby)

        ps.print_stats()

        print s.getvalue()

        return retval

    return wrapper

注册回调函数

下面这个示例展示了通过URL的路由来调用相关注册的函数示例:

class MyApp():

    def __init__(self):

        self.func_map = {}

    def register(self, name):

        def func_wrapper(func):

            self.func_map[name] = func

            return func

        return func_wrapper

    def call_method(self, name=None):

        func = self.func_map.get(name, None)

        if func is None:

            raise Exception("No function registered against - " + str(name))

        return func()

app = MyApp()

@app.register('/')

def main_page_func():

    return "This is the main page."

@app.register('/next_page')

def next_page_func():

    return "This is the next page."

print app.call_method('/')

print app.call_method('/next_page')

注意:
1)上面这个示例中,用类的实例来做decorator。
2)decorator类中没有__call__(),但是wrapper返回了原函数。所以,原函数没有发生任何变化。

给函数打日志

下面这个示例演示了一个logger的decorator,这个decorator输出了函数名,参数,返回值,和运行时间。

from functools import wraps

def logger(fn):

    @wraps(fn)

    def wrapper(*args, **kwargs):

        ts = time.time()

        result = fn(*args, **kwargs)

        te = time.time()

        print "function      = {0}".format(fn.__name__)

        print "    arguments = {0} {1}".format(args, kwargs)

        print "    return    = {0}".format(result)

        print "    time      = %.6f sec" % (te-ts)

        return result

    return wrapper

@logger

def multipy(x, y):

    return x * y

@logger

def sum_num(n):

    s = 0

    for i in xrange(n+1):

        s += i

    return s

print multipy(2, 10)

print sum_num(100)

print sum_num(10000000)

上面那个打日志还是有点粗糙,让我们看一个更好一点的(带log level参数的):

import inspect

def get_line_number():

    return inspect.currentframe().f_back.f_back.f_lineno

def logger(loglevel):

    def log_decorator(fn):

        @wraps(fn)

        def wrapper(*args, **kwargs):

            ts = time.time()

            result = fn(*args, **kwargs)

            te = time.time()

            print "function   = " + fn.__name__,

            print "    arguments = {0} {1}".format(args, kwargs)

            print "    return    = {0}".format(result)

            print "    time      = %.6f sec" % (te-ts)

            if (loglevel == 'debug'):

                print "    called_from_line : " + str(get_line_number())

            return result

        return wrapper

    return log_decorator

但是,上面这个带log level参数的有两具不好的地方,
1) loglevel不是debug的时候,还是要计算函数调用的时间。
2) 不同level的要写在一起,不易读。

我们再接着改进:

import inspect

def advance_logger(loglevel):

    def get_line_number():

        return inspect.currentframe().f_back.f_back.f_lineno

    def _basic_log(fn, result, *args, **kwargs):

        print "function   = " + fn.__name__,

        print "    arguments = {0} {1}".format(args, kwargs)

        print "    return    = {0}".format(result)

    def info_log_decorator(fn):

        @wraps(fn)

        def wrapper(*args, **kwargs):

            result = fn(*args, **kwargs)

            _basic_log(fn, result, args, kwargs)

        return wrapper

    def debug_log_decorator(fn):

        @wraps(fn)

        def wrapper(*args, **kwargs):

            ts = time.time()

            result = fn(*args, **kwargs)

            te = time.time()

            _basic_log(fn, result, args, kwargs)

            print "    time      = %.6f sec" % (te-ts)

            print "    called_from_line : " + str(get_line_number())

        return wrapper

    if loglevel is "debug":

        return debug_log_decorator

    else:

        return info_log_decorator

你可以看到两点,
1)我们分了两个log level,一个是info的,一个是debug的,然后我们在外尾根据不同的参数返回不同的decorator。
2)我们把info和debug中的相同的代码抽到了一个叫_basic_log的函数里,DRY原则。

一个MySQL的Decorator
下面这个decorator是我在工作中用到的代码,我简化了一下,把DB连接池的代码去掉了,这样能简单点,方便阅读。

import umysql

from functools import wraps

class Configuraion:

    def __init__(self, env):

        if env == "Prod":

            self.host    = "coolshell.cn"

            self.port    = 3306

            self.db      = "coolshell"

            self.user    = "coolshell"

            self.passwd  = "fuckgfw"

        elif env == "Test":

            self.host   = 'localhost'

            self.port   = 3300

            self.user   = 'coolshell'

            self.db     = 'coolshell'

            self.passwd = 'fuckgfw'

def mysql(sql):

    _conf = Configuraion(env="Prod")

    def on_sql_error(err):

        print err

        sys.exit(-1)

    def handle_sql_result(rs):

        if rs.rows > 0:

            fieldnames = [f[0] for f in rs.fields]

            return [dict(zip(fieldnames, r)) for r in rs.rows]

        else:

            return []

    def decorator(fn):

        @wraps(fn)

        def wrapper(*args, **kwargs):

            mysqlconn = umysql.Connection()

            mysqlconn.settimeout(5)

            mysqlconn.connect(_conf.host, _conf.port, _conf.user, \

                              _conf.passwd, _conf.db, True, 'utf8')

            try:

                rs = mysqlconn.query(sql, {})

            except umysql.Error as e:

                on_sql_error(e)

            data = handle_sql_result(rs)

            kwargs["data"] = data

            result = fn(*args, **kwargs)

            mysqlconn.close()

            return result

        return wrapper

    return decorator

@mysql(sql = "select * from coolshell" )

def get_coolshell(data):

    ... ...

    ... ..

线程异步

下面量个非常简单的异步执行的decorator,注意,异步处理并不简单,下面只是一个示例。

from threading import Thread

from functools import wraps

def async(func):

    @wraps(func)

    def async_func(*args, **kwargs):

        func_hl = Thread(target = func, args = args, kwargs = kwargs)

        func_hl.start()

        return func_hl

    return async_func

if __name__ == '__main__':

    from time import sleep

    @async

    def print_somedata():

        print 'starting print_somedata'

        sleep(2)

        print 'print_somedata: 2 sec passed'

        sleep(2)

        print 'print_somedata: 2 sec passed'

        sleep(2)

        print 'finished print_somedata'

    def main():

        print_somedata()

        print 'back in main'

        print_somedata()

        print 'back in main'

    main()

虽然本文很长,但是都是非常实用,非常基础的知识,希望小伙伴们可以耐心开完。

Python 相关文章推荐
Python3基础之基本数据类型概述
Aug 13 Python
python中in在list和dict中查找效率的对比分析
May 04 Python
将pandas.dataframe的数据写入到文件中的方法
Dec 07 Python
用Python从0开始实现一个中文拼音输入法的思路详解
Jul 20 Python
python pygame实现滚动横版射击游戏城市之战
Nov 25 Python
python 画3维轨迹图并进行比较的实例
Dec 06 Python
Python 实现OpenCV格式和PIL.Image格式互转
Jan 09 Python
Python Scrapy框架:通用爬虫之CrawlSpider用法简单示例
Apr 11 Python
详解pytorch tensor和ndarray转换相关总结
Sep 03 Python
matplotlib 范围选区(SpanSelector)的使用
Feb 24 Python
看看如何用Python绘制小米新版天价logo
Apr 20 Python
Pytorch实现图像识别之数字识别(附详细注释)
May 11 Python
python分析nignx访问日志脚本分享
Feb 26 #Python
python分析apache访问日志脚本分享
Feb 26 #Python
Python构造函数及解构函数介绍
Feb 26 #Python
python中的__slots__使用示例
Feb 26 #Python
Python map和reduce函数用法示例
Feb 26 #Python
Python中运行并行任务技巧
Feb 26 #Python
Python通过递归遍历出集合中所有元素的方法
Feb 25 #Python
You might like
php读取html并截取字符串的简单代码
2009/11/30 PHP
ThinkPHP中关联查询实例
2014/12/02 PHP
Laravel 5.1 on SAE环境开发教程【附项目demo源码】
2016/10/09 PHP
解决laravel 5.1报错:No supported encrypter found的办法
2017/06/07 PHP
作为PHP程序员你要知道的另外一种日志
2018/07/30 PHP
用javascript实现兼容IE7的类库 IE7_0_9.zip提供下载
2007/08/08 Javascript
js函数调用常用方法详解
2012/12/03 Javascript
js数组操作常用方法
2014/05/08 Javascript
javascript求日期差的方法
2016/03/02 Javascript
JS定时器使用,定时定点,固定时刻,循环执行详解
2016/05/31 Javascript
jQuery为动态生成的select元素添加事件的方法
2016/08/29 Javascript
IOS中safari下的select下拉菜单文字过长不换行的解决方法
2016/09/26 Javascript
Javascript快速实现浏览器系统通知
2017/08/26 Javascript
angular 实时监听input框value值的变化触发函数方法
2018/08/31 Javascript
微信小程序导航栏滑动定位功能示例(实现CSS3的positionsticky效果)
2019/01/24 Javascript
jQuery插件simplePagination的使用方法示例
2020/04/28 jQuery
原生JS生成指定位数的验证码
2020/10/28 Javascript
vue 递归组件的简单使用示例
2021/01/14 Vue.js
Python中的super用法详解
2015/05/28 Python
python的格式化输出(format,%)实例详解
2018/06/01 Python
Python查找最长不包含重复字符的子字符串算法示例
2019/02/13 Python
详解Python_shutil模块
2019/03/15 Python
局域网内python socket实现windows与linux间的消息传送
2019/04/19 Python
Python 运行.py文件和交互式运行代码的区别详解
2019/07/02 Python
python实现的生成word文档功能示例
2019/08/23 Python
Pytorch中accuracy和loss的计算知识点总结
2019/09/10 Python
Python轻量级web框架bottle使用方法解析
2020/06/13 Python
详解CSS 3 中的 calc() 方法
2018/01/12 HTML / CSS
您附近的水疗和健康场所:Spafinder(美国)
2019/07/05 全球购物
竞选学生会主席演讲稿
2014/04/24 职场文书
俞敏洪励志演讲稿
2014/04/29 职场文书
教师师德表现自我评价
2015/03/05 职场文书
百家讲坛观后感
2015/06/12 职场文书
钓鱼岛事件感想
2015/08/11 职场文书
为什么中国式养孩子很累?
2019/08/07 职场文书
Centos7 Shell编程之正则表达式、文本处理工具详解
2022/08/05 Servers