python装饰器入门与提高

本文转自http://mingxinglai.com/cn/2015/08/python-decorator/


[TOC]

1. 介绍

Python 2.2 开始提供了装饰器(decorator),装饰器作为修改函数的一种便捷方式,为程序员编写程序提供了便利性和灵活性,适当使用装饰器,能够有效的提高代码的可读性和可维护性,然而,装饰器并没有被广泛的使用,主要还是因为大多数人并不理解装饰器的工作机制。
本文首先介绍了装饰器的概念和用法(第2节),然后介绍了装饰器使用过程中的注意事项(第3节),之后讨论了装饰器的使用场景和注意是想(第4节),最后提供了一些装饰器的学习素材(第5节)。

2. 装饰器

装饰器本质上就是一个函数,这个函数接收其他函数作为参数,并将其以一个新的修改后的函数进行替换。概念比较抽象,一起来看两个装饰器的例子。

1. 装饰器的概念

考虑这样一组函数,它们在被调用时需要对某些参数进行检查,在本例中,需要对用户名进行检查,以判断用户是否有相应的权限进行某些操作。
程序清单1

class Store(object):
    def get_food(self, username, food):
        if username != 'admin':
            raise Exception("This user is not allowed to get food")
        return self.storage.get(food)

    def put_food(self, username, food):
        if username != 'admin':
            raise Exception("This user is not allowed to put food")
        self.storage.put(food)

显然,代码有重复,作为一个有追求的工程师,我们严格遵守DRY(Don’t repeat yourself)原则,于是,代码被改写成了程序清单2这样:
程序清单2

def check_is_admin(username):
    if username != 'admin':
        raise Exception("This user is not allowed to get food")

class Store(object):
    def get_food(self, username, food):
        check_is_admin(username)
        return self.storage.get(food)

    def put_food(self, username, food):
        check_is_admin(username)
        return self.storage.put(food)

现在代码整洁一点了,但是,有装饰器能够做的更好:
程序清单3

def check_is_admin(f):
    def wrapper(*args, **kwargs):
        if kwargs.get('username') != 'admin':
            raise Exception("This user is not allowed to get food")
        return f(*arg, **kargs)
    return wrapper

class Storage(object):
    @check_is_admin
    def get_food(self, username, food):
        return self.storage.get(food)

    @check_is_admin
    def put_food(self, username, food):
        return storage.put(food)

上面这段代码就是使用装饰器的典型例子:函数里面定义了一个函数,并将定义的这个函数作为返回值。这个例子足够简单,所以它的好处也不够明显,但是,却可以很好的演示装饰器的语法。
即使这样,我们也可以说,程序清单3比程序清单2更好,因为程序清单3能够将条件检查与具体逻辑分隔开来。在本例中,check_is_admin只是预检查,它的重要性不如具体的业务逻辑。我们将业务逻辑看做是这段程序的”重点”的话,那么,程序清单3一眼看过去就能看到”重点”,而程序清单2则不能,需要简单的思考(转个弯)才能区分条件检查和业务逻辑。当然,你可能觉得这没什么,但是,作为一名有追求的工程师,我们希望我们写出的代码能和散文一样优美。

2. 装饰器的本质

前面说过,装饰器本质上就是一个函数,这个函数接收其他函数作为参数,并将其以一个新的修改后的函数进行替换。下面这个例子能够更好地理解这句话。
程序清单4

def bread(func):
    def wrapper():
        print "</''''''\>"
        func()
        print "</______\>"
    return wrapper

sandwich_copy = bread(sandwich)
sandwich_copy()

输出结果如下:

</''''''\>
--ham--
</______\>

分析如下: bread是一个函数,它接受一个函数作为参数,然后返回一个新的函数,新的函数对原来的函数进行了一些修改和扩展,且这个新函数可以当做普通函数进行调用。
下面这段代码和程序清单4输出结果一摸一样,只是用了python提供的装饰器语法,看起来更加简单直接。
程序清单5

def bread(func):
    def wrapper():
        print "</''''''\>"
        func()
        print "</______\>"
    return wrapper

@bread
def sandwich(food="--ham--"):
    print food

到这里,我们应该已经能够理解装饰器的作用和用法了,再强调一遍:装饰器本质上就是一个函数,这个函数接收其他函数作为参数,并将其以一个新的修改后的函数进行替换

3. 使用装饰器需要注意的地方

我们在上一节中演示了装饰器的用法,可以看到,装饰器其实很好理解,也非常简单。但是,要用好装饰器,还有一些我们需要注意的地方,这一节就对这些需要注意的地方进行了讨论,首先讨论了在使用装饰器的情况下,如何保留原有函数的属性(见3.1节);然后讨论了如何实现一个更加智能的装饰器;之后讨论了使用多个装饰器时,各个装饰器的调用顺序(见3.3节);最后说明了如何给装饰器传递参数(见3.4节)。

