Python侧开28期-偕行-学习笔记-python内置装饰器

python内置装饰器

装饰器 作用 说明
@lru_cache 用来缓存一个函数的结果,这样后续调用相同参数的函数就不会再执行了。它对于计算量大或使用相同参数频繁调用的函数特别有用。 装饰函数
@total_ordering functools 模块中的 @total_ordering 装饰器用于根据定义的方法为 Python 类生成缺少的比较方法。补充:python可比较类,实现了以下方法的类: __eq__ (相等性比较)、 __ne__ (不等于比较)、__lt__(小于比较)、 __ge__ (大于等于比较)、 __gt__ (大于比较)、 __le__ (小于等于比较); 装饰类
@contextmanager contextlib 模块中的装饰器,可以帮我们实现上下文管理器机制,比如装饰一个文件操作方法,打开或者关闭文件的时候打印相应的信息; 装饰函数
@property 将一个函数装饰为一个变量,访问的时候只需要.函数名,而不需要.函数名() 装饰类中函数
@cached_property Python 3.8 为 functools 模块引入,将一个类的方法转换为一个属性,该属性的值计算一次,然后在实例的生命周期内作为普通属性缓存。 装饰类中方法
@classmethod 被装饰的方法为类方法,通过类名调用; 装饰类中方法
@staticmethod 被装饰的方法为静态方法,即不属于实例也不属于类,只是类中的一个普通方法,通过类名调用; 装饰类中方法
@dataclass Python 3.7引入,模块dataclasses 中的装饰器, 可以自动为一个类生成几个特殊的方法,如__init__、repreq 、__lt__等。 装饰类
@atexit.register atexit 模块的 @register 装饰器可以让我们在 Python 解释器退出时执行一个函数; 装饰函数

1、 @lru_cache(maxsize=128, typed=False)

  • 使用缓存技巧加速 Python 函数的最简单方法是使用 @lru_cache 装饰器。

  • 这个装饰器可以用来缓存一个函数的结果这样后续调用相同参数的函数就不会再执行了。它对于计算量大或使用相同参数频繁调用的函数特别有用。

  • 参数:

    • maxsize:默认缓存长度为128,如果将其设置为 None,LRU 功能将被禁用,并且缓存可以无限增长。

    • typed:If typed is True, arguments of different types will be cached separately.
      For example, f(3.0) and f(3) will be treated as distinct calls with
      distinct results.—如果 typed 为 True,则不同类型的参数将被单独缓存
      例如,f(3.0) 和 f(3) 将被视为不同的调用,其中
      独特的结果。

import time
from functools import lru_cache

# 使用递归方式计算斐波拉契数列---不使用缓存
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

def test_fibonacci():
    start_time = time.perf_counter()
    print(fibonacci(30))
    end_time = time.perf_counter()
    print(f"The execution time: {end_time - start_time:.8f} seconds")

# 使用递归方式计算斐波拉契数列---使用缓存
@lru_cache(maxsize=None)
def fibonacci_cache(n):
    if n < 2:
        return n
    return fibonacci_cache(n - 1) + fibonacci_cache(n - 2)

def test_fibonacci_cache():
    start_time = time.perf_counter()
    print(fibonacci_cache(30))
    end_time = time.perf_counter()
    print(f"The execution time: {end_time - start_time:.8f} seconds")

if __name__ == '__main__':
    # test_fibonacci() # 结果:The execution time: 0.16269750 seconds
    test_fibonacci_cache()# 结果:The execution time: 0.00006690 seconds
  • 说明:从上面的例子可以看出,一模一样的逻辑代码,使用了@lru_cache装饰器的函数运行时间0.00006690 s 比没有装饰器的函数运行时间0.16269750 s少非常多;

2、 @total_ordering

  • functools 模块中的 @total_ordering 装饰器用于根据定义的方法为 Python 类生成缺少的比较方法。

补充:什么是可比较类

  • 实现了以下方法的类: __eq__ (相等性比较)、 __ne__ (不等于比较)、__lt__ (小于比较)、 __ge__ (大于等于比较)、 __gt__ (大于比较)、 __le__ (小于等于比较)就是可比较类,要进行什么比较就实现什么方法;

@total_ordering的用法

  • 被装饰的类必须重写__eq__以及其他方法(__ne__,__lt__,__ge__,__gt__,__le__)中的一个,@total_ordering会自动实现没有重写的其他比较功能;
from functools import total_ordering

class Student:
    def __init__(self, name, grade):
        self.name = name
        self.grade = grade

