通俗的理解Python装饰器所有用法(Decorator)

前言

Python的底层代码,以及各种第三方框架中,你会看到各种各样的@符号,没错,他就是Python的装饰器语法糖。

Python装饰器看起来类似Java中的注解,OC中的Aspect框架,亦或是理解为OC中Runtime的Hook操作,然鹅只是看起来而已。Python是通过@语法糖里面的闭包来实现,iOS是Runtime底层交换方法来实现,再不改原先逻辑的情况下,在方法之前嵌入自己的逻辑,例如日志,统计,预处理,清理,校验等场景。Django中底层代码大量用到了装饰器,广泛应用于缓存、权限校验(如django中的@login_required和@permission_required装饰器)

用法

用法很简单,就三个步骤:

  • 先定义一个装饰函数(可以是类,可以是函数)
  • 在定义你的业务函数,或者类
  • 最后把装饰器装到你的业务函数头上
    根据上面的三个步骤,看看装饰器的所有用法

闭包

首先介绍下闭包,应该都懂,危机百科的解释:

在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。

官方就是不说人话,需要通俗的来介绍下,其实就是OC中的Block,看看Python中的存在形式

# 外部包裹
def decration():
    para = 'I am closure'

    # 嵌套一层 形成闭包
    def wrapper():
        print(para)
    return wrapper

# 获取一个闭包
closure =  decration()

# 执行
closure()

para参数是局部变量,在decration执行后就被回收了。但是嵌套函数引用了这个变量,将局部变量封闭在嵌套函数中,形成闭包。

闭包就是引用自由变量的函数,这个函数保存了执行的上下文,可以脱离原本的作用于存在。

01.入门用法(不带参数)

单个装饰器

def logger(func):
    # args 元祖 ()  kwargs 字典 {} 关键字参数
    def wrapper(*args, **kwargs):
        print('我正在进行计算: %s 函数:'%(func.__name__))
        print('args = {}'.format(*args))
        print('args is ', args)
        print('kwargs is ', kwargs)
        result = func(*args, **kwargs)
        print('搞定,晚饭加个鸡蛋')
        return result

    return wrapper


@logger
def add(a, b, x = 0):
    print("%s + %s = %s" % (a, b, a + b))
    return a + b

@logger
def multpy(a, b, x = 0):
    print("%s * %s = %s" % (a, b, a * b))
    return a * b

print(add(100, 200,x = 1))

print("*"*30)

print(multpy(10, 200))

/Users/mikejing191/Desktop/Python3Demo/venv/bin/python /Users/mikejing191/Desktop/Python3Demo/Demo5.py
我正在进行计算: add 函数:
args = 100
args is  (100, 200)
kwargs is  {'x': 1}
100 + 200 = 300
搞定,晚饭加个鸡蛋
300
******************************
我正在进行计算: multpy 函数:
args = 10
args is  (10, 200)
kwargs is  {}
10 * 200 = 2000
搞定,晚饭加个鸡蛋
2000

对于初学者看到这个@语法会有些困扰,其实其实上面那段代码与下面的调用方式一样:

def add(a, b, x = 0):
    print("%s + %s = %s" % (a, b, a + b))
    return a + b

wrapper =  logger(add)
wrapper(100,200)

仔细看的话,其实原函数被装饰后,比如这个add已经被替换成wrapper的地址了,这样外部打印func.__name__就会变了,这种类似KVO,虽然被监听了,但是Apple把对应的实现隐藏了,不会暴露出新增的类kvo_xxxx,而会重写class方法返回原方法,这里Python也类似,这里下面会有一个方法来隐藏。

多个装饰器

def logger(func):
    # args 元祖 ()  kwargs 字典 {} 关键字参数
    print('日志装饰器')
    def wrapper_log(*args, **kwargs):
        print('我正在进行日志打印: %s 函数:'%(func.__name__))
        print('日志args is ', args)
        print('日志kwargs is ', kwargs)
        result = func(*args, **kwargs)
        print('日志搞定,晚饭加个鸡蛋')
        return result

    return wrapper_log


