在Python中装饰器的作用毋庸置疑,他能有效解决代码复用等问题,最重要的是他的方式非常类似面向切面编程。装饰器要解决的问题是如日志记录、用户认证等,当然更不仅限于此。
在讲装饰器前,我们要理解下Python语言的一个特点,那就是在Python里所有的都是对象,包括函数和类。只有在函数和类是对象的前提下,我们才能使用装饰器装饰函数和类。
1. 装饰器语法
下面我们编写一个简单的装饰器:
def decorator(method):
method.__doc__ += '\n这是个被装饰器装饰过的函数!'
return method
这是一个简单的装饰器, 它的作用仅仅是给被装饰函数的文档加上一句话,我们先用它装饰一个简单的函数:
@decorator
def foo(x, y):
""" 这是个foo函数 """
return x + y
装饰器的语法类似java中的注解,在函数上方写上"@decorator"。foo函数被装饰后,我们看看它的文档_doc_有什么变化:
print(foo.__doc__)
这是个foo函数
这是个被装饰器装饰过的函数!
一个函数或类可以被多个装饰器装饰,顺序是由下向上装饰。如下面的函数被装饰顺序依次@decorator_a-->@decorator_b-->@decorator_c。
@decorator_c
@decorator_b
@decorator_a
def foo():
pass
2. 装饰器的标准写法
有人也许看出来,装饰器有点像是代理方法。确实,它的一些方面确实和代理方法很像,比如装饰器会改变原有函数的属性和方法,返回的可能是变样的函数。看下面的例子:
def base_decorator(method):
def inner(*args, **kwargs):
# 判断传入的参数是否全为int类型
args_list = list(args) + [i for i in kwargs.values()]
for arg in args_list:
if not isinstance(arg, int):
raise ValueError('传入参数不全为int类型')
return method(*args, **kwargs)
return inner
@base_decorator
def foo(x, y, z):
return x + y + z
help(foo)
输出:inner(*args, **kwargs)
我们发现help(foo)打印出的是inner(*args, **kwargs),而不是foo(x, y, z),被装饰的函数像是变成了另一个函数,有时候这并不是我们想要要的,要解决这个问题,python官方给出了一个解决方案,使用functools库中的wraps装饰器,使用方法如下:
from functools import wraps
def base_decorator(method):
@wraps(method)
def inner(*args, **kwargs):
# 判断传入的参数是否全为int类型
args_list = list(args) + [i for i in kwargs.values()]
for arg in args_list:
if not isinstance(arg, int):
raise ValueError('传入参数不全为int类型')
return method(*args, **kwargs)
return inner
@base_decorator
def foo(x, y, z):
return x + y + z
help(foo)
输出:foo(x, y, z)
我们发现,wraps函数本身也是一个带参装饰器(后面会讲带参),参数是被装饰的函数,具体wraps做了什么工作,有兴趣的朋友可以自行查找资料看看。
从上面的装饰器我们可以看出它的强大,它可以对被装饰函数的参数进行操作。仔细理解上面的装饰器的写法,这是我们日常比较常用的一种写法。
3. 带参装饰器
上面我们注意到wraps装饰器是一个带参的装饰器,那怎么写一个根据传入的参数来动态装饰函数的装饰器呢?答案很简单,我们看以下的例子:
from functools import wraps
from json import dumps
def json_output(indent=None, sort_keys=False):
"""根据需要对被装饰函数的结果进行json化输出"""
def decorator(method):
@wraps(method)
def inner(*args, **kwargs):
result = method(*args, **kwargs)
return dumps(result, indent=indent, sort_keys=sort_keys)
return inner
return decorator
@json_output(indent=4)
def do_something():
return {
'code': 'success'
}
print(do_something())
输出:{
"code": "success"
}
这个带参装饰器的作用是可以根据传入的参数来动态输出json的不同格式。
细心的同学可能已经发现,这个装饰器其实只是在原有的装饰器上再套一个函数,确实如此,这里涉及到一个Python解析代码的方式。Python在解析被装饰函数的时候,会先执行 json_output(indent=4) 这个函数,它会返回一个装饰器 decorator ,再用decorator装饰被装饰的函数。
为了向后兼容,也为了有时候可能不需要参数,不传参时上面的写法会报错。将上面的json_output装饰器修改如下:
def json_output(method=None, indent=None, sort_keys=False):
# 判断传入参数是否正确,方法和其他参数传入时报错
if method and (indent or sort_keys):
raise RuntimeError('传入参数错误!')
# 以下为普通装饰器
def decorator(func):
@wraps(func)
def inner(*args, **kwargs):
result = method(*args, **kwargs)
return dumps(result, indent=indent, sort_keys=sort_keys)
return inner
# 此处根据传入的参数有没有method判断是带参装饰器还是普通装饰器
if method:
return decorator(method)
else:
return decorator
上面这个装饰器,即可以传参也可不传,这里有点复杂,可以只理解写法。
4. 类装饰器
装饰器不仅可以装饰函数,也可以装饰类,也就是类装饰器。
类装饰器的写法如下:
def class_decorator(cls):
"""
类装饰器,可以用于改变类的基础属性和装饰类方法
"""
# 保存cls的原有init方法
original_init = cls.__init__
# 装饰cls的init方法
@wraps(original_init)
def decorate_init_(self, *args, **kwargs):
# 给类对象加一个创建时间属性
self.create_time = time()
original_init(self, *args, **kwargs)
cls.__init__ = decorate_init_
return cls
# test
@class_decorator
class DecorateCls:
pass
d = DecorateCls()
print(d.create_time)
1524624846.831774
class_decorator装饰器只是简单的修改被装饰类的_init_方法,在它创建实例的时候为实例加上一个create_time的属性。
类装饰器的作用可以是添加属性,装属性参数化,或是修改一个类的API,从而使类被声明的方式与实例被使用的方式不同。
5. 类型转换
装饰器的一个更高级的用法是装饰一个函数,运行这个被装饰函数后返回一个类。
想象一个场景,我们已经有一个较完备的类,可我们要修改这个类的一个方法,当然我们可以用继承来实现,可有时候我们也可以用装饰器来实现,看如下的例子:
class Task:
def run(self, *args, **kwargs):
raise NotImplementedError('未实现run方法')
def identify(self):
return '我是一个Task类'
def task_decorator(method):
class TaskSubclass(Task):
def run(self, *args, **kwargs):
return method(*args, **kwargs)
return TaskSubclass
task_decorator装饰器返回的是个Task的子类,这个子类实现了run方法,而实际上run方法执行的就是被装饰函数,我们可以简单的只写一个函数却返回一个完整的实现特定方法的类。
上面的写法存在一个问题,看下面示例:
@task_decorator
def foo():
print('i am a foo')
我们给foo函数上加装饰器后,执行foo()时返回的是一TaskSubclass实例,它并不会马上执行print('i am a foo')这句代码,只我们执行foo().run()时,才会执行foo函数里的代码,这与我们的直觉或是惯用方法有点不符。
但我们也有解决方案,只是相对写得复杂一点,将装饰器修改如下:
class Task:
def __call__(self, *args, **kwargs):
return self.run(*args, **kwargs)
def run(self, *args, **kwargs):
raise NotImplementedError('未实现run方法')
def identify(self):
return '我是一个Task类'
def task_decorator(method):
class TaskSubclass(Task):
def run(self, *args, **kwargs):
return method(*args, **kwargs)
return TaskSubclass()
注意看修改后与修改前的区别,类Task加入了一个_call_方法,加入该方法后,输入Task()()时就会执行_call_方法。而我们的task_decorator装饰器则直接返回TaskSubclass()实例。这时我们再用执行foo()时,就会正常执行foo函数里的代码。
对于一些朋友来说,装饰器的语法可能有些不合常理,比如装饰器往往返回一个内构的函数的名称,而不是执行这个内构函数后的结果,有时候返回的是一个类,而不是类的实例,这中间的问题就在于,在Python里,所有的这一切都是对象,在理解了这句话后,再去看之前的装饰器,可能会有不同的体验。