@total_ordering
class StudentOrdering:
    def __init__(self, name, grade):
        self.name = name
        self.grade = grade

    # 重写相等比较方法
    def __eq__(self, other):
        return self.grade == other.grade

    # 重写小于比较方法
    def __lt__(self, other):
        return self.grade < other.grade

if __name__ == '__main__':
    # 非比较类进行比较直接报错
    student1 = Student("Alice", 85)
    student2 = Student("Bob", 75)
    print(student1 < student2)  # TypeError: '<' not supported between instances of 'Student' and 'Student'

    # 比较类
    student1 = StudentOrdering("Alice", 85)
    student2 = StudentOrdering("Bob", 75)
    student3 = StudentOrdering("Charlie", 85)
    print(student1 < student2)  # False
    print(student1 > student2)  # True
    print(student1 == student3)  # True
    print(student1 <= student3) # True
    print(student3 >= student2) # True
    print(student1 != student3) # False
  • 说明:
    • 以上案例中Student类没有实现比较方法,所以不是可比较类,如果将实例进行比较则直接报错:TypeError: ‘<’ not supported between instances of ‘Student’ and ‘Student’;
    • StudentOrdering重写了__eq__和__lt__方法,但是其他比较方法并没有重写,当使用了@total_ordering装饰器之后,其他比较方法会自动实现;

3、 @contextmanager

当需要定义一个自定义的上下文管理器的时候就需要使用该装饰器;

  • Python 有一个上下文管理器机制来帮助我们正确地管理资源。我们可以使用 with 语句打开一个文件,这样它会在写入后自动关闭。我们不需要显式调用 f.close() 函数来关闭文件。
with open("test.txt",'w') as f:
    f.write("Yang is writing!")
  • 但是,如果我们需要为一些特殊的需求定义一个自定义的上下文管理器。在这种情况下,@contextmanager 装饰器是我们的选择。
from contextlib import contextmanager

def file(filename, mode):
    print("The file is opening...")
    file = open(filename,mode)
    yield file
    print("The file is closing...")
    file.close()

# 自定义一个上下文管理器
@contextmanager
def file_manager(filename, mode):
    print("The file is opening...")
    file = open(filename,mode)
    yield file
    print("The file is closing...")
    file.close()

if __name__ == '__main__':
    # 不使用上下文装饰器直接报错:TypeError: 'generator' object does not support the context manager protocol
    # with file('test.txt', 'w') as f:
    #     f.write('hello world!')

    # 使用上下文装饰器则正常运行
    with file_manager('test.txt', 'w') as f:
        f.write('hello world!')
    """
    输出:
    The file is opening...
    The file is closing...
    """

4、 @property

  • Getter 和 setter 是面向对象编程 (OOP) 中的重要概念。对于类的每个实例变量,getter 方法返回它的值,而 setter 方法设置或更新它的值。鉴于此,getter 和 setter 也分别称为访问器和修改器。它们用于保护您的数据不被直接和意外地访问或修改。不同的 OOP 语言有不同的机制来定义 getter 和 setter

  • 在 Python 中,我们可以简使用 @property 装饰器来装饰一个类中方法,property是一种特殊的属性,访问它时会执行一段功能(函数)然后返回值;

  • 注意:

    • 1、被property装饰的函数,需要和成员变量名保持一致,这样才能实现对成员变量的访问,这也是使用property的要求,因为内部就是这么设计的;
    • 2、setter、getter、deleter需要使用@被property装饰的属性.进行调用;

为什么要使用getter和setter呢?

1、 访问限制控制

  • 通过getter和setter,我们可以灵活地控制一个成员变量的访问限制。如果只有getter,就是只读的;如果只有setter,就是只写的;如果同时有getter和setter,就是可读可写的。

2、 值的合法范围限制

  • 一个值,它总会有它的合理范围的。在某些必要的条件下,当某个值被访问时,我们应该返回一个合理的值;当某个值被修改是,我们应该拒绝不合理的修改。

3、 线程同步(并发、数据竞争)

  • 在多线程环境中,如果多个线程对同一个变量进行修改,会出现一些意想不到的问题。往往这个时候我们需要进行线程同步。
  • 如果我们直接使用成员变量,在每个访问这个变量的地方进程同步处理,这将是一个冗余而又不安全的做法。这个时候,通过setter就可以很好的解决问题。

4、 重构与可维护性
如果在初期没有考虑到实例的某些属性,可以直接使用property装饰器

可以使用debug模式运行下方代码更直观感受!