def statistics(func):
    print('统计装饰器')
    def wrapper_static(*args, **kwargs):
        print('我正在进行统计: %s 函数:'%(func.__name__))
        print('统计args is ', args)
        print('统计kwargs is ', kwargs)
        result = func(*args, **kwargs)
        print('统计搞定,晚饭加个鸡蛋')
        return result

    return wrapper_static


@logger
@statistics
def add(a, b):
    print("%s + %s = %s" % (a, b, a + b))
    return a + b


print(add(100, 200))


统计装饰器
日志装饰器
我正在进行日志打印: wrapper_static 函数:
日志args is  (100, 200)
日志kwargs is  {}
我正在进行统计: add 函数:
统计args is  (100, 200)
统计kwargs is  {}
100 + 200 = 300
统计搞定,晚饭加个鸡蛋
日志搞定,晚饭加个鸡蛋
300

和上面的单个装饰器类似,只是多叠加了一个,可以看到我们这里的loggerstatics上面,按正常理解,先装饰logger,再装饰statics,但是Python这里的规则是这样的:

根据日志分析下,首先编译器遇到@logger和@statistics,这里是会有代码执行的,比如两个装饰器的第一句打印,是在装饰器代码执行到就调用,不需要调用被装饰的函数。装饰的前提是装饰器的下一句代码是方法函数,才会装饰,因此先跳过@logger,然后@statistics就会对func函数进行装饰,因此先执行装饰statistics,然后返回的值就是wrapper_static函数,再执行装饰logger,执行的时候就是先执行装饰logger里面的inner函数,然后在执行装饰2里面的wrapper_log函数,好比一个东西,包装的时候由内到外,执行的时候由外到内,这就是多层装饰的逻辑

凑活看下画了个抽象的图,w1和w2分别代表logger和statistics,inner就是分别对应装饰器里面的闭包:
在这里插入图片描述
可以看到最终我们原函数的指针地址只想的是最外层logger的闭包函数地址。

注意点:这里的闭包函数返回的都是闭包,要等函数实际调用的时候才会触发,但是有些写法是不需要闭包的,比如Django中的Admin注册,这就有点不同,他会在装饰器执行到的时候直接触发内部代码,因此,你脑洞多大,装饰器的功能就有多大

from .models import BlogType, Blog

@admin.register(BlogType)
class BlogTypeAdmin(admin.ModelAdmin):
    list_display = ('type_name',)

# 装饰函数
def register(*models, site=None):
    """
    Register the given model(s) classes and wrapped ModelAdmin class with
    admin site:

    @register(Author)
    class AuthorAdmin(admin.ModelAdmin):
        pass

    The `site` kwarg is an admin site to use instead of the default admin site.
    """
    from django.contrib.admin import ModelAdmin
    from django.contrib.admin.sites import site as default_site, AdminSite

    def _model_admin_wrapper(admin_class):
        if not models:
            raise ValueError('At least one model must be passed to register.')

        admin_site = site or default_site

        if not isinstance(admin_site, AdminSite):
            raise ValueError('site must subclass AdminSite')

        if not issubclass(admin_class, ModelAdmin):
            raise ValueError('Wrapped class must subclass ModelAdmin.')

        admin_site.register(models, admin_class=admin_class)

        return admin_class
    return _model_admin_wrapper

02.进阶用法(带参数)

看完入门,应该对装饰器有个大概的了解,不过是不能接受参数的装饰器,这不搞笑呢,对应装饰器,只能执行固定的逻辑,不能被参数控制,这是不能忍的,而且你看过其他项目,可以看到大部分装饰器是带有参数的。
那么装饰器的传参如何实现,这个就需要多层嵌套了,看下实际案例:

def american():
    print("I am from America.")

def chinese():
    print("我来自中国。")

有个需求,给他们两根据不同国家,自动加上打招呼的功能。


def say_hello(contry):
    def wrapper(func):
        def inner_wrapper(*args, **kwargs):
            if contry == 'china':
                print('你好!')
            elif contry == 'america':
                print('Hello!')
            else:
                return
            return func(*args, **kwargs)
        return inner_wrapper
    return wrapper

