python装饰器详细剖析


  装饰器函数其实是这样一个接口约束,它 必须接受一个 callable 对象作为参数,然后返回一个 callable 对象,其作用就是为已经存在的函数或对象添加额外的功能。

函数装饰器

基本函数装饰器

  Talk is cheap, show me the code.所以,下面先给出一个最简单的例子,再来解释装饰器原理。

def log_it(func):
    def wrapper(*args, **kwargs):
        print("[Debug]: enter function {}()".format(func.__name__))
        return func(*args, **kwargs)  # if function func has return, remember to return

    return wrapper


@log_it
def add_number(*args):
    return sum(args)


print(add_number(1, 2, 3, 4))
print(add_number.__name__)

# 输出
[Debug]: enter function add_number()
10
wrapper

  细心的你会发现,print(add_number.__name__)输出的是wrapper,意味着装饰器的实际意思就是 add_number = log_it(add_number),所以执行add_number()函数就相当于执行wrapper()函数,而这也就是装饰器的原理啦。即通过传入一个函数将其包装成一个新的函数,赋予它额外的功能。

传参函数装饰器

  装饰器还有更大的灵活性,例如带参数的装饰器,比如上面的例子中,你想控制print语句的内容是动态的,那么你可以给它传入参数,代码如下:

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


@log_it2(level='Info')
def add_number2(*args):
    return sum(args)


print(add_number2(1, 2, 3, 4, 5))

# 输出
[Info]: enter function add_number2()
15

  看到这里我估计有朋友要晕了,因为这个装饰器居然嵌套了三个函数定义,什么鬼?是不是瞬间感觉还不如学习C++,根本不存在函数嵌套,哈哈~~接下来来仔细分析一下:

  1. 首先我们要清楚相比第一个例子,我们多加了一层函数嵌套,且这一层函数嵌套就是也仅仅是为了给装饰器传递参数,那么不难想到add_number2.__name__应该是inner_wrapper(不信你可以输出看一看);
  2. 基于第1点,我们应该可以推理出它的实际调用是 add_number2 = log_it2(debug='info')(add_number2)

, 从而说明add_number2.__name__的值就是inner_wrapper.

类装饰器

 类装饰器和函数装饰器一样,包括基本类装饰器和传参类装饰器两种类型,但是其形式略有不同,类装饰器必须实现魔法方法__call__函数,关于__call__函数以及更多魔法方法的用法请参考Python面向对象、魔法方法

基本类装饰器

  示例代码如下:

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


@LogIt
def add_number(*args):
    return sum(args)


print(add_number(1, 2, 3, 4))

  我上面说形式上略有不同,可这一看,好像却是大有不同,这又是怎么实现的呢?首先LogIt类实现了构造方法__init__(self,func),它接收一个函数,这看起来很正常;但是,它还是实现了__call__方法并返回了函数func,这不正是我们开篇说的,接受一个函数并返回一个函数吗?这就是它的实现原理。我们可以验证一下:

>>>add_number
<__main__.LogIt object at 0x10e4dd7f0>

  看上面的输出,add_number变成一个函数了,所以我们不难想象,它实际过程是 add_number = LogIt(add_number),如此一来其自然而然就是LogIt的一个实例了。

传参类装饰器

 那么类装饰器要怎么传递参数呢?请看下面的示例代码:

class LogIt2(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__))
            return func(*args, **kwargs)
        return wrapper


@LogIt2(level='INFO')
def add_number2(*args):
    return sum(args)


print(add_number2(1, 2, 3, 4, 5))

  如果你融会贯通了上面所蕴含的思想,应该不难推理出它的包装过程就是:add_number2 = LogIt('INFO')(add_number2),所以add_number2此时应该是实际上是wrapper函数。怎么知道我说的是对的呢?

>>> add_number2.__name__
wrapper

  以上就是装饰器的一些基本使用了,多看几遍,多实践几次,你一定可以看懂!

装饰器执行顺序

  以上我们只讨论了一个装饰器的使用,如果同时使用多个装饰器,会是怎样的执行流程呢?我们先直接上一个例子,看一看:

