1.介绍
Python装饰器在开发过程中,有着较为重要的地位,但是对于初学者来说,并不便于理解,本文将带着大家分析python装饰器的使用。
2.定义
装饰器本质上就是一个函数,这个函数接受其他函数作为参数,并将其以一个新的修改后的函数作为替换。
概念较为抽象,我们来考虑如下一个场景,现在我们需要对用户年龄进行认证,如果年龄小于18,则给出提示,年龄不符合要求(嘿嘿嘿,大家都懂)。代码如下:
class Movie(object):def get_movie(self,age):if age<18:raise Exception('用户年龄不符合要求')return self.moviedef set_movie(self,age,movie):if age <18:raise Exception('用户年龄不符合要求')self.movie = movie复制代码
考虑到复用性的问题,我们对其修改:
def check_age(age):
if age < 18:raise Exception('用户年龄不符合要求')class User(object):def get_movie(self, age):check_age(age)return self.moviedef set_movie(self, age, movie):check_age(age)self.movie = movie复制代码
现在,代码看起来整洁了一点,但是用装饰器的话可以做的更好:
def check_age(f):def wrapper(*args,**kwargs):if args[1]<18:raise Exception('用户年龄不符合要求')return f(*args,**kwargs)return wrapperclass User(object):@check_agedef get_movie(self, age):return self.movie@check_agedef set_movie(self, age, movie):self.movie = movie复制代码
上面这段代码就是使用装饰的一个典型例子,函数check_age中定义了另一个函数wrapper,并将wrapper做为返回值。这个例子很好的展示了装饰器的语法。
2.2 装饰器的本质
上面说到装饰器的本质就是一个函数,这个函数接受另一个函数作为参数,并将其其以一个新的修改后的函数进行替换。再来看下面一个例子,可以帮我们更好的理解:
def bread(func):def wrapper():print ("</''''''\>")func()print ("</______\>")return wrapperdef sandwich():print('- sandwich -')sandwich_copy = bread(sandwich)
sandwich_copy()复制代码
输出结果如下:
</''''''\>
- sandwich -
</______\>复制代码
bread是一个函数,它接受一个函数作为参数,然后返回一个新的函数,新的函数对原来的函数进行了一些修改和扩展(打印一些东西),且这个新函数可以当做普通函数进行调用。
使用python提供的装饰器语法,简化上面的代码:
def bread(func):def wrapper():print ("</''''''\>")func()print ("</______\>")return wrapper@bread
def sandwich():print('- sandwich -')sandwich = sandwich()复制代码
到这里,我们应该理解了装饰器的用法和作用了,再次强调一遍,装饰器本质上就是一个函数,这个函数接受其他的函数作为参数,并将其以一个新的修改后的函数进行替换
3.使用装饰器需要注意的地方
前面我们介绍了装饰器的用法,可以看出装饰器其实很好理解,也非常简单。但是装饰器还有一些需要我们注意的地方
3.1 函数的属性变化
装饰器动态替换的新函数替换了原来的函数,但是,新函数缺少很多原函数的属性,如docstring和函数名。
def bread(func):def wrapper():print ("</''''''\>")func()print ("</______\>")return wrapper@bread
def sandwich():'''there are something'''print('- sandwich -')def hamberger():'''there are something'''print('- hamberger -')def main():print(sandwich.__doc__)print(sandwich.__name__)print(hamberger.__doc__)print(hamberger.__name__)main()复制代码
执行上面的程序,得到如下结果:
None
wrapper
there are something
hamberger复制代码
在上述代码中,定义了两个函数sandwich和hanberger,其中sandwich使用装饰器@bread进行了封装,我们获取sandwich和hanberger的docstring和函数名字,可以看到,使用了装饰器的函数,无法正确获取函数原有的docstring和名字,为了解决这个问题,可以使用python内置的functools模块。
def bread(func):@functools.wrap(func)def wrapper():print ("</''''''\>")func()print ("</______\>")return wrapper复制代码
我们只需要增加一行代码,就能正确的获取函数的属性。
此外,也可以像下面这样:
import functools
def bread(func):def wrapper():print ("</''''''\>")func()print ("</______\>")return functools.wraps(func)(wrapper)复制代码
不过,还是第一种方法的可读性要更强一点。
3.2使用inspect函数来获取函数参数
我们再来看如下一段代码:
def check_age(f):@functools.wraps(f)def wrapper(*args,**kwargs):if kwargs.get('age')<18:raise Exception('用户年龄不符合要求')return f(*args,**kwargs)return wrapperclass User(object):@check_agedef get_movie(self, age):return self.movie@check_agedef set_movie(self, age, movie):self.movie = movieuser = User()
user.set_movie(19,'Avatar')复制代码
这段代码运行后会直接抛出,因为我们传入的'age'是一个位置参数,而我们却用关键字参数(kwargs)获取用户名,因此。‘kwargs.get('age')’返回None,None和int类型是无法比较的,所以会抛出异常。
为了设计一个更加智能的装饰器,我们需要使用python的inspect模块。如下所示:
def check_age(f):@functools.wraps(f)def wrapper(*args,**kwargs):getcallargs = inspect.getcallargs(f, *args, **kwargs)print(getcallargs)if getcallargs.get('age')<18:raise Exception('用户年龄不符合要求')return f(*args,**kwargs)return wrapper复制代码
通过inspect.getcallargs,返回一个将参数名和值作为键值对的字典,在上述代码中,返回{'self': <__main__.user object="" at="">, 'age': 19, 'movie': 'Avatar'},通过这种方式,我们的装饰器不必检查参数username是基于位置参数还是基于关键字参数,而只需在字典中查找即可。
3.3多个装饰器的调用顺序
在开发中,会出现对于一个函数使用两个装饰器进行包装的情况,代码如下:
def bold(f):def wrapper():return "<b>"+f()+"</b>"return wrapper
def italic(f):def wrapper():return "<i>"+f()+"</i>"return wrapper
@bold
@italic
def hello():return "hello world"print(hello()) # <b><i>hello world</i></b>复制代码
分析
在前面我们提到,装饰器就是在外层进行了封装:@italichello()hello = italic(hello)复制代码
对于两层封装便是:
@bold@italichello()hello = bold(italic(hello))复制代码
这样理解多个装饰器的调用顺序,之后就不会再有疑问了
3.4 给装饰器传递参数
现在,我们的需求修改了,并不是限定为18岁了,对于不同的地区可能是20岁,也可能是16岁。那么我们如何设计一个通用的装饰器呢?
def check_age(age='18'):def decorator(f):@functools.wraps(f)def wrapper(*args, **kwargs):getcallargs = inspect.getcallargs(f, *args, **kwargs)if getcallargs.get('age') < age:raise Exception('用户年龄不符合要求')return f(*args, **kwargs)return wrapperreturn decoratorclass User(object):@check_age(18)def get_movie(self, age):return self.movie@check_age(18)def set_movie(self, age, movie):check_age(age)self.movie = movie
user = User()
user.set_movie(16,'Avatar')复制代码
通过上述方式,我们可以在使用装饰器时设置age的值,而不需要修改装饰器内的代码,使程序的健壮性更强,符合开闭原则。
4.总结
到这里,关于装饰器的理解,我们就介绍完了,配合在实际开发中的使用,你很快就能掌握它。