Python装饰器学习笔记

装饰器(Decorators)

装饰器是 Python 的一个重要部分。它是修改其余函数的功能的函数,有助于让咱们的代码更简短html

 

装饰器本质上是一个Python函数,它可让其余函数在不须要作任何代码变更的前提下增长额外功能,装饰器的返回值也是一个函数对象。它常常用于有切面需求的场景,好比:插入日志、性能测试、事务处理、缓存、权限校验等场景。装饰器是解决这类问题的绝佳设计,有了装饰器,咱们就能够抽离出大量与函数功能自己无关的雷同代码并继续重用。python

 

归纳的讲,装饰器的做用就是为已经存在的函数或对象添加额外的功能程序员

为何须要装饰器

咱们假设你的程序实现了func_enter()func_quit()两个函数。缓存

def func_enter():
print "enter!"


def func_quit():
print "enter!" # bug here


if __name__ == '__main__':
func_enter()
func_quit()

运行结果:bash

enter!
enter!
(wda_python) bash-3.2$ 

可是在实际调用中, 咱们发现程序出错了, 上面打印了2个enter。通过调试咱们发现是func_quit()出错了app

如今假如要求调用每一个方法前都要记录进入函数的名称, 好比这样:函数

[DEBUG]: enter func_enter()
enter!
[DEBUG]: enter func_quit()
enter!

一种最直白简单的方式是这样写:性能

def func_enter():
    print "[DEBUG]: enter func_enter()"
    print "enter!"


def func_quit():
    print "[DEBUG]: enter func_quit()"
    print "enter!"  # bug here


if __name__ == '__main__':
    func_enter()
    func_quit()

可是很low对吧, 咱们能够试着这样写:测试

def debug():
    import inspect
    caller_name = inspect.stack()[1][3]
    print '[BEBUG]: enter {}()'.format(caller_name)

def func_enter():
    debug()
    print "enter!"


def func_quit():
    debug()
    print "enter!"  # bug here


if __name__ == '__main__':
    func_enter()
    func_quit()

看起来会好一点, 可是每一个函数都要调用一次debug()函数,仍是不太够, 万一若是又改需求进出不打印调用者了, 其余地方或者函数在打印, 又要大改优化

怎么办呢? 这个时候装饰器就能够派上用场了

 

怎么写一个装饰器

咱们来看一个例子

def debug(func):
    def wrapper():
        print '[DEBUG]: enter {}()'.format(func.__name__)
        return func()
    return wrapper

@debug
def func_enter():
    print "enter!"

@debug
def func_quit():
    print "enter!"  # bug here


if __name__ == '__main__':
    func_enter()
    func_quit()

运行结果:

[DEBUG]: enter func_enter()
enter!
[DEBUG]: enter func_quit()
enter!
(wda_python) bash-3.2$ 

这是一个最简单的装饰器, 可是有个问题, 若是被装饰的函数须要传入参数, 那么这个装饰器就坏了,由于返回的函数并不能接受参数

这里能够指定装饰器函数wrapper接受和原函数同样的参数, 好比:

#coding: utf-8

def debug(func):
    def wrapper(something):     # 这里指定同样的参数
        print '[DEBUG]: enter {}()'.format(func.__name__)
        return func(something)
    return wrapper # 返回包装过的函数

@debug
def func_enter(something):
    print "enter {}!".format(something)

@debug
def func_quit(something):
    print "enter {}!".format(something)  # bug here


if __name__ == '__main__':
    func_enter("enter_func")
    func_quit("quit_func")

运行结果:

[DEBUG]: enter func_enter()
enter enter_func!
[DEBUG]: enter func_quit()
enter quit_func!

这样解决了传参数的问题, 可是这里有个很大的问题是这里只适配了咱们的func_enter和func_quit函数的参数, 若是要用来去装饰其余带参数的函数呢?

还好python提供可变参数*args和关键字参数**kwargs, 有这两个参数装饰器就能够用于任意目标函数了

#coding: utf-8

def debug(func):
    def wrapper(*args, **kwargs):     # 这里指定同样的参数
        print '[DEBUG]: enter {}()'.format(func.__name__)
        return func(*args, **kwargs)
    return wrapper # 返回包装过的函数

@debug
def func_enter(something):
    print "enter {}!".format(something)

@debug
def func_quit(something):
    print "enter {}!".format(something)  # bug here


if __name__ == '__main__':
    func_enter("enter_func")
    func_quit("quit_func")

运行结果:

