faster python——dataclass&cache

目录

  • dataclass
    • 参数及版本演进
    • 基本使用
      • 借助dataclass定义的魔法函数快速初始化对象
      • 数据类的替代方案(与具名元组的区别)
      • 基本数据类
      • 默认值
      • 类型提示
      • Frozen(不可变) 实例
      • 后期初始化处理
      • 继承
  • cache
    • lru_cache
    • functools.cache
    • 测试cache缓存效果
    • functools.cached_property(func)
  • 参考翻译

dataclass

Python的dataclass是Python 3.7版本引入的一个装饰器,用于简化创建和管理包含数据的类(即数据类)。它的目的是减少编写样板代码,并提供了一种简单的方式来定义类,其中主要包含用于存储数据的字段。dataclass为类自动生成一些常见的特殊方法,如__init__()__repr__()__eq__()等,从而减少了冗余的代码。

以下是dataclass的一些重要特性和用法:

  1. 装饰器:使用@dataclass装饰器可以将一个普通的类转换为数据类。

  2. 字段声明:通过在类的属性上使用类型注解,可以声明类的字段。这些字段将存储实例的数据。

    from dataclasses import dataclass
    
    @dataclass
    class Point:
        x: int
        y: int
    
  3. 自动生成特殊方法dataclass会自动生成__init__()__repr__()__eq__()等特殊方法,无需手动编写。

  4. 默认值和默认工厂函数:你可以为字段提供默认值或者默认工厂函数,以便在创建实例时使用。

    from dataclasses import dataclass
    
    @dataclass
    class Point:
        x: int = 0
        y: int = 0
    
  5. 可变与不可变数据类dataclass默认生成可变数据类,但你也可以使用frozen=True参数创建不可变数据类,即一旦创建实例,就不能再修改其属性值。

    from dataclasses import dataclass
    
    @dataclass(frozen=True)
    class Point:
        x: int
        y: int
    
  6. 继承:你可以在数据类中使用继承,但需要小心,因为基类的特殊方法可能不会像你期望的那样工作。

  7. 自定义特殊方法:如果需要自定义某个特殊方法,你可以手动实现它们,它们将覆盖自动生成的方法。

  8. 字段的默认排序dataclass支持通过order=True参数为字段启用默认的比较和排序功能。

    from dataclasses import dataclass
    
    @dataclass(order=True)
    class Person:
        name: str
        age: int
    
  9. 数据类的替代方案namedtuple是Python标准库中的另一种轻量级数据类,它也用于表示只包含数据的类。然而,与namedtuple不同,dataclass允许你更灵活地自定义类的行为。

dataclass是一个强大的工具,可用于编写更干净、更易读、更易维护的代码,特别是当你需要创建大量包含数据的类时。

参数及版本演进

The parameters to dataclass() are:

init: If true (the default), a init() method will be generated.

If the class already defines init(), this parameter is ignored.

repr: If true (the default), a repr() method will be generated. The generated repr string will have the class name and the name and repr of each field, in the order they are defined in the class. Fields that are marked as being excluded from the repr are not included. For example: InventoryItem(name=‘widget’, unit_price=3.0, quantity_on_hand=10).

If the class already defines repr(), this parameter is ignored.

eq: If true (the default), an eq() method will be generated. This method compares the class as if it were a tuple of its fields, in order. Both instances in the comparison must be of the identical type.

If the class already defines eq(), this parameter is ignored.

order: If true (the default is False), lt(), le(), gt(), and ge() methods will be generated. These compare the class as if it were a tuple of its fields, in order. Both instances in the comparison must be of the identical type. If order is true and eq is false, a ValueError is raised.

If the class already defines any of lt(), le(), gt(), or ge(), then TypeError is raised.

unsafe_hash: If False (the default), a hash() method is generated according to how eq and frozen are set.

hash() is used by built-in hash(), and when objects are added to hashed collections such as dictionaries and sets. Having a hash() implies that instances of the class are immutable. Mutability is a complicated property that depends on the programmer’s intent, the existence and behavior of eq(), and the values of the eq and frozen flags in the dataclass() decorator.