1. 函数的属性变化

装饰器动态创建的新函数替换原来的函数,但是,新函数缺少很多原函数的属性,如docstring和名字。
程序清单6

def is_admin(f):
    def wrapper(*args, **kwargs):
        if kwargs.get("username") != 'admin':
            raise Exception("This user is not allowed to get food")
        return f(*args, **kwargs)
    return wrapper

def foobar(username='someone'):
    """Do crazy stuff"""
    pass

@is_admin
def barfoo(username='someone'):
    """Do crazy stuff"""
    pass

def main():
    print foobar.func_doc
    print foobar.__name__

    print barfoo.func_doc
    print barfoo.__name__

if __name__ == '__main__':
    main()

程序清单6的输出结果:

Do crazy stuff
foobar

None
wrapper

程序清单6中,我们定义了两个函数foobar与barfoo,其中,barfoo使用装饰器进行了封装,我们获取foobar与barfoo的docstring和函数名字,可以看到,使用了装饰器的函数,不能够正确获取函数原有的docstring与名字,为了解决这个问题,可以使用python内置的 functools模块。
程序清单7

import functools

def is_admin(f):
    @functools.wraps(f)
    def wrapper(*args, **kwargs):
        if kwargs.get("username") != 'admin':
            raise Exception("This user is not allowed to get food")
        return f(*arg, **kwargs)
    return wrapper

我们只需要增加一行代码,就能够正确地获取函数的属性。
此外,我们也可以向下面这样:

def is_admin(f):
    def wrapper(*args, **kwargs):
        if kwargs.get("username") != 'admin':
            raise Exception("This user is not allowed to get food")
        return f(*args, **kwargs)
    return functools.wraps(f)(wrapper) # important

当然,个人推荐第一种方法,因为第一种方法可读性更强。

2. 使用inspect获取函数参数

下面看一下程序清单8,它是否会正确输出结果呢?
程序清单8

import functools

def check_is_admin(f):
    @functools.wraps(f)
    def wrapper(*args, **kwargs):
        print kwargs
        if kwargs.get('username') != 'admin':
            raise Exception("This user is not allowed to get food")
        return f(*args, **kwargs)
    return wrapper


@check_is_admin
def get_food(username, food='chocolate'):
    return "{0} get food: {1}".format(username, food)


def main():
    print get_food('admin')

if __name__ == '__main__':
    main()