[DEBUG]: enter func_enter()
enter enter_func!
[DEBUG]: enter func_quit()
enter quit_func!
(wda_python) bash-3.2$ 

 

带参数的装饰器

若是前面咱们的装饰器须要完成的功能不只仅是能在进入某个函数后打印出调用信息,还要指定log级别, 那么装饰器能够是这样:

#coding: utf-8

def debug(level):
    def wrapper(func):
        def inner_wrapper(*args, **kwargs):
            print '[{level}]: enter {func}()'.format(level=level,func=func.__name__)
            return func(*args, **kwargs)
        return inner_wrapper
    return wrapper

@debug(level='Debug')
def func_enter(something):
    print "enter {}!".format(something)

@debug(level='Debug')
def func_quit(something):
    print "enter {}!".format(something)  # bug here


if __name__ == '__main__':
    func_enter("enter_func")
    func_quit("quit_func")

运行结果:

[Debug]: enter func_enter()
enter enter_func!
[Debug]: enter func_quit()
enter quit_func!
(wda_python) bash-3.2$ 

 

基于类实现的装饰器

装饰器函数实际上是这样一个接口约束,它必须接受一个callable对象做为参数,而后返回一个callable对象。在Python中通常callable对象都是函数,但也有例外。只要某个对象重载了__call__()方法,那么这个对象就是callable的。

class Test():
    def __call__(self, *args, **kwargs):
        print 'call me!'

t = Test()
t()

运行结果:

call me!
(wda_python) bash-3.2$ 

__call__这样先后都带下划线的方法在Python中被称为内置方法,有时候也被称为魔法方法。重载这些魔法方法通常会改变对象的内部行为。上面这个例子就让一个类对象拥有了被调用的行为。

 

回到装饰器上的概念上来,装饰器要求接受一个callable对象,并返回一个callable对象(不太严谨,详见后文)。那么用类来实现也是也能够的。咱们可让类的构造函数__init__()接受一个函数,而后重载__call__()并返回一个函数,也能够达到装饰器函数的效果。

class Debug_info(object):
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print "[DEBUG]: enter function {func}()".format(func=self.func.__name__)
        return self.func(*args, **kwargs)

@Debug_info
def func_enter(something):
    print 'enter {}!'.format(something)

if __name__ == '__main__':
    func_enter("enter_func")

运行结果:

[DEBUG]: enter function func_enter()
enter enter_func!
(wda_python) bash-3.2$ 

 

带参数的类装饰器

若是须要经过类形式实现带参数的装饰器,那么会比前面的例子稍微复杂一点。那么在构造函数里接受的就不是一个函数,而是传入的参数。经过类把这些参数保存起来。而后在重载__call__方法是就须要接受一个函数并返回一个函数。

#coding: utf-8

class Debug_info(object):
    def __init__(self, level='INFO'):
        self.level= level

    def __call__(self, func):  # 接受函数
        def wrapper(*args, **kwargs):
            print "[{level}]: enter function {func}()".format(level=self.level,func=func.__name__)
            func(*args, **kwargs)
        return wrapper

@Debug_info(level='INFO')
def func_enter(something):
    print 'enter {}!'.format(something)

if __name__ == '__main__':
    func_enter("enter_func")

运行结果:

[INFO]: enter function func_enter()
enter enter_func!
(wda_python) bash-3.2$ 

 

内置的装饰器

在绑定属性时,若是咱们直接把属性暴露出去,虽然写起来很简单,可是,没办法检查参数,致使能够把成绩随便改:

s = Student()
s.score = 9999

这显然不合逻辑。为了限制score的范围,能够经过一个set_score()方法来设置成绩,再经过一个get_score()来获取成绩,这样,在set_score()方法里,就能够检查参数:

class Student(object):

    def get_score(self):
        return self._score

    def set_score(self, value):
        if not isinstance(value, int):
            raise ValueError('score must be an integer!')
        if value < 0 or value > 100:
            raise ValueError('score must between 0 ~ 100!')
        self._score = value

如今,对任意的Student实例进行操做,就不能为所欲为地设置score了:

>>> s = Student()
>>> s.set_score(60) # ok!
>>> s.get_score()
60
>>> s.set_score(9999)
Traceback (most recent call last):
  ...
ValueError: score must between 0 ~ 100!

可是,上面的调用方法又略显复杂,没有直接用属性这么直接简单。

有没有既能检查参数,又能够用相似属性这样简单的方式来访问类的变量呢?对于追求完美的Python程序员来讲,这是必需要作到的!