By default, dataclass() will not implicitly add a hash() method unless it is safe to do so. Neither will it add or change an existing explicitly defined hash() method. Setting the class attribute hash = None has a specific meaning to Python, as described in the hash() documentation.

If hash() is not explicitly defined, or if it is set to None, then dataclass() may add an implicit hash() method. Although not recommended, you can force dataclass() to create a hash() method with unsafe_hash=True. This might be the case if your class is logically immutable but can nonetheless be mutated. This is a specialized use case and should be considered carefully.

Here are the rules governing implicit creation of a hash() method. Note that you cannot both have an explicit hash() method in your dataclass and set unsafe_hash=True; this will result in a TypeError.

If eq and frozen are both true, by default dataclass() will generate a hash() method for you. If eq is true and frozen is false, hash() will be set to None, marking it unhashable (which it is, since it is mutable). If eq is false, hash() will be left untouched meaning the hash() method of the superclass will be used (if the superclass is object, this means it will fall back to id-based hashing).

frozen: If true (the default is False), assigning to fields will generate an exception. This emulates read-only frozen instances. If setattr() or delattr() is defined in the class, then TypeError is raised. See the discussion below.

match_args: If true (the default is True), the match_args tuple will be created from the list of parameters to the generated init() method (even if init() is not generated, see above). If false, or if match_args is already defined in the class, then match_args will not be generated.

New in version 3.10.

kw_only: If true (the default value is False), then all fields will be marked as keyword-only. If a field is marked as keyword-only, then the only effect is that the init() parameter generated from a keyword-only field must be specified with a keyword when init() is called. There is no effect on any other aspect of dataclasses. See the parameter glossary entry for details. Also see the KW_ONLY section.

New in version 3.10.

slots: If true (the default is False), slots attribute will be generated and new class will be returned instead of the original one. If slots is already defined in the class, then TypeError is raised.

New in version 3.10.

Changed in version 3.11: If a field name is already included in the slots of a base class, it will not be included in the generated slots to prevent overriding them. Therefore, do not use slots to retrieve the field names of a dataclass. Use fields() instead. To be able to determine inherited slots, base class slots may be any iterable, but not an iterator.

weakref_slot: If true (the default is False), add a slot named “weakref”, which is required to make an instance weakref-able. It is an error to specify weakref_slot=True without also specifying slots=True.

New in version 3.11.

译文:

dataclass()的参数如下:

  • init(默认为True):如果为True,将生成一个__init__()方法。如果类已经定义了__init__()方法,则忽略此参数。

  • repr(默认为True):如果为True,将生成一个__repr__()方法。生成的repr字符串将包含类名和每个字段的名称和repr表示,按照它们在类中定义的顺序排列。被标记为在repr中排除的字段不会包含在其中。例如:InventoryItem(name='widget', unit_price=3.0, quantity_on_hand=10)

  • eq(默认为True):如果为True,将生成一个__eq__()方法。此方法将比较类,就好像它是其字段的元组,按顺序排列。比较中的两个实例必须是相同类型的。

  • order(默认为False):如果为True,则会生成__lt__()__le__()__gt__()__ge__()方法。这些方法会按照类似于元组的方式比较类的字段,按顺序排列。比较中的两个实例必须是相同类型的。如果order为True且eq为False,将引发ValueError

  • unsafe_hash(默认为False):如果为False(默认值),将根据eqfrozen的设置生成一个__hash__()方法。__hash__()用于内置的hash()函数,以及当对象添加到哈希集合(如字典和集合)时。拥有__hash__()意味着该类的实例是不可变的。不可变性是一个依赖于程序员意图、__eq__()的存在和行为,以及dataclass()装饰器中的eqfrozen标志值的复杂属性。默认情况下,除非安全,否则dataclass()不会隐式添加__hash__()方法。也不会添加或更改已经显式定义的__hash__()方法。设置类属性__hash__ = None对Python具有特定的含义,如__hash__()文档中所描述的那样。如果__hash__()没有被显式定义,或者被设置为None,那么dataclass()可能会添加一个隐式的__hash__()方法。尽管不建议,但你可以通过unsafe_hash=True来强制dataclass()创建一个__hash__()方法。如果你的类在逻辑上是不可变的但仍然可以被改变,这可能是一种特殊的用例,应该谨慎考虑。

  • frozen(默认为False):如果为True,将分配给字段将引发异常。这模拟了只读的不可变实例。如果类中定义了__setattr__()__delattr__()方法,则会引发TypeError。请参阅下面的讨论。

  • match_args(默认为True):如果为True(默认值为True),将从生成的__init__()方法的参数列表中创建__match_args__元组(即使__init__()未生成,请参见上文)。如果为False,或者如果类中已经定义了__match_args__,则不会生成__match_args__

  • kw_only(默认为False):如果为True(默认值为False),则所有字段都将标记为仅关键字参数。如果字段标记为仅关键字参数,则唯一的效果是生成的__init__()参数必须在调用__init__()时以关键字的方式指定。这不会影响dataclasses的任何其他方面。有关详细信息,请参阅参数词汇表中的条目。还请参阅KW_ONLY部分。

  • slots(默认为False):如果为True,将生成__slots__属性,并返回新类,而不是原始类。如果类中已经定义了__slots__,则会引发TypeError

  • weakref_slot(默认为False):如果为True(默认为False),则添加一个名为“weakref”的插槽,需要使实例可弱引用。在不指定slots=True的情况下,指定weakref_slot=True是错误的。