@say_hello('america')
def american():
    print("I am from America.")


@say_hello('china')
def chinese():
    print("我来自中国。")


@say_hello('japanese')
def japanese():
    print('I am from jp')



american()
chinese()
japanese()

Hello!
I am from America.
你好!
我来自中国。

em。。。实属牛逼。。。。。。。。

但是又有点懵逼,包了一层,内部的wrapperfunc是怎么穿进去的?
其实去掉@语法,我们来恢复下调用逻辑:

def american():
    print("I am from America.")


decoration = say_hello('china')
wrapper =  decoration(american)
wrapper()

em。。。好像一点也不牛逼。。。。。。。。

装饰器这一语法体现了Python中函数是第一公民,函数是对象、是变量,可以作为参数、可以是返回值,非常的灵活与强大。

03.高阶用法(不带参数的类装饰器)

以上是基于函数实现的装饰器,在阅读别人的代码的时候,经常还能发现基于类实现的装饰器。

__call__内置函数

绝大多数装饰器都是基于函数和 闭包 实现的,但这并非制造装饰器的唯一方式。事实上,Python 对某个对象是否能通过装饰器( @decorator)形式使用只有一个要求:decorator 必须是一个“可被调用(callable)的对象。

class Foo():
    def __call__(self, *args, **kwargs):
        print('Hello Foo')

class Bar():
    pass

print(callable(Foo))
print(callable(Foo()))
print(callable(Bar))
print(callable(Bar()))
True
True
True
False

要实现基于类的装饰器,必须理解__call__内置函数的作用。

import sys
class MKJ(object):
    def __init__(self, name):
        super().__init__()
        self.name = name

    def __call__(self, *args, **kwargs):
        print('当前类名:%s'%self.__class__.__name__)
        print('当前函数名称:%s'%sys._getframe().f_code.co_name)
        print('当前参数:',args)
        
m =  MKJ('mikejing')
m('Faker', 'Deft')

当前类名:MKJ
当前函数名称:__call__
当前参数: ('Faker', 'Deft')

call()
官方定义:Called when the instance is “called” as a function; if this method is defined, x(arg1, arg2, …) is a shorthand for x.call(arg1, arg2, …).
它是在“实例被当成函数调用时”被调用。

举个例子,实例如果是m = MKJ(),那么,当你写下m()的时候,该实例(即m)的创建者MKJ类(注意:此处提到的创建者既有可能是类,也有可能是元类)中的__call__()被调用。
如果这个实例是一个类,那么它的创建者就是一个元类,如果这个实例是一个对象,那么它的创建者就是一个类。

明白了__call__的用法,就可以实现最基本的不带参数的类装饰器,代码如下:

import sys
class logger(object):
    def __init__(self, func):
        super().__init__()
        print('装饰类开始')
        self.func = func

    def __call__(self, *args, **kwargs):
        print('当前类名:%s'%self.__class__.__name__)
        print('当前函数名称:%s'%sys._getframe().f_code.co_name)
        print('装饰函数名称:%s' % self.func.__name__)
        print('当前参数:',args)
        self.func(*args, **kwargs)



@logger
def say(sm):
    print('say:%s'%sm)


say('hello!')

print(say) # <__main__.logger object at 0x108323160>

# 输出如下
装饰类开始
当前类名:logger
当前函数名称:__call__
装饰函数名称:say
当前参数: ('hello!',)
say:hello!

