1.介绍
Python装饰器在开发过程中,有着较为重要的地位,但是对于初学者来说,并不便于理解,本文将带着大家分析python装饰器的使用。
2.定义
装饰器本质上就是一个函数,这个函数接受其他函数作为参数,并将其以一个新的修改后的函数作为替换。
概念较为抽象,我们来考虑如下一个场景,现在我们需要对用户年龄进行认证,如果年龄小于18,则给出提示,年龄不符合要求(嘿嘿嘿,大家都懂)。代码如下:
class Movie(object):
def get_movie(self,age):
if age<18:
raise Exception('用户年龄不符合要求')
return self.movie
def 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.movie
def 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 wrapper
class User(object):
@check_age
def get_movie(self, age):
return self.movie
@check_age
def set_movie(self, age, movie):
self.movie = movie
上面这段代码就是使用装饰的一个典型例子,函数check_age中定义了另一个函数wrapper,并将wrapper做为返回值。这个例子很好的展示了装饰器的语法。
2.2 装饰器的本质
上面说到装饰器的本质就是一个函数,这个函数接受另一个函数作为参数,并将其其以一个新的修改后的函数进行替换。再来看下面一个例子,可以帮我们更好的理解:
def bread(func):
def wrapper():
print ("</''''''\>")
func()
print ("</______\>")
return wrapper
def 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 wrapper
class User(object):
@check_age
def get_movie(self, age):
return self.movie
@check_age
def set_movie(self, age, movie):
self.movie = movie
user = 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 0x10be19320>, '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>
分析
在前面我们提到,装饰器就是在外层进行了封装:@italic hello() hello = italic(hello)
对于两层封装便是:
@bold @italic hello() 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 wrapper
return decorator
class 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.总结
到这里,关于装饰器的理解,我们就介绍完了,配合在实际开发中的使用,你很快就能掌握它。