这些是dataclass()的参数,用于配置数据类的行为。请注意,有些参数在不同的Python版本中可能会有所不同。

基本使用

借助dataclass定义的魔法函数快速初始化对象

数据类(data class)已经预先实现了基本功能。例如,您可以立即实例化、打印和比较数据类实例:

>>> queen_of_hearts = DataClassCard('Q', 'Hearts')
>>> queen_of_hearts.rank
'Q'
>>> queen_of_hearts
DataClassCard(rank='Q', suit='Hearts')
>>> queen_of_hearts == DataClassCard('Q', 'Hearts')
True

将其与普通类进行比较。一个最简单的普通类可能如下所示:

class RegularCard:
    def __init__(self, rank, suit):
        self.rank = rank
        self.suit = suit

尽管要编写的代码不多,但您已经可以看到样板痛苦的迹象:为了初始化对象,等级(rank)和花色(suit)都被重复了三次。此外,如果尝试使用这个普通类,您会注意到对象的表示不太描述性,而且出现了一个奇怪的问题,即一张红心皇后与另一张红心皇后不相等:

>>> queen_of_hearts = RegularCard('Q', 'Hearts')
>>> queen_of_hearts.rank
'Q'
>>> queen_of_hearts
<__main__.RegularCard object at 0x7fb6eee35d30>
>>> queen_of_hearts == RegularCard('Q', 'Hearts')
False

看起来数据类在幕后帮助我们。默认情况下,数据类实现了.__repr__()方法以提供漂亮的字符串表示,以及一个.__eq__()方法,可以进行基本的对象比较。为了使RegularCard类模仿上面的数据类,您需要添加这些方法:

class RegularCard:
    def __init__(self, rank, suit):
        self.rank = rank
        self.suit = suit

    def __repr__(self):
        return (f'{self.__class__.__name__}'
                f'(rank={self.rank!r}, suit={self.suit!r})')

    def __eq__(self, other):
        if other.__class__ is not self.__class__:
            return NotImplemented
        return (self.rank, self.suit) == (other.rank, other.suit)

dataclass的类属性实际上是self对象属性,使用cls赋值是无法修改其内容的:

from dataclasses import dataclass


@dataclass
class A:
    b: int
    v: int
    a: str

    @classmethod
    def set_a(cls, val):
        cls.a = val

    def set_a2(self, val):
        self.a = val


