
本文共 15763 字,大约阅读时间需要 52 分钟。
前言
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@loggerdef add(a, b, x = 0): print("%s + %s = %s" % (a, b, a + b)) return a + b@loggerdef multpy(a, b, x = 0): print("%s * %s = %s" % (a, b, a * b)) return a * bprint(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 = 100args is (100, 200)kwargs is { 'x': 1}100 + 200 = 300搞定,晚饭加个鸡蛋300******************************我正在进行计算: multpy 函数:args = 10args 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 + bwrapper = 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_logdef 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@statisticsdef add(a, b): print("%s + %s = %s" % (a, b, a + b)) return a + bprint(add(100, 200))统计装饰器日志装饰器我正在进行日志打印: wrapper_static 函数:日志args is (100, 200)日志kwargs is { }我正在进行统计: add 函数:统计args is (100, 200)统计kwargs is { }100 + 200 = 300统计搞定,晚饭加个鸡蛋日志搞定,晚饭加个鸡蛋300
和上面的单个装饰器类似,只是多叠加了一个,可以看到我们这里的logger
在statics
上面,按正常理解,先装饰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。。。实属牛逼。。。。。。。。
但是又有点懵逼,包了一层,内部的wrapper
的func
是怎么穿进去的?
@
语法,我们来恢复下调用逻辑: 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(): passprint(callable(Foo))print(callable(Foo()))print(callable(Bar))print(callable(Bar()))TrueTrueTrueFalse
要实现基于类的装饰器,必须理解__call__
内置函数的作用。
import sysclass 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 sysclass 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)@loggerdef say(sm): print('say:%s'%sm)say('hello!')print(say) # <__main__.logger object at 0x108323160># 输出如下装饰类开始当前类名:logger当前函数名称:__call__装饰函数名称:say当前参数: ('hello!',)say:hello!
说明:
- 当我们把
logger
类作为装饰器的时候,首先会默认创建logger的实例,可以试试先不调用say('hello)
,可以看到logger实例的__init__
方法被调用,被装饰的函数作为参数被传递进来。func
变量指向了say
的函数体。 - 此时say函数相当于重新指向了logger创建出来的实例对象的地址
- 当调用
say()
的时候,就相当于调用这个对象类的__call__
方法 - 为了能够在
__call__
中调用会原来say
函数,所以在__init__
中需要一个实例变量保存原函数的引用,所有才有了self.func = func
,从而在__cal__
中取出原函数地址和参数,进行回调
印证的话可以打开这个装饰器关闭装饰器打印一下say
看下函数和对象的转换
# 关闭# @loggerdef say(sm): print('say:%s'%sm)print(say)# 输入如下# 打开@loggerdef say(sm): print('say:%s'%sm)print(say)# 输出如下<__main__.logger object at 0x103233160>
04.高阶用法(带参数的类装饰器)
还是用上面的logger
函数,由于日志可以分为很多级别info
,warning
,debug
等类型的日志。这个时候就需要给类装饰器传入参数。回顾下函数装饰器,对于传参或者不传参,只是外部在包一层与否,整体逻辑没什么变化,但是如果类装饰器带参数,就和不带参就有很大不同了。
__init__
:该方法不再接受装饰函数,而是接受传入参数。__call__
:接受被装饰函数,实现装饰逻辑。
import sysclass 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 timeimport functoolsclass 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 + bprint(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 第三方模块编写更扁平的装饰器)
- 实现带参数的装饰器时,层层嵌套的函数代码特别难写、难读
- 因为函数和类方法的不同,为前者写的装饰器经常没法直接套用在后者上
看一下生成随机数注入为函数参数的装饰器
import randomdef 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 randomimport wraptdef 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
后的有点,如果不习惯,还是使用上述的一些装饰器即可
- 嵌套层级少:使用 @wrapt.decorator 可以将两层嵌套减少为一层
- 更简单:处理位置与关键字参数时,可以忽略类实例等特殊情况
- 更灵活:针对 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@singletonclass User: _instance = None def __init__(self, name): print('===== 3 ====') self.name = nameu1 = User('mkj1')u1.age = 100u2 = User('mkj2')print(u1 == u2)print(u2.age)# 日志如下===== 1 ========= 2 ========= 3 ========= 1 ====True100
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 functoolsdef 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 signalimport functoolsclass 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 rprint(add.__name__)print(add(10))print(add(100000000))
该功能可以看到执行add(10)
的时候正常输出,但是执行add(10000000)
的时候由于超时,就会抛出异常崩溃,实现了我们给函数进行装饰的功能。
总结:
- 装饰器实现方式很多种,除了常用的函数+闭包,也可以用类来装饰,概括为一切callable的对象都可以被用来实现装饰器
- 混合使用函数和类,能更好的实现装饰器
- 装饰器只是一种语法糖,你也可以自己拆开来一步步写,它不是装饰器模式。
- 装饰器会改变原函数的所有信息(例如签名),我们需要
functools.wraps
来进行交换回去 - 类装饰器带参数的可以用偏函数
partial
来实现更优雅的方案,推荐使用
参考文献:
发表评论
最新留言
关于作者
