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使用os模块的os.walk遍历文件夹示例
Jan 27 Python
Python3访问并下载网页内容的方法
Jul 28 Python
Python内置函数OCT详解
Nov 09 Python
python获取指定时间差的时间实例详解
Apr 11 Python
python解决pandas处理缺失值为空字符串的问题
Apr 08 Python
Python利用正则表达式实现计算器算法思路解析
Apr 25 Python
Python2和Python3.6环境解决共存问题
Nov 09 Python
使用python实现简单五子棋游戏
Jun 18 Python
Python利用PyExecJS库执行JS函数的案例分析
Dec 18 Python
Python实现aes加密解密多种方法解析
May 15 Python
python gui开发——制作抖音无水印视频下载工具(附源码)
Feb 07 Python
用 Python 元类的特性实现 ORM 框架
May 19 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
短波的认识
2021/03/01 无线电
基于Zookeeper的使用详解
2013/05/02 PHP
PHP入门教程之数组用法汇总(创建,删除,遍历,排序等)
2016/09/11 PHP
PHP实现的XXTEA加密解密算法示例
2018/08/28 PHP
日期函数扩展类Ver0.1.1
2006/09/07 Javascript
两个DIV等高的JS的实现代码
2007/12/23 Javascript
通过 Dom 方法提高 innerHTML 性能
2008/03/26 Javascript
jQuery弹出层插件简化版代码下载
2008/10/16 Javascript
ASP.NET中使用后端代码注册脚本 生成JQUERY-EASYUI的界面错位的解决方法
2010/06/12 Javascript
js实现图片拖动改变顺序附图
2014/05/13 Javascript
jQuery中Form相关知识汇总
2015/01/06 Javascript
js+HTML5基于过滤器从摄像头中捕获视频的方法
2015/06/16 Javascript
jQuery解析Json实例详解
2015/11/24 Javascript
JavaScript实现时间倒计时跳转(推荐)
2016/06/28 Javascript
浅谈jQuery效果函数
2016/09/16 Javascript
使用vue.js实现联动效果的示例代码
2017/01/10 Javascript
JS实现自定义状态栏动画文字效果示例
2017/10/12 Javascript
Vue下的国际化处理方法
2017/12/18 Javascript
浅谈Vue路由快照实现思路及其问题
2018/06/07 Javascript
jQuery实现简单评论区功能
2020/10/26 jQuery
[02:43]DOTA2英雄基础教程 德鲁伊
2014/01/13 DOTA
以windows service方式运行Python程序的方法
2015/06/03 Python
一篇文章入门Python生态系统(Python新手入门指导)
2015/12/11 Python
python中Pycharm 输出中文或打印中文乱码现象的解决办法
2017/06/16 Python
python如何为创建大量实例节省内存
2018/03/20 Python
如何安装并使用conda指令管理python环境
2019/07/10 Python
Pycharm最新激活码2019(推荐)
2019/12/31 Python
python操作链表的示例代码
2020/09/27 Python
python中用Scrapy实现定时爬虫的实例讲解
2021/01/18 Python
德国最大的婴儿用品网上商店:Kidsroom.de(支持中文)
2020/09/02 全球购物
用C语言实现文件读写操作
2013/10/27 面试题
自主招生自荐书
2013/11/29 职场文书
年度考核评语
2014/01/19 职场文书
科学发展观活动总结
2014/08/28 职场文书
学生自我评语
2015/01/04 职场文书
Win10/Win11 任务栏替换成经典样式
2022/04/19 数码科技