if __name__ == '__main__':
    a = A(1, 1, "1")
    print(a)

    a.A = 2
    print(a)

    a.set_a(10)
    print(a)

    a.set_a2(20)
    print(a)

    print(a.__dict__)

A(b=1, v=1, a='1')
A(b=1, v=1, a='1')
A(b=1, v=1, a='1')
A(b=1, v=1, a=20)
{'b': 1, 'v': 1, 'a': 20, 'A': 2}

要区分类属性和实例属性的区别:

class B:
    a: int
    b: int
    c: int

    def __init__(self, a1, b1, c1):
        self.a = a1
        self.b = b1
        self.c = c1

    @classmethod
    def seta(cls, val):
        cls.a = val

    def seta2(self, val):
        self.a = val

在你提供的代码中,类B定义了一个名为a的类属性,它是一个整数。然后,在B类的__init__()方法中,通过self.a来给实例对象的a属性赋值。这个self.a是给类B的实例对象的a属性赋值,而不是给类属性B.a赋值。

  • 类属性是属于类本身的属性,它们被所有类的实例对象所共享。在你的代码中,B.a是一个类属性。
  • 实例属性是属于类的特定实例对象的属性。在__init__()方法中,你使用self.a来给实例对象的a属性赋值,因此这个属性只能通过特定的B类实例访问和修改。

下面是一个示例,说明了类属性和实例属性之间的区别:

# 创建两个B类的实例对象
obj1 = B(1, 2, 3)
obj2 = B(4, 5, 6)

# 修改类属性B.a
B.seta(100)

# 查看实例属性和类属性的值
print(obj1.a)  # 输出:1,因为obj1的a属性是实例属性,不受B.a的影响
print(obj2.a)  # 输出:4,同样,obj2的a属性是实例属性,不受B.a的影响
print(B.a)      # 输出:100,这是类属性B.a的值

总结一下,self.a__init__()方法中用于给实例对象的a属性赋值,不会影响类属性B.a。如果你想要修改类属性B.a,可以使用@classmethod修饰的seta()方法,或者直接通过类名B来访问和修改。

数据类的替代方案(与具名元组的区别)

对于简单的数据结构,您可能已经使用了元组或字典。您可以使用以下任一方式表示红心皇后卡片:

>>> queen_of_hearts_tuple = ('Q', 'Hearts')
>>> queen_of_hearts_dict = {'rank': 'Q', 'suit': 'Hearts'}

这是可行的。然而,这会将很多责任放在您作为程序员的肩上:

  • 您需要记住queen_of_hearts_...变量表示一张卡片。
  • 对于元组版本,您需要记住属性的顺序。编写('Spades', 'A')可能会损坏您的程序,但可能不会提供易于理解的错误消息。
  • 如果使用字典版本,您必须确保属性的名称一致。例如,{'value': 'A', 'suit': 'Spades'}不会按预期工作。

此外,使用这些结构不是理想的:

>>> queen_of_hearts_tuple[0]  # 没有命名访问
'Q'
>>> queen_of_hearts_dict['suit']  # 使用.suit会更好
'Hearts'

更好的选择是具名元组(namedtuple)。它一直用于创建可读性强的小数据结构。实际上,我们可以使用具名元组来重新创建上面的数据类示例,如下所示:

from collections import namedtuple

NamedTupleCard = namedtuple('NamedTupleCard', ['rank', 'suit'])

这个具名元组NamedTupleCard的定义将产生与我们的DataClassCard示例完全相同的输出:

>>> queen_of_hearts = NamedTupleCard('Q', 'Hearts')
>>> queen_of_hearts.rank
'Q'
>>> queen_of_hearts
NamedTupleCard(rank='Q', suit='Hearts')
>>> queen_of_hearts == NamedTupleCard('Q', 'Hearts')
True

那么为什么还要使用数据类呢?首先,数据类提供了远比您迄今为止看到的更多功能。同时,具名元组具有一些其他特性,这些特性未必是理想的。按设计,具名元组是一个普通的元组。这可以从比较中看出,例如:

>>> queen_of_hearts == ('Q', 'Hearts')
True

