细说Python装饰器
一、概念
装饰器(Decorators)是 Python 的一个重要部分。简单地说:装饰器是个Python函数,能够在不修改任何原先代码的前提下,对被修饰函数添加额外的功能。他们有助于让我们的代码更简短,也更Pythonic(Python范儿)。需要指出的是,Python的装饰器与Java中的注解完全是两个概念,切勿进行参考对照。装饰器对于初次接触该概念的开发人员可能并不是那么非常容易理解掌握,所以在这里我会从最基础的开始讲起,且每次只讨论一个步骤,这样你能完全理解它。
二、逐步理解解析装饰器
1、一切皆对象
首先我们来理解下 Python 中的函数:
def hi(name="cl"):
return "hi " + name
print(hi()) # 调用hi函数
# output: 'hi cl'
# 将函数赋值给一个变量,比如
greet = hi
# 我们这里没有在使用小括号,因为我们并不是在调用hi函数
# 而是将它放在greet变量里头。
print(greet())
# output: 'hi cl'
del hi # 删掉hi函数
print(hi())
#outputs: NameError
print(greet())
#outputs: 'hi cl'
在这里你可以看到,函数名hi可以被任意传递并赋值,函数也只不过只是一个对象而已。
2、在函数中定义函数
在刚刚的基础上,我们可以在一个函数中定义另一个函数:
def hi(name="cl"):
print("now you are inside the hi() function")
def greet():
return "now you are in the greet() function"
def welcome():
return "now you are in the welcome() function"
print(greet())
print(welcome())
print("now you are back in the hi() function")
hi()
#output:now you are inside the hi() function
# now you are in the greet() function
# now you are in the welcome() function
# now you are back in the hi() function
# 上面展示了无论何时你调用hi(), greet()和welcome()将会同时被调用。
# 然后greet()和welcome()函数在hi()函数之外是不能访问的,比如:
greet()
#outputs: NameError: name 'greet' is not defined
3、在函数中返回函数
同样的,函数也能返回函数。
def hi(name="cl"):
def greet():
return "now you are in the greet() function"
def welcome():
return "now you are in the welcome() function"
if name == "cl":
return greet
else:
return welcome
a = hi()
print(a)
#outputs: <function hi.<locals>.greet at 0x000001AE9CEF7318>
#上面清晰地展示了`a`现在指向到hi()函数中的greet()函数
#现在试试这个
print(a())
#outputs: now you are in the greet() function
在这里需要注意的是,我们return的是函数名(greet、welcome),并没有进行调用(也就是没有加括号),所以a变量保存的是greet函数对象。
4、将函数作为参数传给另一个函数
def hi():
return "hi cl!"
def doSomethingBeforeHi(func):
print("I am doing some boring work before executing hi()")
print(func())
doSomethingBeforeHi(hi)
#outputs:I am doing some boring work before executing hi()
# hi cl!
以上这些其实是Python中非常基础的概念,相信其实你们都是嗤之以鼻的快速一扫而过。但是这些东西其实已经触及到了装饰器的概念。
5、第一个装饰器
def a_new_decorator(a_func):
def wrapTheFunction():
print("I am doing some boring work before executing a_func()")
a_func()
print("I am doing some boring work after executing a_func()")
return wrapTheFunction
def a_function_requiring_decoration():
print("I am the function which needs some decoration to remove my foul smell")
a_function_requiring_decoration()
#outputs: "I am the function which needs some decoration to remove my foul smell"
a_function_requiring_decoration = a_new_decorator(a_function_requiring_decoration)
#now a_function_requiring_decoration is wrapped by wrapTheFunction()
a_function_requiring_decoration()
#outputs:I am doing some boring work before executing a_func()
# I am the function which needs some decoration to remove my foul smell
# I am doing some boring work after executing a_func()
这里的每一步都运用了上面我们提到的那些基本概念,你应该是可以理解的。其实a_function_requiring_decoration = a_new_decorator(a_function_requiring_decoration)
这一行代码就是装饰器,只不过Python提供了@语法糖让我们来简化、优雅我们的代码,下面即是我们常见的装饰器:
@a_new_decorator
def a_function_requiring_decoration():
"""Hey you! Decorate me!"""
print("I am the function which needs some decoration to "
"remove my foul smell")
a_function_requiring_decoration()
#outputs: I am doing some boring work before executing a_func()
# I am the function which needs some decoration to remove my foul smell
# I am doing some boring work after executing a_func()
#the @a_new_decorator is just a short way of saying:
a_function_requiring_decoration = a_new_decorator(a_function_requiring_decoration)
这里可以得到一个核心逻辑理念: 在Python Version < 2.4时,也就是2004年以前,如果想为一个函数添加额外的功能,那么写法就是a_function_requiring_decoration = a_new_decorator(a_function_requiring_decoration)
,这也是最直观可以去理解装饰器的表达式。装饰器接收一个callable对象。
在这里会有个问题,你可以运行一下如下代码:
def a_new_decorator(a_func):
def wrapTheFunction():
"""wrapper函数"""
print("I am doing some boring work before executing a_func()")
a_func()
print("I am doing some boring work after executing a_func()")
return wrapTheFunction
@a_new_decorator
def a_function_requiring_decoration():
"""需要被装饰的函数"""
print("I am the function which needs some decoration to remove my foul smell")
print(a_function_requiring_decoration.__name__)
print(a_function_requiring_decoration.__doc__)
# outputs:wrapTheFunction
# wrapper函数
这明显是有问题的,其实道理也很简单: a_new_decorator函数返回的是wrapTheFunction这个函数对象,然后根据上面提到的核心逻辑,装饰器其实就是a_function_requiring_decoration = a_new_decorator(a_function_requiring_decoration),也就是说a_new_decorator(a_function_requiring_decoration)将返回值(也就是wrapTheFunction函数对象)赋给a_function_requiring_decoration,那么自然a_function_requiring_decoration的__name__和__doc__都来自wrapTheFunction。使用functools.wraps可以轻松解决这个问题:
from functools import wraps
def a_new_decorator(a_func):
# 注意:@wraps接受一个函数来进行装饰,并加入了复制函数名称、注释文档、参数列表等等的功能。
# 这可以让我们在装饰器里面访问在装饰之前的函数的属性。
@wraps(a_func)
def wrapTheFunction():
"""wrapper函数"""
print("I am doing some boring work before executing a_func()")
a_func()
print("I am doing some boring work after executing a_func()")
return wrapTheFunction
@a_new_decorator
def a_function_requiring_decoration():
"""需要被装饰的函数"""
print("I am the function which needs some decoration to remove my foul smell")
print(a_function_requiring_decoration.__name__)
print(a_function_requiring_decoration.__doc__)
# outputs:a_function_requiring_decoration
# 需要被装饰的函数
OK,这个问题被解决,下面我们来谈一下有哪些地方可以完美使用装饰器。
装饰器经常被用于有切面需求的场景,较为经典的有插入日志、性能测试、事务处理、缓存、权限校验等。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量函数中与函数功能本身无关的雷同代码并继续重用。所谓切面,这个概念在编程中非常重要,概括来说也就是,将业务逻辑代码与系统需求代码(如插入日志)分开,把系统需求代码封装成装饰器,从而直接重用在各种业务逻辑方法上。
另外,wrapTheFunction函数应该和a_function_requiring_decoration的参数情况是一致的,要么都有参数,要么都没有参数,但是,实际情况中,我们都会在wrapTheFunction中加上不定长参数,即
def wrapTheFunction(*args, **kwargs)
6、实例代码
授权(Authorization):装饰器能有助于检查某个人是否被授权去使用一个web应用的端点(endpoint)。它们被大量使用于Flask和Django web框架中。这里是一个例子来使用基于装饰器的授权:
from functools import wraps
def requires_auth(f):
@wraps(f)
def decorated(*args, **kwargs):
auth = request.authorization
if not auth or not check_auth(auth.username, auth.password):
authenticate()
return f(*args, **kwargs)
return decorated
日志(Logging):
from functools import wraps
def logit(func):
@wraps(func)
def with_logging(*args, **kwargs):
logs.info(func.__name__ + " was called") # logs为自己封装的log对象
return func(*args, **kwargs)
return with_logging
7、带参数的装饰器
上面说到,装饰器其实也就是函数,那么函数自然可以带参数,也可以不带参数,上面的都是不带参数的装饰器,那么装饰器可以带参数吗?答案是显而易见的:可以。
以上面的Logging场景为例,如果说我想定义输出的日志文件,那么就可以这样编写代码:
from functools import wraps
def logit(logfile='out.log'):
def logging_decorator(func):
@wraps(func)
def wrapped_function(*args, **kwargs):
log_string = func.__name__ + " was called"
print(log_string)
# 打开logfile,并写入内容
with open(logfile, 'a') as opened_file:
# 现在将日志打到指定的logfile
opened_file.write(log_string + '\n')
return func(*args, **kwargs)
return wrapped_function
return logging_decorator
@logit() # 括号不能丢,等同于myfunc1=logit(logfile='out.log')(myfunc1)
def myfunc1():
pass
myfunc1()
# Output: myfunc1 was called
# 现在一个叫做 out.log 的文件出现了,里面的内容就是上面的字符串
@logit(logfile='func2.log') # 等同于myfunc2=logit(logfile='func2.log')(myfunc2)
def myfunc2():
pass
myfunc2()
# Output: myfunc2 was called
# 现在一个叫做 func2.log 的文件出现了,里面的内容就是上面的字符串
8、类装饰器
很明显,我们现在已经可以编写函数形式的装饰器了,但是你可能会发现,按照合理的编码思维来说,每个装饰器都只会实现一个额外功能的拓展,如果想同时对一个函数添加多个额外功能的话,可以同时添加好几个装饰器即可,那么现在设想这样一种场景,还是以Logging为例。在某些时刻,你只想打印日志,那么上面的函数格式的装饰器即可满足需求,但是在某些时刻,你除了想打印日志,同时你还想把日志文件发送电子邮件给你的leader,那么很明显这里可以应用继承重写的思想,也就是把logit当作父类,只实现打印日志的功能,但是包含发送邮件的抽象方法,同时编写一个子类email_logit,对发送邮件的抽象方法进行重写,这样,email_logit就在打印日志的基础上,同时可以实现发送邮件的功能,代码如下:
from functools import wraps
class logit:
def __init__(self, logfile='out.log'):
self.logfile = logfile
def __call__(self, func):
@wraps(func)
def wrapped_function(*args, **kwargs):
log_string = func.__name__ + " was called"
print(log_string)
# 打开logfile并写入
with open(self.logfile, 'a+') as opened_file:
# 现在将日志打到指定的文件
opened_file.write(log_string + '\n')
# 现在,发送一个通知
self.notify()
return func(*args, **kwargs)
return wrapped_function
def notify(self):
# logit只打日志,不做别的,在子类中重写
pass
class email_logit(logit):
"""
一个logit的实现版本,可以在函数调用时发送email给管理员
"""
def __init__(self, email='admin@myproject.com', *args, **kwargs):
self.email = email
super(email_logit, self).__init__(*args, **kwargs)
def notify(self):
# 发送一封email到self.email
# 这里就不做实现了
print(f'邮件发送给{self.email}成功')
@email_logit(email='xxx@sina.com', logfile='output.log')
def f():
pass
f()
# outputs:f was called
# 邮件发送给xxx@sina.com成功
三、小结
Python的装饰器和Java的注解(Annotation)并不是同一回事,和C#中的特性(Attribute)也不一样,完全是两个概念。
装饰器的理念是对原函数、对象的加强,相当于重新封装,所以一般装饰器函数都被命名为wrapper()
,意义在于包装。函数只有在被调用时才会发挥其作用。比如@logging
装饰器可以在函数执行时额外输出日志,@cache
装饰的函数可以缓存计算结果等。而注解和特性则是对目标函数或对象添加一些属性,相当于将其分类。这些属性可以通过反射拿到,在程序运行时对不同的特性函数或对象加以干预。比如带有Setup
的函数就当成准备步骤执行,或者找到所有带有TestMethod
的函数依次执行等等。