Python 装饰器简介与应用
装饰器(Decorator),也称为包装器(Wrapper),是一种重要的设计模式。它允许在不修改原始类文件或使用继承的情况下,动态地增强对象的功能。其核心思想是创建一个包装对象来包裹真实的对象,从而扩展其行为。相较于生成子类,装饰器模式提供了更灵活的功能扩展方式。 在 Python 中,装饰器能够为多个已有的函数添加共同的功能。可以将函数中重复的代码抽象成一个装饰器,然后应用于各个函数,实现代码复用并简化原函数。装饰器是实现日志记录、性能分析、事务管理等场景的理想选择。
设计原则
- 优先使用组合,减少继承。
- 遵循开放-封闭原则:对扩展开放,对修改关闭。
核心要点
- 装饰者与被装饰对象应遵循相同的超类型。
- 一个对象可以被一个或多个装饰者包装。
- 装饰者可以在调用被装饰者的方法前后插入自身行为,以达成特定目的。
- 对象可以在运行时动态地、无限制地被装饰。
- 在装饰模式中,继承的关键在于确保类型匹配,而非仅仅获取行为。
- 装饰者通常对组件的客户是透明的。但根据需求,也可添加新行为,实现"半透明"装饰。
- 适配器模式旨在改变接口,装饰模式旨在保持接口并增加职责。
Python 中的装饰器 Python 通过装饰器(Decorators)语法,可以方便地为函数、方法或类添加额外的职责或控制其调用。Python 的装饰器本质上是一种"语法糖",是语句的简写形式。
理解装饰器的演进过程
考虑如何测量一个函数 foo() 的执行时间。
def foo():
print('in foo()')
foo()
场景一:修改函数定义以添加计时
直接在 foo 函数内部添加计时代码。但这种方式会侵入原函数,且不易管理。
import time
def foo():
start_time = time.perf_counter()
print('in foo()')
end_time = time.perf_counter()
print('used:', end_time - start_time)
foo()
场景二:包装函数调用
创建一个独立的 timeit 函数来包装 foo 的调用。但如果 foo 在多处被调用,或调用代码无法修改,此方法会显得繁琐。
import time
def foo():
print('in foo()')
def timeit(func):
start_time = time.perf_counter()
func() # 调用传入的函数
end_time = time.perf_counter()
print('used:', end_time - start_time)
timeit(foo) # 调用时需要修改为 timeit(foo)
场景三:通过返回包装函数实现无侵入
创建一个 timeit 函数,它接收一个函数 func,并返回一个内部包装函数 wrapper。wrapper 负责执行计时和调用原函数。通过 foo = timeit(foo) 重新绑定 foo。
import time
def foo():
print('in foo()')
def timeit(func):
def wrapper():
start_time = time.perf_counter()
func()
end_time = time.perf_counter()
print('used:', end_time - start_time)
return wrapper
foo = timeit(foo) # 重新绑定 foo
foo() # 调用时,实际执行的是 wrapper 函数
场景四:使用 Python 装饰器语法糖
Python 提供了 @ 语法糖,使得装饰器的应用更加简洁。
import time
def timeit(func):
def wrapper():
start_time = time.perf_counter()
func()
end_time = time.perf_counter()
print('used:', end_time - start_time)
return wrapper
@timeit # 使用 @ 语法糖
def foo():
print('in foo()')
foo() # 直接调用 foo(),会自动应用 timeit 装饰器
定义装饰器
示例:无参数装饰器 装饰器函数接收被装饰的函数对象,并返回该函数对象(或包装后的函数)。
def deco(func):
print("Executing deco") # 装饰器逻辑
return func # 返回原函数
@deco
def foo():
print("in foo()")
foo() # 实际上是 deco(foo) 被调用,然后返回的函数被调用
示例:带参数的装饰器(被装饰函数无参数) 装饰器需要接收参数,因此需要一个外部函数来接收这些参数,并返回实际的装饰器函数。
def deco(arg):
print(f"Executing deco with arg: {arg}")
def decorator(func): # 内部函数,接收被装饰的函数
print("Executing decorator")
return func # 返回原函数
return decorator # 返回装饰器函数
@deco("custom_arg")
def foo():
print("in foo()")
foo() # 实际调用链: deco("custom_arg")(foo)
示例:带参数的装饰器(被装饰函数有参数)
当被装饰的函数带有参数时,包装函数 wrapper 需要使用 *args 和 **kwargs 来接收和传递参数。
def deco(arg):
print(f"Executing deco with arg: {arg}")
def decorator(func):
def wrapper(*args, **kwargs):
print("Executing wrapper")
return func(*args, **kwargs) # 传递所有参数
print("Executing decorator")
return wrapper
return decorator
@deco("custom_arg")
def foo(data):
print("in foo()")
print(f"Received data: {data}")
foo("test_data")
示例:带有前后置逻辑的装饰器 演示一个简单的装饰器,在调用被装饰函数前后打印信息。
def BeforeAfter(f):
def wrapper(x):
print('before function')
f(x) # 调用原函数
print('after function')
return wrapper
# 不使用 @ 语法糖的方式
def func1(x):
print('this is function func1')
print('I have a parameter: %s' % x)
wrapped_func1 = BeforeAfter(func1)
wrapped_func1('test_val1')
print("-" * 20)
# 使用 @ 语法糖的方式
@BeforeAfter
def func2(x):
print('this is function func2')
print('I have a parameter: %s' % x)
func2('test_val2')
装饰器的实际应用
示例:装饰不同参数列表的函数
使用 *args 和 **kwargs 可以让装饰器兼容各种参数签名的函数。
def deco(func):
def wrapper(*args, **kwargs):
print("Wrap start")
result = func(*args, **kwargs) # 调用原函数并获取返回值
print("Wrap end\n")
return result
return wrapper
@deco
def foo(x):
print("In foo():")
print("I have a para: %s" % x)
@deco
def bar(x, y):
print("In bar():")
print("I have two para: %s and %s" % (x, y))
@deco
def foo_dict(x, z='default_z'):
print("In foo_dict:")
print("I have two para, %s and %s" % (x, z))
foo('param_x')
bar('param_x', 'param_y')
foo_dict('param_x', z='custom_z')
示例:多装饰器组合 多个装饰器可以叠加应用,它们的执行顺序是从下往上。
def make_bold(fn):
def wrapped():
return "<b>" + fn() + "</b>"
return wrapped
def make_italic(fn):
def wrapped():
return "<i>" + fn() + "</i>"
return wrapped
@make_bold # 外层装饰器
@make_italic # 内层装饰器
def hello():
return "Hello World"
print(hello())
# 输出: <b><i>Hello World</i></b>
# 等同于: make_bold(make_italic(hello))()
示例:检查函数文档字符串 一个装饰器,用于检查被装饰函数是否具有文档字符串。
def deco_require_docstring(func):
if func.__doc__ is None:
print(f"Warning: Function '{func.__name__}' lacks a docstring.")
else:
print(f"Function '{func.__name__}' has docstring: '{func.__doc__}'")
return func
@deco_require_docstring
def function_without_doc():
print('Doing something without docstring')
@deco_require_docstring
def function_with_doc():
'This is a well-documented function.'
print('Doing something with docstring')
function_without_doc()
function_with_doc()