虽然这可能看起来很好,但它对自己类型的缺乏意识可能导致难以察觉和难以找到的错误,特别是因为它也会愉快地比较两个不同的具名元组类:

>>> Person = namedtuple('Person', ['first_initial', 'last_name'])
>>> ace_of_spades = NamedTupleCard('A', 'Spades')
>>> ace_of_spades == Person('A', 'Spades')
True

具名元组还具有一些限制。例如,向具名元组的某些字段添加默认值很困难。具名元组天生是不可变的。也就是说,具名元组的值永远不会改变。在某些应用中,这是一个很棒的特性,但在其他情况下,拥有更多灵活性会更好:

>>> card = NamedTupleCard('7', 'Diamonds')
>>> card.rank = '9'
AttributeError: can't set attribute

数据类不会取代所有对具名元组的使用。例如,如果您需要使数据结构的行为类似于元组,那么具名元组是一个很好的选择!

基本数据类

创建一个Position类,用于表示地理位置,包括名称、经度和纬度:

from dataclasses import dataclass

@dataclass
class Position:
    name: str
    lon: float
    lat: float

使其成为数据类的是在类定义正上方的@dataclass装饰器。在class Position:行下面,只需列出您想要在数据类中的字段。用于字段的:标记使用了Python 3.6中的一个新特性,叫做变量注释,比如strfloat

这几行代码就是您需要的全部内容。新类已经可以使用了:

>>> pos = Position('Oslo', 10.8, 59.9)
>>> print(pos)
Position(name='Oslo', lon=10.8, lat=59.9)
>>> pos.lat
59.9
>>> print(f'{pos.name} is at {pos.lat}°N, {pos.lon}°E')
Oslo is at 59.9°N, 10.8°E

您还可以类似于创建具名元组的方式创建数据类。以下内容(几乎)等同于上面Position定义的方式:

from dataclasses import make_dataclass

Position = make_dataclass('Position', ['name', 'lat', 'lon'])

数据类是普通的Python类。唯一使它与众不同的是,它已经为您实现了基本的数据模型方法,如.__init__().__repr__().__eq__()

默认值

向数据类字段添加默认值非常简单:

from dataclasses import dataclass

@dataclass
class Position:
    name: str
    lon: float = 0.0
    lat: float = 0.0

这与您在普通类的.__init__()方法定义中指定默认值的方式完全相同:

>>> Position('Null Island')
Position(name='Null Island', lon=0.0, lat=0.0)
>>> Position('Greenwich', lat=51.8)
Position(name='Greenwich', lon=0.0, lat=51.8)
>>> Position('Vancouver', -123.1, 49.3)
Position(name='Vancouver', lon=-123.1, lat=49.3)

类型提示

实际上,在定义数据类中的字段时,添加某种形式的类型提示是强制的。没有类型提示,该字段将不会成为数据类的一部分。但是,如果不想在数据类中添加显式类型提示,可以使用typing.Any

from dataclasses import dataclass
from typing import Any

@dataclass
class WithoutExplicitTypes:
    name: Any
    value: Any = 42

虽然在使用数据类时需要以某种形式添加类型提示,但这些类型在运行时不会强制执行。以下代码可以正常运行而不会出现问题:

>>> Position(3.14, 'pi day', 2018)
Position(name=3.14, lon='pi day', lat=2018)

这就是Python中类型提示通常的工作方式:Python始终是一种动态类型语言。要实际捕获类型错误,可以在源代码上运行类型检查器(如Mypy)。

Frozen(不可变) 实例

Frozen 实例是在初始化对象后无法修改其属性的对象。

在 Python 中创建对象的不可变属性是一项艰巨的任务(无法创建真正不可变的 Python 对象)

以下是期望不可变对象能够做到的:

>>> a = Number(10) #Assuming Number class is immutable
>>> a.val = 10 # Raises Error

有了dataclass,就可以通过使用dataclass装饰器作为可调用对象配合参数frozen=True来定义一个frozen对象。

