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
执行后就被回收了。但是嵌套函数引用了这个变量,将局部变量封闭在嵌套函数中,形成闭包。
闭包就是引用自由变量的函数,这个函数保存了执行的上下文,可以脱离原本的作用于存在。
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
和上面的单个装饰器类似,只是多叠加了一个,可以看到我们这里的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
看完入门,应该对装饰器有个大概的了解,不过是不能接受参数的装饰器,这不搞笑呢,对应装饰器,只能执行固定的逻辑,不能被参数控制,这是不能忍的,而且你看过其他项目,可以看到大部分装饰器是带有参数的。
那么装饰器的传参如何实现,这个就需要多层嵌套了,看下实际案例:
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中函数是第一公民,函数是对象、是变量,可以作为参数、可以是返回值,非常的灵活与强大。
以上是基于函数实现的装饰器,在阅读别人的代码的时候,经常还能发现基于类实现的装饰器。
绝大多数装饰器都是基于函数和 闭包 实现的,但这并非制造装饰器的唯一方式。事实上,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!
说明:
logger
类作为装饰器的时候,首先会默认创建logger的实例,可以试试先不调用say('hello)
,可以看到logger实例的__init__
方法被调用,被装饰的函数作为参数被传递进来。func
变量指向了say
的函数体。say()
的时候,就相当于调用这个对象类的__call__
方法__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>
还是用上面的logger
函数,由于日志可以分为很多级别info
,warning
,debug
等类型的日志。这个时候就需要给类装饰器传入参数。回顾下函数装饰器,对于传参或者不传参,只是外部在包一层与否,整体逻辑没什么变化,但是如果类装饰器带参数,就和不带参就有很大不同了。
__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…
绝大部分装饰器都是基于函数和闭包来实现的,但是并非只此一种,看了上面的类装饰器,我们来实现一个与众不同,但是底层框架都大量使用的方式(类和偏函数实现),这种方式就是扩展的不带参数类函数装饰器。
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__
至此,我们了解了函数装饰器,类装饰器的两种实现,分别有带参数和不带参数的区别。最后一种是偏函数实现的类装饰器,一共五种。
那么类装饰器比函数装饰器有哪些优势:
实现有状态的装饰器时,操作类属性比操作闭包内变量更符合直觉、不易出错
实现为函数扩充接口的装饰器时,使用类包装函数,比直接为函数对象追加属性更易于维护
更容易实现一个同时兼容装饰器与上下文管理器协议的对象(参考 unitest.mock.patch)
看一下生成随机数注入为函数参数的装饰器
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
后的有点,如果不习惯,还是使用上述的一些装饰器即可
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
上面介绍了多种装饰器,而且还引入了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__
的时候,虽然指针被已经指向被装饰的函数,但是通过再次装饰,属性会被原函数一样打印出来。
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)
的时候由于超时,就会抛出异常崩溃,实现了我们给函数进行装饰的功能。
functools.wraps
来进行交换回去partial
来实现更优雅的方案,推荐使用参考文献:
Python 工匠:使用装饰器的技巧
搞懂装饰器所有用法
Python中*args 和**kwargs的用法