使用Python装饰器减少重复代码
python 装饰器介绍¶
python 装饰器是一个很好用的功能,有很多用处,其原理是对输入的函数进行封装,然后返回一个封装后的函数。用户使用同样的名称调用函数,但实际上调用的是封装后的函数。
装饰器可以在封装过程中执行一些额外操作,从而拓展实际函数的功能。
一个简单的例子¶
# 定义一个装饰器
def print_function_name(f):
def _f(*args, **kwarg):
# 在调用实际的 f 之前打印 f 的名字
print(f"callable name is {f}")
# 调用 f
f(*args, **kwarg)
return _f
我们再定义一个普通函数:
def hello(name: str):
print(f"hello, {name}")
调用这个 hello
函数:
hello("Zen")
hello, Zen
结果如我们预期,如果我们再定义一个相同的函数,但是用装饰器进行封装:
@print_function_name
def hello2(name: str):
print(f"hello, {name}")
hello
和 hello2
两个函数的内容是一样的,我们调用 hello2
看看:
hello2("Zen")
callable name is <function hello2 at 0x00000171DA59F600> hello, Zen
是的,这就是魔法发生的地方,使用同样的函数名,hello2
已经不是原始的函数,而是通过装饰器装饰起来的函数。
调用hello2
的效果可以拆分成下面的步骤:
hello3 = print_function_name(hello)
hello3("Zen")
callable name is <function hello at 0x00000171DA34FB00> hello, Zen
上面的例子中,print_function_name
只是一个简单的装饰器,没有什么实际用处,但是演示了装饰器的基本原理,实际的装饰器函数都会执行更有意义的工作,否则干嘛要多一层嵌套封装呢!
什么样的函数可以作为装饰器¶
装饰器函数必须满足两个条件:
- 可以接受另外一个函数作为唯一输入参数
- 必须返回一个函数对象
这种可以接受函数本身作为输入参数的函数,在其他语言中称为高阶函数(Higher Order Function)以和普通的函数作为区分。
使用装饰器可以为一个函数添加额外的参数,这在一些场合下非常有用。比如我们有一些文件保存的操作,需要每次由用户来选择一下保存路径,选择保存路径这个过程就可以作为装饰器在调用实际函数之前来执行的通用操作。
如果使用传统的实现方式,大概如下:
def get_save_path() -> str:
file_path, _ = QFileDialog.getSaveFileName(
self,
"选择保存文件路径",
"",
"文本文件 (*.txt);;所有文件 (*)"
)
return file_path
def save_something1(something):
fpath = get_save_path()
if fpath:
print(f"save {something} to {fpath}")
def save_something2(something):
fpath = get_save_path()
if fpath:
print(f"save {something} to {fpath}")
我们再来看一下装饰器版本的实现:
def get_save_path_decorator(f):
def _f(*args, **kwarg):
file_path, _ = QFileDialog.getSaveFileName(
self,
"选择保存文件路径",
"",
"文本文件 (*.txt)"
)
if file_path:
f(file_path, *args, **kwargs)
else:
print("user doesn't select a valid save path")
return _f
@get_save_path_decorator
def save_something1(fpath: str, something):
print(f"save {something} to {fpath}")
@get_save_path_decorator
def save_something2(fpath: str, something):
print(f"save {something} to {fpath}")
使用装饰器,我们定义的 save_something1
多了一个参数 fpath
,但是需要注意的是在调用 save_something1
的时候,我们仍然只需要传入 something
这个参数,fpath
参数是在装饰器中通过让用户选择保存文件来获取到保存路径,如果不是有效的保存路径,就会打印错误,不调用保存函数。
通过使用装饰器实现,我们获得了以下好处:
save_something
的实现更加简单,不需要考虑输入路径为空等corner case
,这些情况已经在装饰器中统一处理- 可维护性更高,我们只需要在装饰器中修改错误处理函数,而不需要在每个
save_something
中逐个修改
可定制的装饰器¶
以上演示的装饰器,都只是固定的功能,只能接受一个函数作为输入参数。那么如何根据一些参数来配置装饰器的功能呢?比如说上述的get_save_path_decorator
装饰器,它的文件过滤规则只能是 txt
文件,假设有另外的函数需要选择后缀名为 json
的文件路径呢?当然可以重新定义一个装饰器,但是新的装饰器和旧的装饰器代码大部分都雷同,所以肯定有更好的方法。
此时,我们就需要定义一个返回装饰器的函数。先看代码:
def get_save_path_decorator_factory(file_type: str):
def decorator(f):
def _f(*args, **kwarg):
file_path, _ = QFileDialog.getSaveFileName(
self,
"选择保存文件路径",
"",
file_type
)
if file_path:
f(file_path, *args, **kwargs)
else:
print("user doesn't select a valid save path")
return _f
return decorator
上述的get_save_path_decorator_factory
就是一个返回装饰器的函数,返回的装饰器中文件类型是由传入的参数确定的,而不是固定的,那么如何它来装饰函数呢?
@get_save_path_decorator_factory("文本文件(*.txt)")
def save_something3(fpath: str, something):
print(f"save {something} to {fpath}")
@get_save_path_decorator_factory("Json文件(*.json)")
def save_something4(fpath: str, something):
print(f"save {something} to {fpath}")
注意调用的时候,我们使用 get_save_path_decorator_factory("文本文件(*.txt)")
返回一个装饰器,然后用这个装饰器再装饰 save_something3
函数,如果我们把这两个函数的签名打印出来:
get_save_path_decorator_factory("文本文件(*.txt)"), save_something3
(<function __main__.get_save_path_decorator_factory.<locals>.decorator(f)>, <function __main__.get_save_path_decorator_factory.<locals>.decorator.<locals>._f(*args, **kwarg)>)
可以更清楚地理解这个过程,get_save_path_decorator_factory("文本文件(*.txt)")
返回的就是在get_save_path_decorator_factory
中定义的decorator
这个函数;save_something3
就是在get_save_path_decorator_factory
中的decorator
中的_f
函数。
总结¶
本文介绍了python的装饰器的概念,并且介绍了一些高级用法,巧用装饰器,可以大大减少代码的冗余,提高维护性。