当实例化一个frozen对象时,任何企图修改对象属性的行为都会引发FrozenInstanceError。

@dataclass(frozen = True)
class Number:
    val: int = 0
>>> a = Number(1)
>>> a.val
>>> 1
>>> a.val = 2
>>> Traceback (most recent call last):
 File “<stdin>, line 1, in <module>
 File “<string>, line 3, in __setattr__
dataclasses.FrozenInstanceError: cannot assign to field ‘val’

因此,一个frozen 实例是一种很好方式来存储:

  1. 常数
  2. 设置

这些通常不会在应用程序的生命周期内发生变化,任何企图修改它们的行为都应该被禁止。

后期初始化处理

有了dataclass,需要定义一个__init__方法来将变量赋给self这种初始化操作已经得到了处理。但是失去了在变量被赋值之后立即需要的函数调用或处理的灵活性。

幸运的是,使用post_init方法已经能够处理后期初始化操作。

import math
@dataclass
class FloatNumber:
    val: float = 0.0
    def __post_init__(self):
        self.decimal, self.integer = math.modf(self.val)
>>> a = Number(2.2)
>>> a.val
>>> 2.2
>>> a.integer
>>> 2.0
>>> a.decimal
>>> 0.2

继承

Dataclasses支持继承,就像普通的Python类一样。

因此,父类中定义的属性将在子类中可用。

@dataclass
class Person:
    age: int = 0
    name: str
@dataclass
class Student(Person):
    grade: int
>>> s = Student(20, "John Doe", 12)
>>> s.age
>>> 20
>>> s.name
>>> "John Doe"
>>> s.grade
>>> 12

由于__post_init__只是另一个函数,因此必须以传统方式调用它:

@dataclass
class A:
    a: int
    def __post_init__(self):
        print("A")
@dataclass
class B(A):
    b: int
    def __post_init__(self):
        print("B")
>>> a = B(1,2)
>>> B

因为它是父类的函数,所以可以用super来调用它。

@dataclass
class B(A):
    b: int
    def __post_init__(self):
        super().__post_init__() # 调用 A 的 post init
        print("B")
>>> a = B(1,2)
>>> A
    B

cache

在Python中,functools模块提供了一些有用的装饰器和函数,用于实现缓存和性能优化。以下是functools模块中的一些常用缓存相关函数和装饰器的详细解释:

  1. lru_cache(最近最少使用缓存)

    lru_cache 是一个装饰器,用于缓存函数的结果,以避免多次计算相同的输入。它可以有效地提高函数的性能,特别是对于计算成本高的函数。

    • maxsize:可选参数,指定缓存的大小(缓存的最大元素数量),如果设置为 None,则缓存可以无限大。
    • typed:可选参数,如果设置为 True,则不同类型的参数将分别缓存结果。默认值为 False

    使用示例:

    from functools import lru_cache
    
    @lru_cache(maxsize=None)
    def fibonacci(n):
        if n < 2:
            return n
        return fibonacci(n-1) + fibonacci(n-2)
    
  2. cache(Python 3.9+)

    cache 是 Python 3.9+ 新增的一个装饰器,与 lru_cache 类似,用于缓存函数的结果。它可以将函数的结果缓存以避免重复计算,但不支持具体的大小限制。

    使用示例:

    from functools import cache
    
    @cache
    def factorial(n):
        if n < 2:
            return 1
        return n * factorial(n-1)
    
  3. cached_property(Python 3.8+)

    cached_property 是 Python 3.8+ 新增的一个装饰器,用于将方法转换为只读属性。方法的结果将被缓存,以避免重复计算。

    使用示例:

    from functools import cached_property
    
    class Circle:
        def __init__(self, radius):
            self.radius = radius
    
        @cached_property
        def area(self):
            return 3.14 * self.radius * self.radius
    

    在上面的示例中,area 方法被转换为一个只读属性,它的值在第一次访问后会被缓存,以提高性能。