def decorator_a(func):
    print('Get in decorator_a')

    def inner_a(*args, **kwargs):
        print('Get in inner_a')
        return func(*args, **kwargs)

    return inner_a


def decorator_b(func):
    print('Get in decorator_b')

    def inner_b(*args, **kwargs):
        print('Get in inner_b')
        return func(*args, **kwargs)

    return inner_b


@decorator_b
@decorator_a
def f(x):
    print('Get in f')
    return x * 2


res = f(1)
print('res:', res)

# 输出结果
# Get in decorator_a
# Get in decorator_b
# Get in inner_b
# Get in inner_a
# Get in f
# res: 2

  如果你是第一次看装饰器,且你的预想中也是这个输出,那么恭喜你,你一定是996ICU友情链接的天选之人;如果你看的目瞪口呆,其实也不要紧,只要你有995ICU友情链接2的精神,你也可以是王者。好吧,回到正题,其实关于多装饰器执行顺序的规则就是从里到外顺序执行,即最先调用最里层的装饰器,最后调用最外层的装饰器。其调用过程为:

f = decorator_b(decorator_a(f))

且最后f的形式为:

# 注意下面不是可执行代码,缩进是为了表示逻辑关系
def inner_b(*args, **kwargs):
  print('Get in inner_b')
  	# -----------inner_a-------------
  	print('Get in inner_a')
  		# -----------f-------------
  		print('Get in f')
  		return x * 2

  我觉得,如果你看懂了上面这一段"伪代码",那么你就真的理解了装饰器原理。如果没看懂,不要紧,试着多看几遍就好;就好比心情不好,那就去吃一顿火锅,如果不够,那就再来一顿!

内置装饰器

  在python中有以下几个常见的内置装饰器,它们都和python面向对象编程有关,下面分别做简要介绍:

@abstractmethod

  这是python中抽象类"虚方法"的定义方式,一个类中如果存在@abc.abstractmethod装饰的方法,那么其不可以实例化对象,且继承的子类必须实现@abc.abstractmethod装饰的方法。

import abc
class A(object):
    __metaclass__ = abc.ABCMeta

    @abc.abstractmethod
    def load(self, _input):
        pass

    @abc.abstractmethod
    def save(self, output, data):
        pass


class B(A):
    def load(self, _input):
        return _input.read()

    def save(self, output, data):
        return output.write(data)


if __name__ == '__main__':
    print(issubclass(B, A))
    print(isinstance(B(), A))
    print(A.__subclasses__())
    
# 输出
True
True
[<class '__main__.B'>]

@property

  类属性有三个装饰器: setter , getter , deleter,它们都是在 property () 的基础上做了一些封装,其中getter 装饰器和不带 getter 的属性装饰器效果是一样的。该特性最重要的功能之一就是能实现属性的参数检验。

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
    @score.deleter
    def score(self):
        del self._score

s = Student()
s.score = 10
print(s.score)

@classmethod

  被@classmethod装饰的函数不需要实例化,不需要 self 参数,但第一个参数需要是表示自身类的 cls 参数,可以来调用类的属性,类的方法,实例化对象等。

class A(object):

    # 属性默认为类属性(可以给直接被类本身调用)
    num = "类属性"

    # 实例化方法(必须实例化类之后才能被调用)
    def func1(self): # self : 表示实例化类后的地址id
        print("func1")
        print(self)

    # 类方法(不需要实例化类就可以被类本身调用)
    @classmethod
    def func2(cls):  # cls : 表示没用被实例化的类本身
        print("func2")
        print(cls)
        print(cls.num)
        cls().func1()

    # 不传递传递默认self参数的方法(该方法也是可以直接被类调用的,但是这样做不标准)
    def func3():
        print("func3")
        print(A.num) # 属性是可以直接用类本身调用的
    
# A.func1() 这样调用是会报错:因为func1()调用时需要默认传递实例化类后的地址id参数,如果不实例化类是无法调用的
A.func2()
A.func3()