class Student:
    def __init__(self):
        # 私有实例属性,外部无法访问,但是要想访问怎么办?
        self._score = []

    """
    解决:定义一个见名之意的方法(一般和属性名一样),通过self去访问私有变量,
    然后用property装饰器进行装饰,这样就可以间接的去访问和操作私有变量
    """
    # 获取分数
    @property
    def score(self):
        return self._score

    # 设置分数
    @score.setter
    def score(self, s):
        self._score.append(s)

    # 删除分数
    @score.deleter
    def score(self):
        # 每次删除分数列表中的第一个值
        if len(self._score) > 0:
            self._score.pop(0)
        else:
            print(f"分数列表中没有分数")

    # 通过property装饰器新增一个属性
    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, name):
        self._name = name

if __name__ == '__main__':
    # 初始化学生
    Yang = Student()
    # 给学生设置分数,其实调用的是:@score.setter
    Yang.score=99
    Yang.score=98
    Yang.score=97
    # 获取分数,其实调佣的是:@property
    print(Yang.score)# [99, 98, 97]
    # 删除分数,其实调用的是:@score.deleter
    del Yang.score
    del Yang.score
    print(Yang.score)# [97]

    # 使用新增的name属性
    # 调用了@name.setter装饰的函数name
    Yang.name = "张三"
    # 调用了@property装饰的函数name
    print(Yang.name)

5、 @cached_property

Python 3.8 为 functool 模块引入,它可以将一个类的方法转换为一个属性,该属性的值计算一次,然后在实例的生命周期内作为普通属性缓存。

说明:

  • area方法使用@cached_property装饰,下方调用两次该属性,只有第一次执行了该函数,而第二次直接拿到结果,说明有缓存;
  • circumference方法使用@property装饰,下方调用两次该属性,两次都执行了该函数,说明没有缓存;
from functools import cached_property

class Circle:
    def __init__(self, radius):
        self.radius = radius

    # 面积
    @cached_property
    def area(self):
        return 3.14 * self.radius ** 2

    # 周长
    @property
    def circumference(self):
        return 3.14 * self.radius * 2

circle = Circle(10)
#访问被装饰的属性并缓存起来供其他地方使用--使用debug模式查看运行过程
print(circle.area)# 调用了@cached_property装饰的函数
print(circle.area)# 没有调用任何函数,而是直接就拿到结果---说明缓存的存在

# 通过debug模式发现,两次都去调用了 @property装饰的函数---说明没有缓存
print(circle.circumference)
print(circle.circumference)

6、 @classmethod和 @staticmethod

在 Python 类中,有 3 种可能的方法类型:

  • 实例方法:绑定到实例的方法。他们可以访问和修改实例数据。在类的实例上调用实例方法,它可以通过 self 参数访问实例数据。

  • 类方法:绑定到类的方法。他们不能修改实例数据。在类本身上调用类方法,它接收类作为第一个参数,通常命名为 cls。—通过类名.访问;

  • 静态方法:未绑定到实例或类的方法。常用于执行一组相关任务(例如数学计算)的实用程序类。通过将相关函数组织到类中的静态方法中,我们的代码将变得更有条理,也更容易理解。—通过类名.访问;

7、 @dataclass

  • @dataclass装饰器(Python 3.7引入)可以自动为一个类生成几个特殊的方法,如__init__、__repr__(输出格式)、__eq__、__lt__等。

  • 因此,它可以为我们节省大量编写这些基本方法的时间。如果一个类主要用于存储数据,那么@dataclass 装饰器是最好的选择。

from dataclasses import dataclass

@dataclass
class Teacher:
    name: str
    age: int

class Student:
    name: str
    age: int

teacher = Teacher("张三",18)
# student = Student("李四",16)# TypeError: Student() takes no arguments
print(teacher)# Teacher(name='张三', age=18)
  • 说明:
    • 使用@dataclass的Teacher类自动实现了__init__(构造方法)、repr(输出格式),所以可以传参进行实例化以及拥有输出格式;
    • Student类没有使用@dataclass装饰,只有默认的构造函数,所以传入参数创建对象的时候直接报错;

8、 @atexit.register

来自 atexit 模块的 @register 装饰器可以让我们在 Python 解释器退出时执行一个函数。

这个装饰器对于执行最终任务非常有用,例如释放资源或只是说再见!

import atexit

@atexit.register
def goodbye():
    print("Bye bye!")

if __name__ == '__main__':
    goodbye()
    print("Hello world!")
    """
    输出:
    Bye bye!
    Hello world!
    Bye bye!------说明:在程序运行结束之后再运行了一次goodbye()
    """