Python 中的装饰器详解(@decorator、装饰器函数、装饰器类)
一切皆对象的思想
要理解装饰器,首先需要理解 Python 中函数也是对象的思想。
1) 函数作为对象
def hi():
return "hi"
greet = hi
# 可以将一个函数赋值给一个变量
# 没有使用小括号,因为不是在调用hi函数
# 而是在将它放在greet变量里头
print(greet()) # 输出: 'hi'
# 如果删掉旧的hi函数,print(greet())依然可以正常运行
del hi
print(greet()) # 输出: 'hi'
2) 函数作为返回值
def hi(name="yasoob"):
def greet():
return "hi"
return greet
a = hi()
print(a) # 输出: <function greet at 0x7f2143c01500>
# 说明变量a现在指向到hi()函数中的greet()函数
print(a()) # 输出: hi
3) 函数作为参数传给另一个参数
def hi():
return "hi"
def useHi(func):
print(func())
useHi(hi) # 输出: hi
从上面的代码可以看出,Python 中的函数和 Java、C++不太一样,Python 中的函数可以像普通变量一样当做参数传递给另外一个函数。函数名加上括号就会被执行,而函数名不加括号就可以作为一个对象进行传递。
装饰器的实现
1) 装饰器函数的定义
- 装饰器是一个参数和返回值都是函数的特殊函数
def decorator(被装饰函数):
# 装饰器需内嵌一个函数用作返回值
def 包裹函数():
# 以下两行是 “新的被装饰函数”
print("这是装饰语句")
被装饰函数()
return 包裹函数 # 装饰器返回值是一个函数对象
2) 装饰器函数的简单使用
def 被装饰函数():
print("hi")
被装饰函数 = decorator(被装饰函数) # 将被装饰函数进行装饰改造
被装饰函数()
# 输出:
# 这是装饰语句
# hi
3) 使用 @ 符号简化代码:
被装饰函数 = decorator(被装饰函数)
可简化为 @decorator
# 下面使用@符号来简化上面的代码
@decorator
def 被装饰函数():
print("hi")
被装饰函数()
# 输出:
# 这是装饰语句
# hi
装饰器进阶
使用 @wraps()
print(被装饰函数.__name__)
# 输出: 包裹函数
# 应当输出: 被装饰函数
可以看出被装饰函数经过装饰后,函数的 __name__
和注释文档会被改写,Python 提供了一个简单的装饰函数来解决这个问题,那就是 functools.wraps:
from functools import wraps
# 修改后的装饰器
def 装饰器函数(被装饰函数):
@wrap(被装饰函数) # wraps装饰器帮我们保留被装饰函数的__name__和注释文档
def 包裹函数():
print("这是装饰语句")
被装饰函数()
return 包裹函数
*args,**kwargs 作为装饰器内嵌函数的形参
对于不同的函数有不同的参数数量和类型,将装饰器包裹函数的形参设为 *args,**kwargs
就可以实现让不同的函数可以使用同一装饰器:
from functools import wraps
# 进一步修改后的装饰器
def 装饰器函数(被装饰函数):
@wrap(被装饰函数) # wraps装饰器帮我们保留被装饰函数的__name__和注释文档
def 包裹函数(*args,**kwargs):
print("这是装饰语句")
被装饰函数(*args, **kwargs)
return 包裹函数
装饰器示例:
如果你仔细阅读了上文,那么下面这段代码对你而言应该已经非常容易理解:
from functools import wraps
# 先定义一个装饰器
def decorator(f):
@wraps(f)
def decorated(*args, **kwargs):
if not can_run:
return "Function will not run"
return f(*args, **kwargs)
return decorated
# 使用装饰器
@decorator
def func():
return("Function is running")
# 测试装饰器
can_run = True
print(func())
# 输出: Function is running
can_run = False
print(func())
# 输出: Function will not run
带参数的装饰器
带参数的装饰器实质上就是在装饰器函数外再嵌套一层函数用于接收参数的三层嵌套函数,(4) 中提到的 @wraps() 就是一个带参数的装饰器。
下面就是一个带参数的用于指定文件输出日志的装饰器示例:
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)
with open(logfile, 'a') as opened_file:
opened_file.write(log_string + '\n')
return func(*args, **kwargs) # 这句话不被执行!
return wrapped_function # 返回已封装被装饰函数的包裹函数
return logging_decorator # 返回已封装装饰器参数的函数
@logit()
def myfunc1():
pass
@logit(logfile='func2.log')
def myfunc2():
pass
myfunc1() # 输出: myfunc1 was called
# 现在一个叫做 out.log 的文件出现了,里面的内容就是上面的字符串
myfunc2() # 输出: myfunc2 was called
# 现在一个叫做 func2.log 的文件出现了,里面的内容就是上面的字符串
三、装饰器类
装饰器类相较上文中描述的多层嵌套式的装饰器函数具有更好的拓展性,可以使用子类来构造出具有更多功能的装饰器。
下面用装饰器类的方法来重构上文的日志装饰器 @logit :
from functools import wraps
class logit(object):
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
下面构造一个继承 @logit 的 @email_logit 装饰器类,在打日志的基础上,还有发送邮件给管理员的功能:
class email_logit(logit):
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
# 这里就不做实现了
pass