Python装饰器-提高函数的可重用性
随着软件系统的不断升级和改进,我们的程序也越来越复杂,通常只是一系列彼此相关的函数和类。一个好的软件系统应该能够轻松地添加、修改或删除这些函数和类,同时不会影响到其它部分的运行。Python装饰器这个概念就是来满足这个需求的,它可以帮助我们提高函数的可重用性、降低代码的重复率、增加程序的可扩展性。
装饰器的定义
在Python中,装饰器是一种高阶函数,它可以接收另一个函数作为输入,并返回一个新的函数,同时不改变原有函数的调用方式和输出结果。装饰器的作用是,在不修改原有函数的情况下,给其添加新的功能或特性,包括但不限于:
- 记录函数的运行时间和执行次数
- 缓存函数的输出结果
- 验证函数的权限和输入参数
- 注册和调度函数
- 在函数执行前和执行后做某些事情等等
这里我们用一个示例来介绍装饰器的基本概念。假设有一个函数,能够将两个数字相加并返回结果:
def add(x, y):
return x + y
print(add(1, 2)) # 输出 3
现在,我们希望这个函数能够在执行前和执行后,分别打印一行文本:
def add(x, y):
print("函数开始执行了!")
result = x + y
print("函数执行结束了!")
return result
print(add(1, 2)) # 输出:
# 函数开始执行了!
# 函数执行结束了!
# 3
这样虽然能达到我们的想法,但是也不可避免地增加了代码的长度和重复率。为了让代码更简洁,我们可以使用装饰器来实现同样的功能:
def add_wrapper(func):
def wrapper(x, y):
print("函数开始执行了!")
result = func(x, y)
print("函数执行结束了!")
return result
return wrapper
add = add_wrapper(add) # 使用装饰器
print(add(1, 2)) # 输出:
# 函数开始执行了!
# 函数执行结束了!
# 3
这里我们定义了一个装饰器函数add_wrapper,它接收一个函数func作为参数,返回一个新的函数wrapper。在wrapper函数中,我们首先打印一行文本表示函数开始执行了,然后调用原函数func并获取其返回值,最后打印一行文本表示函数执行结束了,并返回原函数的返回值。为了使用装饰器,我们只需要在原函数定义前添加一个@add_wrapper的装饰语句即可。
函数签名和元数据
虽然装饰器能够为函数添加新的功能,但是也会造成一个小问题,就是函数的原始信息可能会被覆盖。这里我们为了解决这个问题,介绍两个Python内置库:
- functools: 提供了一些有用的函数和工具类,可以方便地增强程序的可读性和可重用性。
- inspect: 提供了一些有用的函数和类,可以自动化地解析Python代码和对象,并提供元数据和调试信息。
下面是一个示例,展示如何使用functools模块来保留函数的签名信息:
import functools
def add_wrapper(func):
@functools.wraps(func)
def wrapper(x, y):
print("函数开始执行了!")
result = func(x, y)
print("函数执行结束了!")
return result
return wrapper
@add_wrapper
def add(x, y):
"""求两个数字的和"""
return x + y
print(add.__name__) # 输出 add
print(add.__doc__) # 输出 求两个数字的和
注意到这里我们在内部嵌套的wrapper函数上,添加了一个functools.wraps(func)的装饰语句。这个语句的作用是,将函数wrapper的元数据设置为原函数func的元数据,包括函数名、文档字符串、参数列表等等。这样,我们在调用add函数时,就能够正确地显示函数的签名和文档。
另外一个有用的库是inspect,其中最常用的函数是signature,它能够获取函数的参数列表,并返回一个带有注释的字典对象。下面是一个示例,展示如何使用inspect模块来检查函数的参数类型和默认值:
import inspect
def type_check(func):
sig = inspect.signature(func)
params = [(name, param.annotation)
for name, param in sig.parameters.items()]
def wrapper(*args, **kwargs):
for i, arg in enumerate(args):
name = sig.parameters[i].name
expected_type = sig.parameters[i].annotation
if not isinstance(arg, expected_type):
raise TypeError(f"{name}应该为{expected_type}类型!")
for name, arg in kwargs.items():
expected_type = sig.parameters[name].annotation
if not isinstance(arg, expected_type):
raise TypeError(f"{name}应该为{expected_type}类型!")
return func(*args, **kwargs)
return wrapper
@type_check
def add(x: int, y: int=0):
"""求两个数字的和"""
return x + y
print(add(1, 2)) # 输出 3
print(add(1.0, 2.0)) # 报错 TypeError: x应该为<class 'int'>类型!
注意到这里我们定义了一个装饰器函数type_check,它会读取原函数的参数类型,并自动检查实际参数是否符合要求。在wrapper函数中,我们首先通过inspect.signature函数获取原函数的参数列表,并筛选出参数名和参数类型的列表。然后我们使用两个循环遍历所有传入参数,并检查其数据类型是否正确。最后,我们调用原函数并返回其返回值,实现参数类型检查的功能。这样一来,我们就可以在使用add函数时自动进行参数类型检查,并避免了参数类型不匹配的错误。
小结
本文介绍了Python装饰器的基本概念,展示了如何使用装饰器来提高函数的可重用性和可读性。装饰器是一种非常强大和灵活的编程技术,可以为程序提供额外的功能和属性,同时减少代码冗余和复杂度。在实际开发中,我们可以根据自己的需要,自定义各种各样的装饰器,并将其组合使用,以优化程序的性能和可维护性。
