前言:
在说装饰器之前,我们先来了解一下闭包的概念。
闭包(Closure)
定义:闭包是函数式编程的概念,在 Python 中指的是在一个外部函数中定义的内部函数,这个内部函数引用了外部函数的变量(状态),即使外部函数已经执行完毕,这些变量的状态仍然被保持,可以通过内部函数来访问。
作用:闭包允许你保存函数的状态,从而在函数执行完毕后仍然能够访问其内部状态。
示例:
def outer_function(msg):
message = msg
def inner_function():
print(message)
return inner_function
printer = outer_function('Hello, World!')
printer()
在这个例子中,inner_function() 是一个内嵌在 outer_function() 中的函数。inner_function() 使用了外部函数的 message 变量,并且 outer_function() 返回了 inner_function() 这个函数对象,而不是调用它。因此,printer 变成了指向 inner_function() 的引用,且 message 变量的值在 outer_function() 执行完成后依然被保留。
装饰器(Decorator)
定义:装饰器本质上也是一个函数,它的特殊之处在于它的参数是另一个函数。装饰器返回了一个包含了原函数功能加上新功能的函数。
作用:装饰器主要用于在不修改原有函数代码的情况下给函数添加新的功能。这种功能的添加是通过"装饰"这个行为实现的,即在函数执行前或执行后自动执行一些额外的代码。
联系
实现基础:装饰器的实现依赖于闭包。装饰器通过返回一个函数(闭包)来实现在不修改被装饰函数原有功能的基础上增加新功能。
增强功能:它们都可以用来增强原有函数的功能,闭包通过封装状态增强,而装饰器提供了一种更灵活的方式来增强函数功能(添加前处理、后处理逻辑等)。
区别
目的和应用范围:闭包通常用于封装变量,创建只有函数内部可以访问的私有变量;而装饰器主要用于增强函数功能,常用于日志记录、性能测试、事务处理、缓存等方面。
返回的函数关系:闭包在使用时通常返回一个函数,这个返回的函数可以访问其外部作用域中的变量。装饰器虽然也返回一个函数,但这个返回的函数通常是对原函数进行包装后的版本,即增加了额外功能的函数。
使用语法:在 Python 中,装饰器有专门的语法糖(@ 符号)进行标识和使用,而闭包则没有专门的语法糖,通常以函数嵌套的形式出现。
1. 装饰器示例
接下来,我们看几个装饰器的具体例子:
1.1 示例1: 函数装饰函数(即装饰器与被装饰对象都是函数)
1.1.1 不带参数类装饰器函数
#!/usr/bin/env python
# coding=utf-8
# Author: Summer
# Time: 2024.03.29
import time
def decorator(func):
"""实现一个普通装饰器,打印每次函数开始执行的时间"""
def wrapper(*args, **kwargs):
execute_time = time.strftime('%Y-%m-%d', time.localtime(time.time()))
print(f"当前函数 {func.__name__} 的执行时间是:{execute_time}")
return func(*args, **kwargs)
return wrapper
@decorator # 等价于 func2 = decorator(func2)
def func2():
print("python 函数装饰器")
func2()
# 输出:
#当前函数 func2 的执行时间是:2024-03-29
#python 函数装饰器
1.1.2 带参数类装饰器函数
#!/usr/bin/env python
# coding=utf-8
# Author: Summer
# Time: 2024.03.29
def prefix_decorator(prefix):
def wrapper(original_function):
def inner(*args, **kwargs):
print(prefix, 'Executed Before', original_function.__name__)
result = original_function(*args, **kwargs)
print(prefix, 'Executed After', original_function.__name__, '\n')
return result
return inner
return wrapper
@prefix_decorator('LOG:') # 等价于 display_info= decorator('Log:')(display_info),调用时需要传入参数
def display_info(name, age):
print('display_info ran with arguments ({}, {})'.format(name, age))
display_info('John', 25)
# 输出
#LOG: Executed Before display_info
#display_info ran with arguments (John, 25)
#LOG: Executed After display_info
当我们调用 display_info(‘John’, 25) 函数时,实际上是在执行由 @prefix_decorator(‘LOG:’) 创建的 wrapper_function。结果是,在 display_info 函数执行前后,会额外打印出定制的日志信息。
1.2 示例2: 类装饰器装饰函数
装饰器类装饰函数,即装饰器是一个类,被装饰对象是一个函数
#!/usr/bin/env python
# coding=utf-8
# Author: Summer
# Time: 2024.03.29
class DecoratorClass:
def __init__(self, original_function):
self.original_function = original_function
def __call__(self, *args, **kwargs):
print('call method executed before {}'.format(self.original_function.__name__))
return self.original_function(*args, **kwargs)
@DecoratorClass
def display():
print('display function ran')
display()
# 输出:
# call method executed before display
# display function ran
类 DecoratorClass 定义了两个方法:
init:特殊方法,当创建 DecoratorClass 的实例时调用。它接受一个函数 original_function 作为参数并将其存储为一个实例属性,以便之后在 call 中调用。
call:特殊方法,当类的实例被当作函数调用时执行。call 方法被设计为接受任意个参数 *args 和任意个关键字参数 **kwargs。它首先打印出一条信息表示即将执行包装的原始函数 self.original_function,然后调用原始函数并向其传递接收到的参数。最终,call 方法返回原始函数的执行结果。
1.3 示例3:函数装饰器装饰类
函数装饰类是使用函数作为装饰器来装饰类的一种方式,这可以用来修改或增强类的行为。以下面代码为例,我们将通过一个装饰器函数为类动态地添加一个新方法:
#!/usr/bin/env python
# coding=utf-8
# Author: Summer
# Time: 2024.03.29
def add_method(cls):
def print_name(self):
print(f"My name is {self.name}")
cls.print_name = print_name
return cls
@add_method
class Person:
def __init__(self, name):
self.name = name
# 使用装饰过的类
p = Person("John")
p.print_name() # 输出: My name is John
在这个例子中,我们定义了一个装饰器函数 add_method,它接收一个类 cls 作为参数。装饰器的内部定义了一个 print_name 方法,这个方法简单地打印出实例的名字。然后这个方法被动态地添加到传入的类 cls 上,最后返回这个修改过的类。
通过在 Person 类上使用 @add_method 装饰器,我们不必直接在类定义中编写 print_name 方法;相反,print_name 方法是通过装饰器动态添加到类中的。创建了 Person 实例 p 之后,我们可以直接调用 p.print_name(),这证明了 print_name 方法已成功被添加到 Person 类中。
这个方法特别适用于当你想要在多个类中添加共同的方法时,可以通过编写一个装饰器来实现,而避免了代码的重复。此外,它也可以用于在不修改原始类定义的情况下,向类动态添加额外功能,从而实现更灵活的设计。
1.4 示例4 类装饰器装饰类
#!/usr/bin/env python
# coding=utf-8
# Author: Summer
# Time: 2024.03.29
"""
一个类来装饰另一个类,不同于其他几种装饰器的实现方式,
在使用类装饰器的时候需要注意:
在装饰器类内需要实现__init__()和__call__()方法
"""
class Decorator:
"""定义一个类装饰器,其初始化方法,需要传入self, decorated_class, *args, **kwargs
其中decorated_class: 被装饰的类, *args, **kw被装饰类的初始化方法所需要传入的参数
"""
def __init__(self, decorated_class, *args, **kwargs):
# 将需要装饰的类作为装饰类的一个实例对象属性
print("The first step begin")
self.decorated_class = decorated_class
print("The first step end")
def __call__(self, *args, **kwargs):
# 对被装饰的类执行初始化,并返回创建的实例对象
print("the second step")
return self.decorated_class(*args, **kwargs)
"""
此处即 DecoratedClass = Decorator(DecoratedClass), 当我们使用DecoratedClass(*agrs, **kwargs)创建
一个DecoratedClass的实例对象时,将自动调用__call__中实现的原本的DecoratedClass的实例对象初始化并返回该实例对象
"""
@Decorator
class DecoratedClass:
def __init__(self, action, des):
self.action = action
self.des = des
def getele(self):
return self.des
obj = DecoratedClass("被装饰的类", "python的类装饰器")
print(obj.getele())
# 输出:
# The first step begin
# The first step end
# the second step
# python的类装饰器
1.5 wraps的使用
在 Python 中使用装饰器时,通常会遇到一个问题:被装饰函数的元数据(比如函数名 name 和文档字符串 doc)会被覆盖,因为装饰器中通常会返回一个新的函数。为了解决这个问题,Python 的 functools 模块提供了一个装饰器 wraps,它可以保留原始函数的元数据。
下面我们通过一个例子来说明 wraps 的作用:
不带wraps的输出:
#!/usr/bin/env python
# coding=utf-8
# Author: Summer
# Time: 2024.03.29
def my_decorator(f):
def wrapper(*args, **kwargs):
"""Wrapper function"""
result = f(*args, **kwargs)
return result
return wrapper
@my_decorator
def say_hello(name):
"""Greet someone by their name."""
print(f"Hello, {name}!")
输出:
带wraps的输出:
#!/usr/bin/env python
# coding=utf-8
# Author: Summer
# Time: 2024.03.29
from functools import wraps
def my_decorator(f):
@wraps(f)
def wrapper(*args, **kwargs):
"""Wrapper function"""
result = f(*args, **kwargs)
return result
return wrapper
@my_decorator
def say_hello(name):
"""Greet someone by their name."""
print(f"Hello, {name}!")
say_hello("Alice")
print(say_hello.__name__)
print(say_hello.__doc__)
1.6 多装饰器执行顺序
在Python中有时会遇到多个装饰器同时装饰一个函数的场景,其装饰顺序与执行顺序为:靠近原函数的先进行装饰后执行,离原函数远的后装饰先执行。如下例:
#!/usr/bin/env python
# coding=utf-8
# Author: Summer
# Time: 2024.03.29
import datetime
import time
def decorator1(func):
def wrapper1(*args, **kwargs):
print(func.__name__)
print("这是第一个装饰器, 执行时间为: ", datetime.datetime.now())
time.sleep(5)
return func(*args, **kwargs)
return wrapper1
def decorator2(func):
def wrapper2(*args, **kwargs):
print(func.__name__)
print("这是第二个装饰器, 执行时间为: ", datetime.datetime.now())
time.sleep(5)
return func(*args, **kwargs)
return wrapper2
@decorator1 # 此处即 func5 = decorator1(decorator2(func5))
@decorator2 # 此处即 func5 = decorator2(func5)
def func5():
print("这是原函数,执行时间为: ", datetime.datetime.now())
func5()
输出:
wrapper2
这是第一个装饰器, 执行时间为: 2024-03-29 16:48:39.036366
func5
这是第二个装饰器, 执行时间为: 2024-03-29 16:48:44.041291
这是原函数,执行时间为: 2024-03-29 16:48:49.041935
执行过程:
1.在应用了两个装饰器 @decorator1 和 @decorator2 到 func5 上时,实际执行的顺序是从里到外,也就是先应用 @decorator2,然后应用 @decorator1。这导致 func5 实际上首先被 wrapper2 包装,再被 wrapper1 包装。
2.当你调用 func5() 时,会首先执行 decorator1 装饰器中的 wrapper1 函数。在这里,func.name 打印的是它所装饰的函数的名称,在这个例子中就是由 decorator2 返回的 wrapper2 函数的名称,所以打印 “wrapper2”。
3.接着,经过 time.sleep(5) 休眠之后,wrapper1 调用它包装的函数,即 wrapper2。然后,decorator2 中的 wrapper2 函数被执行,打印出它装饰的函数的名称。由于 wrapper2 是直接包装 func5 的,所以它打印出 “func5”,然后同样休眠 5 秒。
4.最后,执行到 func5 函数本身,打印出原函数的执行时间。