装饰器(Decorator)是 Python 中一种很有趣且非常强大的工具。简单来说,装饰器本质上就是一个函数,它能让你在不修改原函数代码的情况下,给函数增加新的功能。
参考内容:Python - 装饰器
# 基础知识
# 闭包
闭包概念:在一个内部函数中,对外部作用域的变量进行引用,并且外部函数的返回值为内部函数,那么内部函数就叫做闭包。
def outer_func (year): | |
def inner_func (month): | |
print (f'year:{year}, month:{month}') | |
return inner_func | |
closure_func = outer_func ('2023') | |
closure_func (12) |
调用 func 时,产生了闭包 inner_func,其持有外层函数的自由变量 year,当函数 func 的生命周期结束之后,year 这个变量依然存在,因为它被闭包引用了,不会被回收。
闭包的特点:内部函数可以读取外层函数内的局部变量,并让其常驻内存,不会被回收,所以注意内存泄漏
# 函数也是对象
在 Python 中,函数可以像变量一样被传递、赋值和返回。举个例子:
def greet (name): | |
return f"Hello, {name}!" | |
say_hello = greet | |
print (say_hello ("World")) # 输出:Hello, World! |
这里我们把函数 greet 赋值给了变量 say_hello,然后通过 say_hello 调用了 greet 函数。这证明了函数在 Python 中是可以像对象一样操作的。
# 装饰器的概念
装饰器本质上就是一个函数,它能让你在不修改原函数代码的情况下,给函数增加新的功能。
装饰器是一个返回函数的函数。
# 示例一:装饰器的执行过程
下面,让我们先用一个简单的示例了解装饰器的执行过程:
def my_decorator (func): | |
def wrapper (): | |
print ("Something is happening before the function is called.") | |
func () | |
print ("Something is happening after the function is called.") | |
return wrapper | |
def say_hello (): | |
print ("Hello!") | |
say_hello = my_decorator (say_hello) | |
say_hello () | |
# 输出 | |
# Something is happening before the function is called. | |
# Hello! | |
# Something is happening after the function is called. |
这里我们定义了一个简易的装饰器 my_decorator
,它接收一个函数 func
作为参数,并定义了一个内部函数 wrapper
来包裹 func
。在调用 func
前后, wrapper
分别打印了一些额外的信息。
然后,我们把 say_hello
函数传递给 my_decorator
并重新赋值给 say_hello
,这样 say_hello
就被装饰了。
简单来说,在上述例子中,我们实现了让函数 say_hello 在执行前后,执行额外的内容。
# 示例二:装饰器的简易运用
举个例子:如何计算函数的执行时间?
如下,你需要计算 add 函数的执行时间。
# 函数 | |
def add (a, b): | |
res = a + b | |
return res |
import time | |
def add (a, b) | |
start_time = time.time () | |
res = a + b | |
exec_time = time.time () - start_time | |
print ("add 函数,花费的时间是:{}".format (exec_time)) | |
return res |
这个时候,如果你又需要计算减法函数(sub)的时间。不用装饰器的话,你又得重复写一段减法的代码。
这样显得很麻烦,也不灵活,万一计算时间的代码有改动,你得每个函数都要改动。
所以,我们需要引入装饰器。
下列代码的两个函数实现了计算 n 的阶乘和计算斐波那契数列的第 n 个数,同时引入了装饰器进行计算函数执行时间
import time | |
# 定义装饰器 | |
def time_calc (func): | |
def wrapper (*args, **kargs): | |
start_time = time.time () | |
f = func (*args,**kargs) | |
exec_time = time.time () - start_time | |
print (f"Function {func.__name__} executed in {exec_time} seconds") | |
return f | |
return wrapper | |
# 使用装饰器 | |
@time_calc | |
def factorial_iterative (n) -> int: | |
# 计算 n 的阶乘 | |
result = 1 | |
for i in range (1, n + 1): | |
result *= i | |
return result | |
@time_calc | |
def fibonacci_iterative (n): | |
# 计算斐波那契数列的第 n 个数 | |
if n <= 0: | |
return 0 | |
elif n == 1: | |
return 1 | |
a, b = 0, 1 | |
for _ in range (2, n+1): | |
a, b = b, a + b | |
return b | |
factorial_iterative (20000) | |
fibonacci_iterative (20000) |
装饰器的作用:增强函数的功能,确切的说,可以装饰函数,也可以装饰类。
装饰器的原理:函数也是对象,函数可以像变量一样被传递、赋值和返回。
# 装饰器的基本用法
# 定义装饰器
装饰器可以传参,也可以不用传参。
# 定义一个自身不传参的装饰器
自身不传入参数的装饰器(采用两层函数定义装饰器)
def decorator (func): | |
def wrapper (*args,**kargs): | |
# 可以自定义传入的参数 | |
print (func.__name__) | |
# 返回传入的方法名参数的调用 | |
return func (*args,**kargs) | |
# 返回内层函数函数名 | |
return wrapper | |
@decorator | |
def f (): | |
pass | |
f () |
定义装饰器 decorator
- 上述代码定义了一个装饰器
decorator
,decorator
接受一个函数func
作为参数,并返回一个新的函数wrapper
。
定义包装函数 wrapper
wrapper
函数接受任意数量的位置参数(*args
)和关键字参数(**kargs
),因此它可以装饰接受任何参数的函数。- 在
wrapper
函数内部,首先打印出传入函数的名称(func.__name__
),然后调用传入的函数func
,并将传入的参数传递给这个函数。最后,decorator
返回wrapper
函数。
当
decorator
应用于另一个函数时,实际上是在调用wrapper
函数,而不是原始函数。通过这种方式,你可以在调用原始函数之前或之后添加额外的逻辑(例如在这个例子中,打印函数名称)。
# 定义一个自身传参的装饰器
自身传入参数的装饰器(采用三层函数定义装饰器)
def login (text): | |
def decorator (func): | |
def wrapper (*args,**kargs): | |
print (f"{text} -> {func.__name__}") | |
return func (*args,**kargs) | |
return wrapper | |
return decorator | |
@login ('this is a parameter of decorator') | |
def f (): | |
pass | |
f () |
定义装饰器 login
login
函数接受一个参数text
,这个参数将被用于装饰器内部。login
函数返回一个装饰器decorator
,这是一个闭包,因为它捕获了外部函数的变量text
。
定义内部装饰器 decorator
decorator
接受一个函数func
作为参数,这是将要被装饰的函数。
定义包装函数 wrapper
wrapper
函数接受任意数量的位置参数*args
和关键字参数**kargs
,这使得它可以装饰接受任何参数的函数。- 在 wrapper 函数内部,首先打印出由
login
函数传入的text
参数和被装饰函数的名称func.__name__
,然后wrapper
调用原始函数func
,并将传入的参数传递给这个函数。
返回包装函数
- 包装函数
wrapper
返回func
的调用结果,内部装饰器decorator
返回wrapper
函数,这样当装饰器应用于另一个函数时,实际上是在调用wrapper
函数。
# 使用装饰器
在上述装饰器格式中, decorator
是我们定义好的装饰器,我们有两种方法将装饰器运用于函数
** 方法一:直接调用 **
# 装饰器不传入参数时 | |
f = decorator (函数名) | |
f () | |
# 装饰器传入参数时 | |
f = (decorator (参数))(函数名) | |
f () |
如前面所说,在实际给函数 f
运用时,实际上是调用了装饰器 decorator
里的 wrapper
函数,并将 wrapper
赋给 f
。
** 方法二:采用语法糖 @符号**
Python 提供了一种更简洁的 @ 语法来使用装饰器,效果是一样的,但代码看起来更简洁、更易读。
# 装饰器不传入参数时 | |
@decorator | |
def f (): | |
pass | |
f () | |
# 装饰器传入参数时 | |
@login (参数) | |
def f (): | |
pass | |
f () |
# 系统装饰器
# @property
把类内方法当成属性来使用,必须要有返回值,相当于 getter
;
假如没有定义 @func.setter
修饰方法的话,就是只读属性
# property 应用场景
在获取、设置和删除对象属性的时候,需要额外做一些工作。比如在游戏编程中,设置敌人死亡之后需要播放死亡动画。
需要限制对象属性的设置和获取。比如用户年龄为只读,或者在设置用户年龄的时候有范围限制。
这时就可以使用 property 工具,它把方法包装成属性,让方法可以以属性的形式被访问和调用。
# property () 函数
语法:property (fget=None, fset=None, fdel=None, doc=None) -> property attribute
说明:
- fget 是获取属性值的方法。
- fset 是设置属性值的方法。
- fdel 是删除属性值的方法。
- doc 是属性描述信息。如果省略,会把 fget 方法的 docstring 拿来用(如果有的话)
示例代码;
class Student: | |
def __init__(self): | |
self._age = None | |
def get_age (self): | |
print (' 获取属性时执行的代码 ') | |
return self._age | |
def set_age (self, age): | |
print (' 设置属性时执行的代码 ') | |
self._age = age | |
def del_age (self): | |
print (' 删除属性时执行的代码 ') | |
del self._age | |
age = property (get_age, set_age, del_age, ' 学生年龄 ') | |
student = Student () | |
# 注意要用 类名。属性.__doc__ 的形式查看属性的文档字符串 | |
print (' 查看属性的文档字符串:' + Student.age.__doc__) | |
# 设置属性 | |
student.age = 18 | |
# 获取属性 | |
print (' 学生年龄为:' + str (student.age)) | |
# 删除属性 | |
del student.age | |
# 代码输出: | |
# 查看属性的文档字符串:学生年龄 | |
# 设置属性时执行的代码 | |
# 获取属性时执行的代码 | |
# 学生年龄为:18 | |
# 删除属性时执行的代码 |
# @property 装饰器
@property
语法糖提供了比 property ()
函数更简洁直观的写法。
- 被
@property
装饰的方法是获取属性值的方法,被装饰方法的名字会被用做属性名
。 - 被
@属性名.setter
装饰的方法是设置属性值的方法。 - 被
@属性名.deleter
装饰的方法是删除属性值的方法。
以下示例代码与使用 property ()
函数版本的代码等价:
class Student: | |
def __init__(self): | |
self._age = None | |
@property | |
def age (self): | |
print (' 获取属性时执行的代码 ') | |
return self._age | |
@age.setter | |
def age (self, age): | |
print (' 设置属性时执行的代码 ') | |
self._age = age | |
@age.deleter | |
def age (self): | |
print (' 删除属性时执行的代码 ') | |
del self._age | |
student = Student () | |
print (' 查看属性的文档字符串:' + Student.age.__doc__) | |
student.age = 18 | |
print (' 学生年龄为:' + str (student.age)) | |
del student.age |
# 注意事项
可以省略设置属性值的方法,此时该属性变成只读属性。如果此时仍然设置属性,会抛出异常 AttributeError: can't set attribute
。
如果报错 RecursionError: maximum recursion depth exceeded while calling a Python object
,很可能是对象属性名和 @property
装饰的方法名重名了,一般会在对象属性名前加一个下划线 _
避免重名,并且表明这是一个受保护的属性。
# @staticmethod
staticmethod
修饰过的方法叫静态方法,可以直接通过类调用方法,这样做的好处是执行效率比较高,也可以通过实例调用该方法。
静态方法,不需要表示自身对象的 self 和自身类的 cls 参数,就跟使用函数一样。
from datetime import datetime | |
import time | |
TIME_FORMAT_STR = "% Y/% m/% d/ % H:% M:% S" | |
class TimeUtil (): | |
@staticmethod | |
def timestamp_to_utc_str (timestamp: float, format_str=TIME_FORMAT_STR) -> str: | |
"""时间戳转 utc-0 时区的时间""" | |
datetime_obj: datetime = datetime.utcfromtimestamp (timestamp) | |
return datetime_obj.strftime (format_str) | |
@staticmethod | |
def timestamp_to_local_str (timestamp: float, format_str=TIME_FORMAT_STR) -> str: | |
"""时间戳转当前本地时区的时间""" | |
datetime_obj: datetime = datetime.fromtimestamp (timestamp) | |
return datetime_obj.strftime (format_str) | |
timeutil = TimeUtil () | |
utc_0 = timeutil.timestamp_to_utc_str (timestamp=time.time ()) | |
utc_loacl = timeutil.timestamp_to_local_str (timestamp=time.time ()) | |
print (f"utc-0: {utc_0}\nutc-loacl: {utc_loacl}") |
# @classmethod
classmethod
修饰过的方法叫类方法,可以直接通过类或者实例调用方法
类方法,不需要 self 参数,但第一个参数需要是表示自身类的 cls 参数。
利用 @classmethod
实现单例模式
from datetime import datetime | |
class SingletonBase (object): | |
__instance = None | |
@classmethod | |
def get_instance (cls, *arg, **kwargs): | |
if cls.__instance is None: | |
cls.__instance = cls () | |
cls.__instance.init (*arg, **kwargs) | |
return cls.__instance | |
def init (self, *arg, **kwargs): | |
pass | |
class TestMgr (SingletonBase): | |
def init (self, *arg, **kwargs): | |
print (f'arg:{arg}, kwargs:{kwargs}') | |
if __name__ == '__main__': | |
test_mgr = TestMgr.get_instance ('hello', 'world', time=datetime.now ().strftime ("% Y/% m/% d/ % H:% M:% S")) |
# 三种方法比较
** 实例方法、类方法、静态方法的区别 **
在定义静态类方法和类方法时, @staticmethod
装饰的静态方法里面,想要访问类属性或调用实例方法,必须需要把类名写上;
而 @classmethod
装饰的类方法里面,会传一个 cls
参数,代表本类,这样就能够避免手写类名的硬编码。
在调用静态方法和类方法时,实际上写法都差不多,一般都是通过 类名。静态方法 ()
或 类名。类方法 ()
。
也可以用实例化对象去调用静态方法和类方法,但为了和实例方法区分,最好还是用类去调用静态方法和类方法。
** 使用场景 **
在定义类的时候,假如不需要用到与类相关的属性或方法时,就用静态方法 @staticmethod
假如需要用到与类相关的属性或方法,然后又想表明这个方法是整个类通用的,而不是对象特异的,就可以使用类方法 @classmethod
class Demo (object): | |
text = "三种方法的比较" | |
def instance_method (self): | |
print ("调用实例方法") | |
@classmethod | |
def class_method (cls): | |
print ("调用类方法") | |
print (f"在类方法中 访问类属性 text: {format (cls.text)}") | |
print (f"在类方法中 调用实例方法 instance_method: {format (cls ().instance_method ())}") | |
@staticmethod | |
def static_method (): | |
print ("调用静态方法") | |
print (f"在静态方法中 访问类属性 text: {format (Demo.text)}") | |
print (f"在静态方法中 调用实例方法 instance_method: {format (Demo ().instance_method ())}") | |
if __name__ == "__main__": | |
# 实例化对象 | |
d = Demo () | |
# 对象可以访问 实例方法、类方法、静态方法 | |
# 通过对象访问 text 属性 | |
print (d.text) | |
# 通过对象调用实例方法 | |
d.instance_method () | |
# 通过对象调用类方法 | |
d.class_method () | |
# 通过对象调用静态方法 | |
d.static_method () | |
# 类可以访问类方法、静态方法 | |
# 通过类访问 text 属性 | |
print (Demo.text) | |
# 通过类调用类方法 | |
Demo.class_method () | |
# 通过类调用静态方法 | |
Demo.static_method () |
# @functools.wraps
通过前面的内容,我们已经知道,经过装饰器之后的函数不是原来的函数,虽然原来的函数还存在,但是真正调用的是装饰后生成的新函数。
那岂不是打破了 “不能修改原函数” 的规则?
是的,看下面的示例:
def auth (permission): | |
def _auth (func): | |
def wrapper (*args, **kwargs): | |
print (f"func name {func.__name__}") | |
return func (*args, **kwargs) | |
return wrapper | |
return _auth | |
@auth ("add") | |
def add (a, b): | |
"""求和运算""" | |
print (a + b) | |
print (add) | |
print (add.__name__) | |
print (add.__doc__) |
输出如下
<function auth.<locals>._auth.<locals>.wrapper at 0x000002B851E19300> | |
wrapper | |
None |
为了消除装饰器对原函数的影响,我们需要伪装成原函数,拥有原函数的属性,看起来就像是同一个人一样。
functools
为我们提供了便捷的方式,只需这样:
import functools # 注意此处 | |
def auth (permission): | |
def _auth (func): | |
@functools.wraps (func) # 注意此处 | |
def wrapper (*args, **kwargs): | |
print (f"func name {func.__name__}") | |
return func (*args, **kwargs) | |
return wrapper | |
return _auth | |
@auth ("add") | |
def add (a, b): | |
"""求和运算""" | |
print (a + b) | |
print (add) | |
print (add.__name__) | |
print (add.__doc__) |
输出结果
<function add at 0x000001F5F62FFBA0> | |
add | |
求和运算 |
functools.wraps
对我们的装饰器函数进行了装饰之后, add
表面上看起来还是 add
。
functools.wraps
内部通过 partial
和 update_wrapper
对函数进行再加工,将原始被装饰函数 (add) 的属性拷贝给装饰器函数 (wrapper)。
# 装饰器的四种类型
python 装饰器的 4 种类型:函数装饰函数、函数装饰类、类装饰函数、类装饰类
# 函数装饰函数
def wrapFun (func): | |
def inner (a, b): | |
print ('function name:', func.__name__) | |
r = func (a, b) | |
return r | |
return inner | |
@wrapFun | |
def myadd (a, b): | |
return a + b | |
print (myadd (2, 3)) |
# 函数装饰类
def wrapClass (cls): | |
def inner (a): | |
print ('class name:', cls.__name__) | |
return cls (a) | |
return inner | |
@wrapClass | |
class Foo (): | |
def __init__(self, a): | |
self.a = a | |
def fun (self): | |
print ('self.a =', self.a) | |
m = Foo ('xiemanR') | |
m.fun () |
# 类装饰函数
class ShowFunName (): | |
def __init__(self, func): | |
self._func = func | |
def __call__(self, a): | |
print ('function name:', self._func.__name__) | |
return self._func (a) | |
@ShowFunName | |
def Bar (a): | |
return a | |
print (Bar ('xiemanR')) |
# 类装饰类
class ShowClassName (object): | |
def __init__(self, cls): | |
self._cls = cls | |
def __call__(self, a): | |
print ('class name:', self._cls.__name__) | |
return self._cls (a) | |
@ShowClassName | |
class Foobar (object): | |
def __init__(self, a): | |
self.value = a | |
def fun (self): | |
print (self.value) | |
a = Foobar ('xiemanR') | |
a.fun () |
# 多装饰器
有的时候,我们需要用多个装饰器装饰一个函数,例如:
def wrapFun (func): | |
def inner (*args,**kargs): | |
print (f"装饰器 1 before {func.__name__}") | |
f = func (*args,**kargs) | |
print (f"装饰器 1 after {func.__name__}") | |
return f | |
return inner | |
def decorator (func): | |
def wrapper (*args,**kargs): | |
print (f"装饰器 2 before {func.__name__}") | |
f = func (*args,**kargs) | |
print (f"装饰器 2 after {func.__name__}") | |
return f | |
return wrapper | |
@wrapFun | |
@decorator | |
def f (): | |
print ("func") | |
f () |
上述代码输出
装饰器 1 before wrapper | |
装饰器 2 before f | |
func | |
装饰器 2 after f | |
装饰器 1 after wrapper |
在上述代码中,f 被 @wrapFun
和 @decorato
r 装饰,我们可以将装饰器的执行过程理解成 包装
函数: 装饰器会将函数包裹起来,再剥离
被装饰的函数 f,首先被内层的 @decorator
包裹起来,此时的 f 变为了 @decorator
的包装函数 wrapper
,之后,外层 @wrapFun
再将 wrapper
包裹起来,变为 inner
。
因此,实际运行的函数为最外层装饰器 @wrapFun
的包装函数 inner
,该函数接收到的是内层的包装函数 wrapper
,内层包装函数再接收 f 。
可以想象成装饰器将 f 包裹起来:
| 外层 before | 内存 before | func | 内层 after | 外层 after |
——————————— 执行顺序 ———————————>
↓外层 @wrapFun | |
↓内层 @decorator | |
↓ def f (): | |
↓ print ("func") |