Python装饰器入门与提高

理解PYTHON中的装饰器

Posted by xiaoh on March 27, 2016

目录

  1. 开篇介绍
    1. 简介
    2. 函数即对象
  2. 装饰器的本质
  3. 需注意的地方
    1. 属性变化
    2. 使用inspect获取函数参数
    3. 多个装饰器的调用顺序
    4. 给装饰器传递参数
  4. 装饰器的使用场景与缺点
    1. 装饰器的使用场景
    2. 缺点
  5. 参考链接
  6. 学习资料

项目里面用到LOG的功能很常见,最近我想,可以将这个写成一个装饰器来做LOG的自动记录(打印函数的参数和生命周期信息),研究了一下装饰器,才有了如下的文章。


开篇介绍

简介

Python 2.2 开始提供了装饰器(decorator),装饰器作为修改函数的一种便捷方式,为程序员编写程序提供了便利性和灵活性,适当使用装饰器,能够有效的提高代码的可读性和可维护性,然而,装饰器并没有被广泛的使用,主要还是因为大多数人并不理解装饰器的工作机制。

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

函数即对象

要理解装饰器,需要理解下面两个Python的概念

Python的函数是对象

简单的例子来说明一下:

def shout(word="yes"):
    return word.capitalize()+"!"

print shout()
# outputs : 'Yes!'

# 作为一个对象,你可以讲函数赋值给另一个对象
scream = shout

# 注意到这里我们并没有使用括号:我们不是调用函数,而是将函数'shout'赋给变量'scream'
# 这意味着,你可以通过'scream'调用'shout'

print scream()
# outputs : 'Yes!'

# 不仅如此,你可以删除老的名称'shout',但是通过'scream'依旧可以访问原有函数

del shout
try:
    print shout()
except NameError, e:
    print e
    #outputs: "name 'shout' is not defined"

print scream()
# outputs: 'Yes!'
函数可以被定义在另一个函数里

代码说明

def talk():
    # 你可以定义一个函数
    def whisper(word="yes"):
        return word.lower()+"..."

    # ... 并且立刻调用
    print whisper()

# 每次当你调用"talk", 都会定义"whisper"
# 并且在"talk"中被调用
talk()
# outputs:
# "yes..."

#但是在"talk"外部,函数"whisper"不存在!
try:
    print whisper()
except NameError, e:
    print e
    #outputs : "name 'whisper' is not defined"*
函数引用

理解了上面两个概念,就很容易理解这个概念:一个函数可以返回另一个函数

def getTalk(type="shout"):

    # 定义函数
    def shout(word="yes"):
        return word.capitalize()+"!"

    def whisper(word="yes") :
        return word.lower()+"...";

    # 返回函数
    if type == "shout":
        # 没有使用"()", 并不是要调用函数,而是要返回函数对象
        return shout
    else:
        return whisper

# 如何使用?

# 将函数返回值赋值给一个变量
talk = getTalk()

# 我们可以打印下这个函数对象
print talk
#outputs : <function shout at 0xb7ea817c>

# 这个对象是函数的返回值
print talk()
#outputs : Yes!

# 不仅如此,你还可以直接使用之
print getTalk("whisper")()
#outputs : yes...

同时,可以理解,一个函数也可以做为参数来进行传递

def doSomethingBefore(func):
    print "I do something before then I call the function you gave me"
    print func()

doSomethingBefore(scream)
#outputs:
#I do something before then I call the function you gave me
#Yes!

装饰器就是封装器,可以让你在被装饰函数之前或之后执行代码,而不必修改函数本身


装饰器的本质

先来看几段代码:

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)

程序中多个函数需要检查用户是否为admin,我们可以独立出一个函数:

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)

这样看着就好看多了,不过还有没有更加简练的呢?

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)

可以看到,我们将检查的逻辑单独拎出来,这样更容易理解函数的真实用意,将一些小事情独立出来,能够更好的让同事理解咱们的代码。

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

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

sandwich_copy = bread(sandwich)
sandwich_copy()

输出结果如下:

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

bread是一个函数,它接受一个函数作为参数,然后返回一个新的函数,新的函数对原来的函数进行了一些修改和扩展,且这个新函数可以当做普通函数进行调用。

下面这段代码和上面的程序输出结果一摸一样,只是用了python提供的装饰器语法,看起来更加简单直接。

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

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

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


需注意的地方

属性变化

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

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()

输出结果:

Do crazy stuff
foobar

None
wrapper

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

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

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

使用inspect获取函数参数

考虑一下下面的程序的输出结果:

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()

这段程序会抛出一个异常,因为我们传入的’admin’是一个位置参数,而我们却去关键字参数(kwargs)获取用户名,因此,`kwargs.get(‘username’)返回None,那么,权限检查发现,用户没有相应的权限,抛出异常。

为了提供一个更加智能的装饰器,我们需要使用python的inspect模块。inspect能够取出函数的签名,并对其进行操作,如下所示:

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是基于位置的参数还是基于关键字的参数,而只需在字典中查找即可。

多个装饰器的调用顺序

多个装饰器的调用顺序也很好理解,其实多个装饰器就是在嵌套而已

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>

装饰器就是在外层进行了封装:

@bread
sandwich()

sandwich_copy = bread(sandwich)

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

@makebold
@makeitalic
hello()

hello-copy = makebold(makeitalic(helo))

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

给装饰器传递参数

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

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)

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

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')

装饰器的使用场景与缺点

装饰器的使用场景

装饰器虽然语法比较复杂,但是,在一些场景下,也确实比较有用。包括:

  • 注入参数(提供默认参数,生成参数)
  • 记录函数行为(日志、缓存、计时什么的)
  • 预处理/后处理(配置上下文什么的)
  • 修改调用时的上下文(线程异步或者并行,类方法)

下面这个例子演示了上面提到的几种情况,如下所示:

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))

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

缺点
  • 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.

  1. http://stackoverflow.com/questions/739654/how-can-i-make-a-chain-of-function-decorators-in-python/1594484#1594484
  2. http://www.wklken.me/posts/2013/07/19/python-translate-decorator.html
  3. http://mingxinglai.com/cn/2015/08/python-decorator/

学习资料

  1. python decorator library
  2. source code of flask
  3. Magic decorator syntax for asynchronous code in Python
  4. A Python decorator that helps ensure that a Python Process is running only once

END