这些装饰器和函数是 Python 中用于缓存和性能优化的强大工具,可以根据具体的需求选择适当的装饰器来提高函数的性能和效率。在Python 3.9+中,cache 装饰器和 Python 3.8+中的 cached_property 提供了更便捷的方式来实现缓存。

lru_cache

一般用于缓存的内存空间是固定的,当有更多的数据需要缓存的时候,需要将已缓存的部分数据清除后再将新的缓存数据放进去。需要清除哪些数据,就涉及到了缓存置换的策略,其中,LRU(Least Recently Used,最近最少使用)是很常见的一个,也是 Python 中提供的缓存置换策略。lru_cache比起成熟的缓存系统还有些不足之处,比如它不能设置缓存的时间,只能等到空间占满后再利用LRU算法淘汰出空间出来,并且不能自定义淘汰算法,但在简单的场景中很适合使用。

functools.lru_cache 函数是一个装饰器,为函数提供缓存功能。在下次以相同参数调用时直接返回上一次的结果,缓存 maxsize 组传入参数,用于节约高开销或I/O函数的调用时间.被 lru_cache 修饰的函数在被相同参数调用的时候,后续的调用都是直接从缓存读结果,而不用真正执行函数。maxsize=None,则LRU特性被禁用,且缓存可无限增长.如果 typed=True(注意,在 functools32 中没有此参数),则不同参数类型的调用将分别缓存,例如 f(3) 和 f(3.0)会分别缓存。

#若,maxsize=None,则LRU特性被禁用,且缓存可无限增长.
@lru_cache(maxsize=128, typed=False)

lru_cache缓存装饰器提供的功能有:

  1. 缓存被装饰对象的结果(基础功能)
  2. 获取缓存信息
  3. 清除缓存内容
  4. 根据参数变化缓存不同的结果
  5. LRU算法当缓存数量大于设置的maxsize时清除最不常使用的缓存结果

查看函数当前的缓存信息可以使用如下方法,比如查看func函数 。

# 查看函数缓存信息
cache_info = func.cache_info()
print(cache_info)
#或者
print(func.cache_info())

输出结果类似:hits代表命中缓存次数,misses代表未命中缓存次数,maxsize代表允许的最大存储结果数目,currsize表示当前存储的结果数据。

CacheInfo(hits=3, misses=2, maxsize=1, currsize=1)

清除缓存

func.cache_clear()

注意1:

查看func必须是被装饰器装饰的函数不能是调用函数。下列中print(func.cache_info())生效 print(func_main().cache_info())不生效。同时有多个缓存函数或着缓存函数需要多次嵌套调用才能调用到时也不生效。(显示缓存命中次数是0,但是实际缓存是命中的。)

@lru_cache(maxsize=128, typed=False)
def func():
    pass

def func_main():
   pass

注意2:发生注意1的情况时,虽然缓存信息无法查看或者查看不准确,但是缓存本身是起作用的。

functools.cache

python3.9 新增,返回值与 lru_cache(maxsize=None) 相同,创建一个查找函数参数的字典的简单包装器. 因为它不需要移出旧值,所以比带有大小限制的 lru_cache() 更小更快.但是需要注意内存的使用问题。

#python3.9 新增
@functools.cache(user_function)

测试cache缓存效果

为了测试缓存效果,提前定义一个计时器:

def timer(fn):
    def core(*args, **kwargs):
        start_time = time()
        # exe
        returned = fn(*args, **kwargs)
        print(f"function:\"{fn.__name__}\",执行耗时:{time() - start_time}")
        return returned

    return core

不建议拿阶乘测试,最大递归python一般只有1000:

在Python中,可以使用sys模块中的getrecursionlimit函数来查看最大递归层数。同时,您也可以使用sys模块中的setrecursionlimit函数来设置最大递归层数。默认情况下,Python的最大递归层数是限制的,以避免无限递归导致栈溢出。

以下是如何查看和设置最大递归层数的示例:

查看最大递归层数:

import sys

max_recursion_depth = sys.getrecursionlimit()
print(f"最大递归层数为: {max_recursion_depth}")

设置最大递归层数(谨慎使用,不建议随意更改):