说明:

  1. 当我们把logger类作为装饰器的时候,首先会默认创建logger的实例,可以试试先不调用say('hello),可以看到logger实例的__init__方法被调用,被装饰的函数作为参数被传递进来。func变量指向了say的函数体。
  2. 此时say函数相当于重新指向了logger创建出来的实例对象的地址
  3. 当调用say()的时候,就相当于调用这个对象类的__call__方法
  4. 为了能够在__call__中调用会原来say函数,所以在__init__中需要一个实例变量保存原函数的引用,所有才有了self.func = func,从而在__cal__中取出原函数地址和参数,进行回调

印证的话可以打开这个装饰器关闭装饰器打印一下say看下函数和对象的转换

# 关闭
# @logger
def say(sm):
    print('say:%s'%sm)

print(say)
# 输入如下
<function say at 0x1011b8268>

# 打开
@logger
def say(sm):
    print('say:%s'%sm)

print(say)
# 输出如下
<__main__.logger object at 0x103233160>

04.高阶用法(带参数的类装饰器)

还是用上面的logger函数,由于日志可以分为很多级别infowarningdebug等类型的日志。这个时候就需要给类装饰器传入参数。回顾下函数装饰器,对于传参或者不传参,只是外部在包一层与否,整体逻辑没什么变化,但是如果类装饰器带参数,就和不带参就有很大不同了。

__init__:该方法不再接受装饰函数,而是接受传入参数。__call__:接受被装饰函数,实现装饰逻辑。

import sys
class logger(object):
    def __init__(self, level):
        super().__init__()
        print('装饰类开始')
        self.level = level

    def __call__(self, func):
        def wrapper(*args, **kwargs):
            print('[%s级别]--当前函数:'%(self.level),sys._getframe().f_code.co_name)
            print('[%s级别]--装饰函数名称:%s'%(self.level,func.__name__))
            print('[%s级别]--当前参数:'%(self.level), args)
            func(*args, **kwargs)
        return wrapper


@logger('WARNING')
def say(sm):
    print('say:%s'%sm)


say('Hello')

# 日志如下
装饰类开始
[WARNING级别]--当前函数: wrapper
[WARNING级别]--装饰函数名称:say
[WARNING级别]--当前参数: ('Hello',)
say:Hello

第二种带参数的类装饰器其实有点奇怪,__init__方法里面没有了func参数,其实按正常逻辑来看,理解起来其实不容易记忆,但是你强行记忆也行。em…

05.高阶用法(偏函数和类实现)

绝大部分装饰器都是基于函数和闭包来实现的,但是并非只此一种,看了上面的类装饰器,我们来实现一个与众不同,但是底层框架都大量使用的方式(类和偏函数实现),这种方式就是扩展的不带参数类函数装饰器。

import time
import functools
class DelayFunc:
    def __init__(self, durations, func):
        super().__init__()
        self.durations = durations
        self.func = func
        print('1111')

    def __call__(self, *args, **kwargs):
        print('please waite for %s seconds...'%self.durations)
        time.sleep(self.durations)
        return self.func(*args, **kwargs)


    def no_delay_call(self, *args, **kwargs):
        print('call immediately 。。。。')
        return self.func(*args, **kwargs)


def delay(durations):
    # Deley 装饰器,推迟某个函数执行,同时提供no_delay_call不等待调用
    # 此处为了避免额外函数,直接使用 functools.partial 帮助构造 具体参见另一个博客介绍,这里的作用我会在下面简单通俗介绍下
    return functools.partial(DelayFunc, durations)

@delay(3)
def add(a, b):
    return a + b

print(add(100,200))
# print(add.no_delay_call(200,300))

这里涉及到一种俗称偏函数的东西functools.partial,可以参见我的另一篇博客介绍,这里简单介绍下怎么理解。首先定义了一个类DelayFunc,做成装饰器的前提是callable也就是实现__call__方法,按不带参数的类装饰器,如果做成传参形式,上面有介绍,需要改动正常的类参数,现在按照此种方式进行扩展。定义一个函数deley,我们把它当做装饰器,类装饰器装饰其实把类实例化,可以看到deley函数返回的应该是一个类,这里能看到用到了functools.partial,该方法先理解为绑定DelayFunc类,暂时先绑定一个durations参数,那么我们看到初始化方法里面还有个参数是Func,这个就是我们最终装饰的时候自带的参数,所以当你看到以下使用的时候

@delay(3)
def add(a, b):
    return a + b

delay返回的绑定一半的类和参数,然后再传输add作为func进行实例化,此时add指向的不再是简单的函数地址,而是指向了新的类的实例。最终调用add(100,200)的时候执行__call__

至此,我们了解了函数装饰器,类装饰器的两种实现,分别有带参数和不带参数的区别。最后一种是偏函数实现的类装饰器,一共五种。

06.类装饰器的优势

那么类装饰器比函数装饰器有哪些优势:

  • 实现有状态的装饰器时,操作类属性比操作闭包内变量更符合直觉、不易出错

  • 实现为函数扩充接口的装饰器时,使用类包装函数,比直接为函数对象追加属性更易于维护

  • 更容易实现一个同时兼容装饰器与上下文管理器协议的对象(参考 unitest.mock.patch)

07.可选优化装饰器(使用 wrapt 第三方模块编写更扁平的装饰器)

  1. 实现带参数的装饰器时,层层嵌套的函数代码特别难写、难读
  2. 因为函数和类方法的不同,为前者写的装饰器经常没法直接套用在后者上

看一下生成随机数注入为函数参数的装饰器

import random


def random_number(min_num, max_num):
    def wrapper(func):
        def decorated(*args, **kwargs):
            num = random.randint(min_num, max_num)
            return func(num, *args, **kwargs)

        return decorated

    return wrapper


@random_number(0, 99)
def print_number(num):
    print(num)


print_number()

@random_number 装饰器功能看上去很不错,但它有着我在前面提到的两个问题:嵌套层级深、无法在类方法上使用。如果直接用它去装饰类方法,会出现下面的情况:

class Foo:
    @random_number(0, 99)
    def print_number(self, num):
        print(num)


print_number()
<__main__.Foo object at 0x10bdc12b0>

Foo类实例中的 print_number方法将会输出类实例 self ,而不是我们期望的随机数 num。

之所以会出现这个结果,是因为类方法(method)和函数(function)二者在工作机制上有着细微不同。如果要修复这个问题, random_number 装饰器在修改类方法的位置参数时,必须聪明的跳过藏在 *args 里面的类实例 self 变量,才能正确的将 num 作为第一个参数注入。

这时,就应该是 wrapt模块闪亮登场的时候了。 wrapt 模块是一个专门帮助你编写装饰器的工具库。利用它,我们可以非常方便的改造 random_number 装饰器,完美解决“嵌套层级深”和“无法通用”两个问题,

import random
import wrapt


def random_number(min_num, max_num):
    @wrapt.decorator
    def wrapper(wrapperd, instance, args, kwargs):
        # 参数含义:
        # - wrapped:被装饰的函数或类方法
        # - instance:
        #   - 如果被装饰者为普通类方法,该值为类实例
        #   - 如果被装饰者为 classmethod 类方法,该值为类
        #   - 如果被装饰者为类/函数/静态方法,该值为 None

        # - args:调用时的位置参数(注意没有 * 符号)
        # - kwargs:调用时的关键字参数(注意没有 ** 符号)
        num = random.randint(min_num, max_num)
        # 无需关注 wrapped 是类方法或普通函数,直接在头部追加参数
        args = (num, ) + args
        return wrapperd(*args, **kwargs)
    return wrapper


@random_number(0, 99)
def print_number(num):
    print(num)


class Foo:
    @random_number(0, 99)
    def print_number(self, num):
        print(num)


print_number()


Foo().print_number()

这就是使用了wrapt后的有点,如果不习惯,还是使用上述的一些装饰器即可

  1. 嵌套层级少:使用 @wrapt.decorator 可以将两层嵌套减少为一层
  2. 更简单:处理位置与关键字参数时,可以忽略类实例等特殊情况
  3. 更灵活:针对 instance 值进行条件判断后,更容易让装饰器变得通用

08.装饰类的装饰器

Python中单例的实现,有一种就是用单例实现的

instances = {}
def singleton(cls):
	def get_instance(*args, **kw):
		cls_name = cls.__name__
		print('===== 1 ====')
		if not cls_name in instances:
			print('===== 2 ====')
			instance = cls(*args, **kw)
			instances[cls_name] = instance
		return instances[cls_name]
	return get_instance

@singleton
class User:
	_instance = None
	def __init__(self, name):
		print('===== 3 ====')
		self.name = name
u1 = User('mkj1')
u1.age = 100
u2 = User('mkj2')
print(u1 == u2)
print(u2.age)
# 日志如下
===== 1 ====
===== 2 ====
===== 3 ====
===== 1 ====
True
100

09.wraps 装饰器有啥用

上面介绍了多种装饰器,而且还引入了functools库,除了用到partitial函数,还有个装饰器wraps,看看到底有啥用。


def say_hello(contry):
    def wrapper(func):

        def inner_wrapper(*args, **kwargs):
            if contry == 'china':
                print('你好!')
            elif contry == 'america':
                print('Hello!')
            else:
                return
            return func(*args, **kwargs)
        return inner_wrapper
    return wrapper


@say_hello('china')
def american():
    print("I am from America.")


print(american.__name__)
american()

# 打印日志
inner_wrapper
你好!
I am from America.

可以看到,按我们上面的分析,其实american已经不再指向原来的函数地址,因此打印出来的名字也变了。理论上没什么问题,但是有时候你定位Bug的时候会很恶心,因此,我们会看到大量的库用到了系统提供的wraps装饰器

import functools

def say_hello(contry):
    def wrapper(func):
        @functools.wraps(func)
        def inner_wrapper(*args, **kwargs):
            if contry == 'china':
                print('你好!')
            elif contry == 'america':
                print('Hello!')
            else:
                return
            return func(*args, **kwargs)
        return inner_wrapper
    return wrapper


@say_hello('china')
def american():
    print("I am from America.")


print(american.__name__)
american()
# 打印如下
american
你好!
I am from America.

方法是使用 functools .wraps 装饰器,它的作用就是将 被修饰的函数(american) 的一些属性值赋值给 修饰器函数(inner_wrapper) ,最终让属性的显示更符合我们的直觉。

准确的来看,functools .wraps也是一个偏函数对象partial

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

可以看到该装饰器也是和我们上面演示的一样,使用了partial偏函数,其中绑定的类或者方法是update_wrapper,其中该方法实际上接收四个参数,这里我们传了三个,用作装饰器,默认会把第四个参数,被装饰的函数inner_wrapper作为wrapper首参数进行装饰初始化。

wrapper.__wrapped__ = wrapped

底层实现中会把原函数的属性全部赋值给修饰器函数inner_wrapper,最终调用__name__的时候,虽然指针被已经指向被装饰的函数,但是通过再次装饰,属性会被原函数一样打印出来。

10.装饰器实战

1.装饰器使我们的代码可读性更高
2.代码结构更加清晰,代码冗余降低

下面是一个实现控制函数运行超时的装饰器,如果超时,就会抛出异常。

import signal
import functools

class TimeoutException(Exception):
    def __init__(self, error='Timeout waiting for response from Cloud'):
        Exception.__init__(self, error)


def timeout_limit(timeout_time):
    def wraps(func):

        def handler(signum, frame):
            raise TimeoutException()

        @functools.wraps(func)
        def deco(*args, **kwargs):
            signal.signal(signal.SIGALRM, handler)
            signal.alarm(timeout_time)
            return func(*args, **kwargs)
            signal.alarm(0)
        return deco
    return wraps


@timeout_limit(1)
def add(x):
    r = 0
    for a in range(0, x):
        r += a
    return r

print(add.__name__)
print(add(10))

print(add(100000000))

该功能可以看到执行add(10)的时候正常输出,但是执行add(10000000)的时候由于超时,就会抛出异常崩溃,实现了我们给函数进行装饰的功能。

总结:

  1. 装饰器实现方式很多种,除了常用的函数+闭包,也可以用类来装饰,概括为一切callable的对象都可以被用来实现装饰器
  2. 混合使用函数和类,能更好的实现装饰器
  3. 装饰器只是一种语法糖,你也可以自己拆开来一步步写,它不是装饰器模式。
  4. 装饰器会改变原函数的所有信息(例如签名),我们需要functools.wraps来进行交换回去
  5. 类装饰器带参数的可以用偏函数partial来实现更优雅的方案,推荐使用

参考文献:
Python 工匠:使用装饰器的技巧
搞懂装饰器所有用法
Python中*args 和**kwargs的用法

©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页