还记得装饰器(decorator)能够给函数动态加上功能吗?对于类的方法,装饰器同样起做用。Python内置的@property装饰器就是负责把一个方法变成属性调用的:

 

class Student(object):

    @property
    def score(self):
        return self._score

    @score.setter
    def score(self, value):
        if not isinstance(value, int):
            raise ValueError('score must be an integer!')
        if value < 0 or value > 100:
            raise ValueError('score must between 0 ~ 100!')
        self._score = value

@property的实现比较复杂,咱们先考察如何使用。把一个getter方法变成属性,只须要加上@property就能够了,此时,@property自己又建立了另外一个装饰器@score.setter,负责把一个setter方法变成属性赋值,因而,咱们就拥有一个可控的属性操做:

>>> s = Student()
>>> s.score = 60 # OK,实际转化为s.set_score(60)
>>> s.score # OK,实际转化为s.get_score()
60
>>> s.score = 9999
Traceback (most recent call last):
  ...
ValueError: score must between 0 ~ 100!

注意到这个神奇的@property,咱们在对实例属性操做的时候,就知道该属性极可能不是直接暴露的,而是经过getter和setter方法来实现的。

还能够定义只读属性,只定义getter方法,不定义setter方法就是一个只读属性:

 

class Student(object):

    @property
    def birth(self):
        return self._birth

    @birth.setter
    def birth(self, value):
        self._birth = value

    @property
    def age(self):
        return 2014 - self._birth

上面的birth是可读写属性,而age就是一个只读属性,由于age能够根据birth和当前时间计算出来。

@property普遍应用在类的定义中,可让调用者写出简短的代码,同时保证对参数进行必要的检查,这样,程序运行时就减小了出错的可能性。

 

装饰器里的那些坑

装饰器可让你代码更加优雅,减小重复,但也不全是优势,也会带来一些问题。

位置错误的代码

让咱们直接看示例代码:

def html_tags(tag_name):
    print 'begin outer function.'
    def wrapper_(func):
        print "begin of inner wrapper function."
        def wrapper(*args, **kwargs):
            content = func(*args, **kwargs)
            print "<{tag}>{content}</{tag}>".format(tag=tag_name, content=content)
        print 'end of inner wrapper function.'
        return wrapper
    print 'end of outer function'
    return wrapper_

@html_tags('b')
def hello(name='Toby'):
    return 'Hello {}!'.format(name)

hello()
hello()

在装饰器中我在各个可能的位置都加上了print语句,用于记录被调用的状况。你知道他们最后打印出来的顺序吗?若是你内心没底,那么最好不要在装饰器函数以外添加逻辑功能,不然这个装饰器就不受你控制了。如下是输出结果:

begin outer function.
end of outer function
begin of inner wrapper function.
end of inner wrapper function.
<b>Hello Toby!</b>
<b>Hello Toby!</b>
(wda_python) bash-3.2$ 

 

错误的函数签名和文档

装饰器装饰过的函数看上去名字没变,其实已经变了。

import datetime

def logging(func):
def wrapper(*args, **kwargs):
"""print log before a function."""
print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__)
return func(*args, **kwargs)
return wrapper

@logging
def say(something):
"""say something"""
print "say {}!".format(something)

print say.__name__
print say.__doc__

运行结果:

wrapper
print log before a function.
(wda_python) bash-3.2$ 

 

为何会这样呢?只要你想一想装饰器的语法糖@代替的东西就明白了。@等同于这样的写法。

say = logging(say)

logging其实返回的函数名字恰好是wrapper,那么上面的这个语句恰好就是把这个结果赋值给saysay__name__天然也就是wrapper了,不只仅是name,其余属性也都是来自wrapper,好比docsource等等。

使用标准库里的functools.wraps,能够基本解决这个问题。

import datetime
from functools import wraps

def logging(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        """print log before a function."""
        print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__)
        return func(*args, **kwargs)
    return wrapper

@logging
def say(something):
    """say something"""
    print "say {}!".format(something)

print say.__name__
print say.__doc__

运行结果:

say
say something
(wda_python) bash-3.2$ 

可是其实还不太完美, 由于函数的签名和源码仍是拿不到

import datetime
from functools import wraps
def logging(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        """print log before a function."""
        print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__)
        return func(*args, **kwargs)
    return wrapper

@logging
def say(something):
    """say something"""
    print "say {}!".format(something)

print say.__name__
print say.__doc__

