装饰器(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 内部通过 partialupdate_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")