@staticmethod

  被@staticmethod修饰的方法是静态方法,静态方法的参数可以根据业务需求传入,没有固定参数;然而前面的实例化方法第一个参数必须是self,类方法第一个参数必须是cls。静态方法,跟普通函数没什么区别,与类和实例都没有所谓的绑定关系,它只不过是碰巧存在类中的一个函数而已,不论是通过类还是实例都可以引用该方法。之所以将其放在类中,是因为该方法仅为这个类服务。

class Method(object):

    def __init__(self, data):
        pass

    @staticmethod
    def static_method():
        print "This is static method in class Method"

内置装饰器小结

  为了更好的理解辨明实例方法、类方法、静态方法、抽象方法,我再举一个例子,从本质上来理一理它们之间的关系:

import abc
class ICU996():

    @staticmethod
    def static_m(self):
        pass
        
    @classmethod
    def class_m(cls):
        pass

    def instance_m(self):
        pass

    @abc.abstractmethod
    def abstract_m(self):
        pass


>>>icu = ICU996()
>>>icu.static_m
<function ICU996.static_m at 0x11af930d0>
>>>icu.class_m
<bound method ICU996.class_m of <class 'test.ICU996'>>
>>>icu.instance_m
<bound method ICU996.instance_m of <test.ICU996 object at 0x11af54978>>
>>>icu.abstract_m
<bound method ICU996.abstract_m of <test.ICU996 object at 0x11af54978>>

  从上面的输出结果不难看出,实例化方法和抽象方法本质上是一样的,都是绑定在实例上的方法;而类方法这是绑定在类上的方法;静态方法则实质上就是一个不同函数。

装饰器属性还原

  最后,我们来解决前面的出现的一个问题,在第一个实例中,我们发现被装饰的函数add_number.__name__变成了wrapper,那么如果我们并不想让这样的事情发生,我们该怎么做呢?其实,这一点,可以用内置的装饰器wraps来实现:

from functools import wraps


def log_it(func):
    @wraps(func)  # comment this line to see the diff
    def wrapper(*args, **kwargs):
        print("[Debug]: enter function {}()".format(func.__name__))
        return func(*args, **kwargs)
    return wrapper


@log_it
def add_number(*args):
    """
    add numbers in tuple
    :param args: should be numbers
    :return: the sum of tuple
    """
    return sum(args)


print(add_number.__name__, add_number.__doc__)
# 输出
add_number 
    add numbers in tuple
    :param args: should be numbers
    :return: the sum of tuple

 它不仅可以使函数名保持不变,还可以保持函数原有的doc string。好,那么是不是应该思考一下它是怎样实现的呢?

from functools import partial

WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
                       '__annotations__')
WRAPPER_UPDATES = ('__dict__',)


def update_wrapper(wrapper,
                   wrapped,
                   assigned=WRAPPER_ASSIGNMENTS,
                   updated=WRAPPER_UPDATES):
    for attr in assigned:
        try:
            value = getattr(wrapped, attr)
        except AttributeError:
            pass
        else:
            setattr(wrapper, attr, value)
    for attr in updated:
        getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
    # Issue #17482: set __wrapped__ last so we don't inadvertently copy it
    # from the wrapped function when updating __dict__
    wrapper.__wrapped__ = wrapped
    # Return the wrapper so this can be used as a decorator via partial()
    return wrapper


def wraps(wrapped,
          assigned=WRAPPER_ASSIGNMENTS,
          updated=WRAPPER_UPDATES):
    return partial(update_wrapper, wrapped=wrapped,
                   assigned=assigned, updated=updated)

  这是python官方的实现方式,关于__module__,__name__等特殊属性,请参考python特殊属性、魔法方法;这里包装的过程简化来看就是add_number = update_wrapper(wrapper, add_number),其中update_wrapper的功能是更新wrapper函数的一些特殊属性。

写在篇后

  装饰器是python的一大难点,它本质上就是一个函数,它可以让其他函数在不需要变动的情况下增加额外的功能。装饰器常用于有切面的应用场景,如插入日志、性能测试等场景。多看、多试、多用,装饰器,其实也不难。

猜你喜欢

转载自blog.csdn.net/jeffery0207/article/details/89042654