import sys

new_max_recursion_depth = 5000  # 新的最大递归层数
sys.setrecursionlimit(new_max_recursion_depth)

请注意,更改最大递归层数可能会导致不稳定的行为和程序崩溃,因此只有在非常了解程序和递归深度要求的情况下才应该尝试更改它。默认的递归层数通常对于绝大多数情况都是足够的。

因此,定义一个加法函数来测试:

@timer
@cache
def function_with_cache(n):
    c = 0
    for i in range(n):
        c += i


@timer
def function_without_cache(n):
    c = 0
    for i in range(n):
        c += i


if __name__ == '__main__':
    n = 100000
    function_with_cache(n)
    function_without_cache(n)

    function_with_cache(n)
    function_without_cache(n)

    print("--------------------------------------------")
    
    n = 12000000
    function_with_cache(n)
    function_without_cache(n)

    function_with_cache(n)
    function_without_cache(n)

function:"function_with_cache",执行耗时:0.002999544143676758
function:"function_without_cache",执行耗时:0.0029997825622558594
function:"function_with_cache",执行耗时:0.0
function:"function_without_cache",执行耗时:0.0030007362365722656
--------------------------------------------
function:"function_with_cache",执行耗时:0.34157276153564453
function:"function_without_cache",执行耗时:0.3233063220977783
function:"function_with_cache",执行耗时:0.0
function:"function_without_cache",执行耗时:0.34238338470458984

可以看到,计算结果被缓存了一份,只有在入参改变的时候才会重新计算结果。

使用到类方法也是可以的:

class Test:

    @timer
    @cache
    def get_info(self, size: int = 1, offset: int = 10):
        # 模拟io
        sleep(random.randint(3, 5))
        return "data"


if __name__ == '__main__':
    t = Test()
    print(t.get_info())

    print(t.get_info())

    print(t.get_info(size=10))

    print(t.get_info(size=10))
    
function:"get_info",执行耗时:3.001025915145874
data
function:"get_info",执行耗时:0.0
data
function:"get_info",执行耗时:5.001091480255127
data
function:"get_info",执行耗时:0.0
data

cachelru_cache 虽然是很有用的缓存装饰器,但它们确实有一些局限性,主要是无法设置超时时间。以下是适用于这两个装饰器的一些情况和范围:

lru_cache 的适用范围:

  1. 计算密集型函数缓存lru_cache 适用于需要频繁调用的计算密集型函数,以避免多次计算相同的输入值所产生的性能损失。它在这种情况下非常有效。

  2. 递归函数的性能优化:对于递归函数,可以使用 lru_cache 来缓存已经计算过的参数,以减少递归调用的计算次数,从而提高性能。

  3. 结果不会变化的函数:对于给定的输入参数,函数的结果永远不会发生变化的情况下,lru_cache 是一个不错的选择。因为函数结果是永久缓存的,这种情况下不需要超时。

cache 的适用范围:

cache 是 Python 3.9+ 中新增的装饰器,与 lru_cache 类似,但没有大小限制。它适用于以下情况:

  1. 计算密集型函数缓存:与 lru_cache 一样,cache 适用于需要频繁调用的计算密集型函数,以避免多次计算相同的输入值所产生的性能损失。

  2. 结果不会变化的函数:对于给定的输入参数,函数的结果永远不会发生变化的情况下,cache 也是一个不错的选择。因为函数结果是永久缓存的,这种情况下不需要超时。

因此,对于数据库查询操作,不能使用这样的缓存,因为哪怕是相同参数,如果数据库变了,该缓存会导致返回同样的数据。

functools.cached_property(func)

cached_property对标property,它将类的方法转化为一个属性,其值计算一次,然后作为实例的普通属性缓存一生。与property()类似,但添加了缓存功能。对于实例的昂贵计算属性非常有用,这些属性在其他情况下基本上是不可变的。

参考翻译

  1. Data Classes in Python 3.7+ (Guide) _https://realpython.com/python-data-classes/

你可能感兴趣的:(python,python,java,前端)