Python 使用装饰器 decorator 修改函数行为
- 使用装饰器修改函数行为
- 使用带有返回值和参数的被装饰函数
- 创建一个可以接受参数的装饰器
- 使用多个装饰器
使用装饰器修改函数行为
Python中装饰器(decorator)的概念基于 Decorator 设计模式,这是一种结构化设计模式。此模式允许您向对象添加新行为,而无需更改对象实现中的任何内容。这个新行为被添加到特殊的包装器对象中。
在 Python 中,装饰器是一种特殊的高阶函数,它使开发者能够在现有函数(或方法)中添加新功能,而无需添加或更改函数中的任何内容。通常,这些装饰器被添加到函数定义之前。装饰器可用于实现应用程序的许多功能,但在数据验证、日志记录、缓存、调试、加密和事务管理等方面尤其流行。
在 Python 中,任何实现了特殊方法 __call__()
的对象,都被称作可调用对象。因此,从最基本的意义上讲,装饰器就是一个返回可调用对象的可调用对象。在 Python 中,一些事物都是对象,函数也是对象。
这里,只以讲解函数返回函数为例。
基本上,装饰器接收一个函数,添加一些功能并返回一个可调用对象。下面的代码演示了一个 add_timestamps
装饰器,它返回了一个内部函数。内部函数在执行作为参数的函数之前和之后,输出当前时间:
from datetime import datetime
def add_timestamps(myfunc):
def _add_timestamps():
print(datetime.now())
myfunc()
print(datetime.now())
return _add_timestamps
@add_timestamps
def hello_world():
print('Hello World!')
hello_world()
上面的代码的输出结果是:
2024-12-30 14:54:17.568101
Hello World!
2024-12-30 14:54:17.568101
在这个例子中, @
符号用来装饰函数,它等价于:
hello = add_timestamps(hell_world)
hello()
这就是 Python 解释器在看到 @
符号之后做的事情——我们显示地调用了装饰器函数add_timestmaps
,传递给它一个hello_world
,它返回了它的内部函数。内部函数在被装饰的函数hell_world
的功能上,添加了新功能。
还有一个问题。我们在调试程序的时候,需要更详细的函数调用信息,然而,使用了装饰器后,当我们在被装饰的函数上调用 help
函数时,或者查看 __doc__
和 __name__
属性时,我们得到的,都不是被装饰函数的信息:
from datetime import datetime
def add_timestamps(myfunc):
def _add_timestamps():
print(datetime.now())
myfunc()
print(datetime.now())
return _add_timestamps
@add_timestamps
def hello_world():
"""你好,世界"""
print('Hello World!')
hello_world()
help(hello_world)
print(hello_world.__name__)
print(hello_world.__doc__)
上面代码的输出结果是:
2024-12-30 15:00:07.785346
Hello World!
2024-12-30 15:00:07.785346
Help on function _add_timestamps in module __main__:
_add_timestamps()
_add_timestamps
None
这显示不是我们想要的,我们仍然需要被装饰函数自己的信息。一个简单的解决方法是使用来自于 functools
模块的 wraps
装饰器。我们重新修订一下上面的代码:
from datetime import datetime
from functools import wraps
def add_timestamps(myfunc):
@wraps(myfunc)
def _add_timestamps():
print(datetime.now())
myfunc()
print(datetime.now())
return _add_timestamps
@add_timestamps
def hello_world():
"""你好,世界"""
print('Hello World!')
hello_world()
help(hello_world)
print(hello_world.__name__)
print(hello_world.__doc__)
上面代码的输出结果是:
2024-12-30 15:06:49.581314
Hello World!
2024-12-30 15:06:49.581314
Help on function hello_world in module __main__:
hello_world()
你好,世界
hello_world
你好,世界
这才是我们想要的,我们又找回了被装饰函数的所有信息。
使用带有返回值和参数的被装饰函数
当被装饰函数带有实参时,装饰这样的函数需要一些额外的技巧。一个技巧是在内部包装函数中使用*args
和**kwargs
。这将使内部函数接受任意数量的位置化和关键字参数。下面是一个带有参数和返回值的装饰函数的简单示例:
from functools import wraps
def power(func):
@wraps(func)
def inner_calc(*args, **kwargs):
print("内部函数")
result = func(*args, **kwargs)
return result
return inner_calc
@power
def power_base2(n):
return 2 ** n
print(power_base2(3))
上面代码的输出结果是:
内部函数
8
在上面的例子中,内部函数inner_calc
接受两个形参,*args
和 **kwargs
。被装饰器函数 power_base2
有一个返回值,它在 inner_calc
里执行完毕后,可以直接通过 inner_calc
把返回值返回给调用装饰器的代码。
创建一个可以接受参数的装饰器
前面例子中讲到的装饰器,是标准装饰器。标准装饰器是一个函数,它接受一个函数作为实参,返回一个内部函数,执行装饰工作。然而,当一个装饰器有自己的过次页参时,事情就有所不同了。这种装饰器构建在标准装饰器上面。简单地说,带有实参的装饰器实际上返回的是标准装饰器。下面的例子演示了这种带有参数的装饰器:
from functools import wraps
def power_calc(base):
def inner_decorator(func):
@wraps(func)
def inner_calc(*args, **kwargs):
exponent = func(*args, **kwargs)
return base ** exponent
return inner_calc
return inner_decorator
@power_calc(base = 3)
def power_n(n):
return n
print(power_n(3))
print(power_n(4))
上面代码的输出结果是:
27
81
上面代码的做的工作是:
power_calc
装饰器接受一个实参base
,返回inner_decorator
,这是一个标准装饰器;inner_decorator
接受一个函数作为实参,返回inner_calc
,它做真正的计算工作;inner_calc
函数调用被装饰函数获取指数exponent
,然后使用由外部装饰器传递进来的base
,一起计算结果。内部函数闭包,使得base
对inner_calc
可见。
使用多个装饰器
在一个函数上,可以使用多个装饰器,这可以通过链接装饰器实现。被链接的装饰器可以相同也可以不相同。通过在函数定义之前一个接一个地放置装饰器可以实现链接装饰器。下面是一个例子。
from datetime import datetime
from functools import wraps
def add_timestamps(func):
@wraps(func)
def inner_func(*args, **kwargs):
res = "{}:{}\n".format(datetime.now().strftime("%Y-%m-%d %H:%M:%S"), func(*args, **kwargs))
return res
return inner_func
def file(func):
@wraps(func)
def inner_func(*args, **kwargs):
res = func(*args, **kwargs)
with open("log.txt", "a") as f:
f.write(res)
return res
return inner_func
def console(func):
@wraps(func)
def inner_func(*args, **kwargs):
res = func(*args, **kwargs)
print(res)
return res
return inner_func
@file
@add_timestamps
def log1(msg):
return msg
@file
@console
@add_timestamps
def log2(msg):
return msg
@console
@add_timestamps
def log3(msg):
return msg
log1("这是一个只写到文件中的日志")
log2("这是一个写到文件和控制台的日志")
log3("这是一个只写到控制台的日志")
在上面的代码中,我们实现了三个装饰器:
file
:这个装饰器把被装饰函数提供的信息写到文件里;console
:这个装饰器把信息写到控制台上;add_timestamps
:添加时间的装饰器。这个装饰器一定要紧跟着被装饰函数,否则在输出信息中能添加上时间。
上面代码的输出结果是:
2024-12-30 16:39:58:这是一个写到文件和控制台的日志
2024-12-30 16:39:58:这是一个只写到控制台的日志
值得一提的是,装饰器在简化代码和以简洁的方式添加行为方面非常有用,但其代价是在执行过程中产生额外的开销。装饰器的使用应仅限于其收益足以补偿任何开销成本的情况。
<完>