程序清单8会抛出一个异常,因为我们传入的’admin’是一个位置参数,而我们却去关键字参数(kwargs)获取用户名,因此,`kwargs.get(‘username’)返回None,那么,权限检查发现,用户没有相应的权限,抛出异常。
为了提供一个更加智能的装饰器,我们需要使用python的inspect模块。inspect能够取出函数的签名,并对其进行操作,如下所示:
程序清单9

import functools
import inspect

def check_is_admin(f):
    @functools.wraps(f)
    def wrapper(*args, **kwargs):
        func_args = inspect.getcallargs(f, *args, **kwargs)
        print func_args
        if func_args.get('username') != 'admin':
            raise Exception("This user is not allowed to get food")
        return f(*args, **kwargs)
    return wrapper


@check_is_admin
def get_food(username, food='chocolate'):
    return "{0} get food: {1}".format(username, food)


def main():
    print get_food('admin')

if __name__ == '__main__':
    main()

承担主要工作的函数是inspect.getcallargs,它返回一个将参数名字和值作为键值对的字典,这程序清单7中,这个函数返回{'username':'admin','food':'chocolate'}。这意味着我们的装饰器不必检查参数username是基于位置的参数还是基于关键字的参数,而只需在字典中查找即可。

3. 多个装饰器的调用顺序

多个装饰器的调用顺序也很好理解,我们一stackoverflow上的这个问题为例进行说明。
问题

How can I make two decorators in Python that would do the following?
@makebold
@makeitalic
def say():
return "Hello"
which should return
<b><i>Hello</i></b>

答案

def makebold(fn):
    def wrapped():
        return "<b>" + fn() + "</b>"
    return wrapped

def makeitalic(fn):
    def wrapped():
        return "<i>" + fn() + "</i>"
    return wrapped

@makebold
@makeitalic
def hello():
    return "hello world"

print hello() ## returns <b><i>hello world</i></b>

分析
我们在2.2节说过,装饰器就是在外层进行了封装:

@bread
sandwich()

sandwich_copy = bread(sandwich)

那么,封装两层可以像这样理解:

@makebold
@makeitalic
hello()

hello-copy = makebold(makeitalic(helo))

因此,这样理解以后,对于多个装饰器的调用顺序,就不再有疑问了。

4. 给装饰器传递参数

下面通过一个官方的例子来看如何给装饰器传递参数。官方介绍了一个非常有用的装饰器,即设置超时器。如果函数调用超时,则抛出异常。
程序清单10

def timeout(seconds, error_message = 'Function call timed out'):
   def decorated(func):
       def _handle_timeout(signum, frame):
           raise TimeoutError(error_message)

       def wrapper(*args, **kwargs):
           signal.signal(signal.SIGALRM, _handle_timeout)
           signal.alarm(seconds)
           try:
               result = func(*args, **kwargs)
           finally:
               signal.alarm(0)
           return result

       return functools.wraps(func)(wrapper)

   return decorated

使用方法如下:

import time

@timeout(1, 'Function slow; aborted')
def slow_function():
    time.sleep(5)

对应于我们这篇博客一直讨论的例子,传递参数的代码如下所示:
程序清单11

def is_admin(admin='admin'):
    def decorated(f):
        @functools.wraps(f)
        def wrapper(*args, **kwargs):
            if kwargs.get("username") != admin:
                raise Exception("This user is not allowed to get food")
            return f(*args, **kwargs)
        return wrapper
    return decorated


@is_admin(admin='root')
def barfoo(username='someone'):
    """Do crazy stuff"""
    print '{0} get food'.format(username)


if __name__ == '__main__':
    barfoo(username='root')

4. 装饰器的使用场景与缺点

1. 装饰器的使用场景

装饰器虽然语法比较复杂,但是,在一些场景下,也确实比较有用。包括:
注入参数(提供默认参数,生成参数)
记录函数行为(日志、缓存、计时什么的)
预处理/后处理(配置上下文什么的)
修改调用时的上下文(线程异步或者并行,类方法)
下面这个例子演示了上面提到的3中情况,如下所示:
程序清单12

def benchmark(func):
    """
    A decorator that prints the time a function takes
    to execute.
    """
    import time
    def wrapper(*args, **kwargs):
        t = time.clock()
        res = func(*args, **kwargs)
        print func.__name__, time.clock()-t
        return res
    return wrapper


def logging(func):
    """
    A decorator that logs the activity of the script.
    (it actually just prints it, but it could be logging!)
    """
    def wrapper(*args, **kwargs):
        res = func(*args, **kwargs)
        print func.__name__, args, kwargs
        return res
    return wrapper


def counter(func):
    """
    A decorator that counts and prints the number of times a function has been executed
    """
    def wrapper(*args, **kwargs):
        wrapper.count = wrapper.count + 1
        res = func(*args, **kwargs)
        print "{0} has been used: {1}x".format(func.__name__, wrapper.count)
        return res
    wrapper.count = 0
    return wrapper

@counter
@benchmark
@logging
def reverse_string(string):
    return str(reversed(string))

关于装饰器的例子,官方列出了一个长长的列表,这里很多代码可以直接拿来使用,如果需要详细地了解装饰器的使用场景,可以学习一下这份列表。

2. 装饰器有哪些缺点

在我们目前的实际项目中,装饰器使用还不够多,所以没有积累很多的经验,下面是国外大神给出的装饰器的缺点,以供参考:

Decorators were introduced in Python 2.4, so be sure your code will be run on >= 2.4.
Decorators slow down the function call. Keep that in mind.
You cannot un-decorate a function. (There are hacks to create decorators that can be removed, but nobody uses them.) So once a function is decorated, it’s decorated for all the code.
Decorators wrap functions, which can make them hard to debug.

5. 其他学习资料

本文较为全面地介绍了装饰器的用法,也给出了装饰器的使用场景和缺点。如果还需要进一步的学习装饰器,可以了解一下下面这几份资料:
python decorator library
source code of flask
Magic decorator syntax for asynchronous code in Python
A Python decorator that helps ensure that a Python Process is running only once

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容

  • 本文为《爬着学Python》系列第四篇文章。从本篇开始,本专栏在顺序更新的基础上,会有不规则的更新。 在Pytho...
    SyPy阅读 2,489评论 4 11
  • http://python.jobbole.com/85231/ 关于专业技能写完项目接着写写一名3年工作经验的J...
    燕京博士阅读 7,548评论 1 118
  • 要点: 函数式编程:注意不是“函数编程”,多了一个“式” 模块:如何使用模块 面向对象编程:面向对象的概念、属性、...
    victorsungo阅读 1,468评论 0 6
  • Python进阶框架 希望大家喜欢,点赞哦首先感谢廖雪峰老师对于该课程的讲解 一、函数式编程 1.1 函数式编程简...
    Gaolex阅读 5,490评论 6 53
  • 鹏程万里 鹏鲲展翅行万里, 程门立雪苦读书。 万卷诗书终受益, 里内里外人称许。 注1:鹏鲲即鲲鹏,古代传说中的大...
    亮靓_27d5阅读 215评论 20 38