import inspect
print inspect.getargspec(say)
print inspect.getsource(say)

运行结果:

say
say something
ArgSpec(args=[], varargs='args', keywords='kwargs', defaults=None)
    @wraps(func)
    def wrapper(*args, **kwargs):
        """print log before a function."""
        print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__)
        return func(*args, **kwargs)

(wda_python) bash-3.2$ 

若是要完全解决这个问题能够借用第三方包,好比wrapt, 后文有介绍

 

不能装饰@staticmethod 或者 @classmethod

当你想把装饰器用在一个静态方法或者类方法时,很差意思,报错了。

class Car(object):
    def __init__(self, model):
        self.model = model

    @logging  # 装饰实例方法,OK
    def run(self):
        print "{} is running!".format(self.model)

    @logging  # 装饰静态方法,Failed
    @staticmethod
    def check_model_for(obj):
        if isinstance(obj, Car):
            print "The model of your car is {}".format(obj.model)
        else:
            print "{} is not a car!".format(obj)

"""
Traceback (most recent call last):
...
  File "example_4.py", line 10, in logging
    @wraps(func)
  File "C:\Python27\lib\functools.py", line 33, in update_wrapper
    setattr(wrapper, attr, getattr(wrapped, attr))
AttributeError: 'staticmethod' object has no attribute '__module__'
"""

前面已经解释了@staticmethod这个装饰器,其实它返回的并非一个callable对象,而是一个staticmethod对象,那么它是不符合装饰器要求的(好比传入一个callable对象),你天然不能在它之上再加别的装饰器。要解决这个问题很简单,只要把你的装饰器放在@staticmethod以前就行了,由于你的装饰器返回的仍是一个正常的函数,而后再加上一个@staticmethod是不会出问题的。

class Car(object):
    def __init__(self, model):
        self.model = model

    @staticmethod
    @logging  # 在@staticmethod以前装饰,OK
    def check_model_for(obj):
        pass

 

如何优化你的装饰器

嵌套的装饰函数不太直观,咱们可使用第三方包类改进这样的状况,让装饰器函数可读性更好。

decorator.py

decorator.py是一个很是简单的装饰器增强包。你能够很直观的先定义包装函数wrapper(),再使用decorate(func, wrapper)方法就能够完成一个装饰器。

from decorator import decorate

def wrapper(func, *args, **kwargs):
    """print log before a function."""
    print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__)
    return func(*args, **kwargs)

def logging(func):
    return decorate(func, wrapper)  # 用wrapper装饰func

你也可使用它自带的@decorator装饰器来完成你的装饰器。

from decorator import decorator

@decorator
def logging(func, *args, **kwargs):
    print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__)
    return func(*args, **kwargs)

decorator.py实现的装饰器能完整保留原函数的namedocargs,惟一有问题的就是inspect.getsource(func)返回的仍是装饰器的源代码,你须要改为inspect.getsource(func.__wrapped__)

 

wrapt

wrapt是一个功能很是完善的包,用于实现各类你想到或者你没想到的装饰器。使用wrapt实现的装饰器你不须要担忧以前inspect中遇到的全部问题,由于它都帮你处理了,甚至inspect.getsource(func)也准确无误。

import wrapt

# without argument in decorator
@wrapt.decorator
def logging(wrapped, instance, args, kwargs):  # instance is must
    print "[DEBUG]: enter {}()".format(wrapped.__name__)
    return wrapped(*args, **kwargs)

@logging
def say(something): pass

使用wrapt你只须要定义一个装饰器函数,可是函数签名是固定的,必须是(wrapped, instance, args, kwargs),注意第二个参数instance是必须的,就算你不用它。当装饰器装饰在不一样位置时它将获得不一样的值,好比装饰在类实例方法时你能够拿到这个类实例。根据instance的值你可以更加灵活的调整你的装饰器。另外,argskwargs也是固定的,注意前面没有星号。在装饰器内部调用原函数时才带星号。

若是你须要使用wrapt写一个带参数的装饰器,能够这样写:

def logging(level):
    @wrapt.decorator
    def wrapper(wrapped, instance, args, kwargs):
        print "[{}]: enter {}()".format(level, wrapped.__name__)
        return wrapped(*args, **kwargs)
    return wrapper

@logging(level="INFO")
def do(work): pass

关于wrapt的使用,建议查阅官方文档,在此不在赘述。

  • http://wrapt.readthedocs.io/en/latest/quick-start.html
相关文章
相关标签/搜索