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 相关文章推荐
解决python中使用plot画图,图不显示的问题
Jul 04 Python
使用Python实现租车计费系统的两种方法
Sep 29 Python
matplotlib实现热成像图colorbar和极坐标图的方法
Dec 13 Python
Python3利用print输出带颜色的彩色字体示例代码
Apr 08 Python
python高斯分布概率密度函数的使用详解
Jul 10 Python
python 矢量数据转栅格数据代码实例
Sep 30 Python
Python高级编程之继承问题详解(super与mro)
Nov 19 Python
python实现图片上添加图片
Nov 26 Python
Python Dict找出value大于某值或key大于某值的所有项方式
Jun 05 Python
python中slice参数过长的处理方法及实例
Dec 15 Python
Pycharm创建python文件自动添加日期作者等信息(步骤详解)
Feb 03 Python
python的netCDF4批量处理NC格式文件的操作方法
Mar 21 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下将XML转换为数组
2010/01/01 PHP
PHP实现批量修改文件名的方法示例
2019/09/18 PHP
使用SyntaxHighlighter实现HTML高亮显示代码的方法
2010/02/04 Javascript
javascript学习笔记(八) js内置对象
2012/06/19 Javascript
Javascript中设置默认参数值示例
2014/09/11 Javascript
JavaScript中使用document.write向页面输出内容实例
2014/10/16 Javascript
Bootstrap每天必学之标签与徽章
2015/11/27 Javascript
下一代Bootstrap的5个特点 超酷炫!
2016/06/17 Javascript
基于Javascript实现文件实时加载进度的方法
2016/10/12 Javascript
jQuery实现鼠标滑过图片移动特效
2016/12/08 Javascript
vue.js学习之递归组件
2016/12/13 Javascript
AngularJS中$http使用的简单介绍
2017/03/17 Javascript
Angular项目从新建、打包到nginx部署全过程记录
2017/12/09 Javascript
vue实现图片滚动的示例代码(类似走马灯效果)
2018/03/03 Javascript
vue2.0学习之axios的封装与vuex介绍
2018/05/28 Javascript
浅谈Angular单元测试总结
2019/03/22 Javascript
在win和Linux系统中python命令行运行的不同
2016/07/03 Python
Python计时相关操作详解【time,datetime】
2017/05/26 Python
给你选择Python语言实现机器学习算法的三大理由
2017/11/15 Python
Python3实现取图片中特定的像素替换指定的颜色示例
2019/01/24 Python
Python英文文本分词(无空格)模块wordninja的使用实例
2019/02/20 Python
python3 mmh3安装及使用方法
2019/10/09 Python
keras .h5转移动端的.tflite文件实现方式
2020/05/25 Python
美国现代家具和家居商店:Apt2B
2016/08/29 全球购物
番木瓜健康和保健产品第一大制造商:Herbal Papaya
2017/04/25 全球购物
SQL Server里面什么样的视图才能创建索引
2015/04/17 面试题
两道JAVA笔试题
2016/09/14 面试题
护士岗位职责
2014/02/16 职场文书
2014年党课学习心得体会
2014/07/08 职场文书
公务员上班玩游戏检讨书
2014/09/17 职场文书
乡镇领导班子批评与自我批评材料
2014/09/23 职场文书
党员群众路线剖析材料
2014/10/08 职场文书
退伍军人感言
2015/08/01 职场文书
该怎么书写道歉信?
2019/07/03 职场文书
2020年元旦祝福语录,总有适合你的
2019/12/31 职场文书
MySQL优化之慢日志查询
2022/06/10 MySQL