Python的dataclass
是Python 3.7版本引入的一个装饰器,用于简化创建和管理包含数据的类(即数据类)。它的目的是减少编写样板代码,并提供了一种简单的方式来定义类,其中主要包含用于存储数据的字段。dataclass
为类自动生成一些常见的特殊方法,如__init__()
、__repr__()
、__eq__()
等,从而减少了冗余的代码。
以下是dataclass
的一些重要特性和用法:
装饰器:使用@dataclass
装饰器可以将一个普通的类转换为数据类。
字段声明:通过在类的属性上使用类型注解,可以声明类的字段。这些字段将存储实例的数据。
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
自动生成特殊方法:dataclass
会自动生成__init__()
、__repr__()
、__eq__()
等特殊方法,无需手动编写。
默认值和默认工厂函数:你可以为字段提供默认值或者默认工厂函数,以便在创建实例时使用。
from dataclasses import dataclass
@dataclass
class Point:
x: int = 0
y: int = 0
可变与不可变数据类:dataclass
默认生成可变数据类,但你也可以使用frozen=True
参数创建不可变数据类,即一旦创建实例,就不能再修改其属性值。
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: int
y: int
继承:你可以在数据类中使用继承,但需要小心,因为基类的特殊方法可能不会像你期望的那样工作。
自定义特殊方法:如果需要自定义某个特殊方法,你可以手动实现它们,它们将覆盖自动生成的方法。
字段的默认排序:dataclass
支持通过order=True
参数为字段启用默认的比较和排序功能。
from dataclasses import dataclass
@dataclass(order=True)
class Person:
name: str
age: int
数据类的替代方案: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(默认值),将根据eq
和frozen
的设置生成一个__hash__()
方法。__hash__()
用于内置的hash()
函数,以及当对象添加到哈希集合(如字典和集合)时。拥有__hash__()
意味着该类的实例是不可变的。不可变性是一个依赖于程序员意图、__eq__()
的存在和行为,以及dataclass()
装饰器中的eq
和frozen
标志值的复杂属性。默认情况下,除非安全,否则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版本中可能会有所不同。
数据类(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中的一个新特性,叫做变量注释,比如str
和float
。
这几行代码就是您需要的全部内容。新类已经可以使用了:
>>> 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 实例是在初始化对象后无法修改其属性的对象。
在 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 实例是一种很好方式来存储:
这些通常不会在应用程序的生命周期内发生变化,任何企图修改它们的行为都应该被禁止。
有了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
在Python中,functools
模块提供了一些有用的装饰器和函数,用于实现缓存和性能优化。以下是functools
模块中的一些常用缓存相关函数和装饰器的详细解释:
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)
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)
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(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缓存装饰器提供的功能有:
查看函数当前的缓存信息可以使用如下方法,比如查看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的情况时,虽然缓存信息无法查看或者查看不准确,但是缓存本身是起作用的。
python3.9 新增,返回值与 lru_cache(maxsize=None) 相同,创建一个查找函数参数的字典的简单包装器. 因为它不需要移出旧值,所以比带有大小限制的 lru_cache() 更小更快.但是需要注意内存的使用问题。
#python3.9 新增
@functools.cache(user_function)
为了测试缓存效果,提前定义一个计时器:
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
cache
和 lru_cache
虽然是很有用的缓存装饰器,但它们确实有一些局限性,主要是无法设置超时时间。以下是适用于这两个装饰器的一些情况和范围:
lru_cache
的适用范围:
计算密集型函数缓存:lru_cache
适用于需要频繁调用的计算密集型函数,以避免多次计算相同的输入值所产生的性能损失。它在这种情况下非常有效。
递归函数的性能优化:对于递归函数,可以使用 lru_cache
来缓存已经计算过的参数,以减少递归调用的计算次数,从而提高性能。
结果不会变化的函数:对于给定的输入参数,函数的结果永远不会发生变化的情况下,lru_cache
是一个不错的选择。因为函数结果是永久缓存的,这种情况下不需要超时。
cache
的适用范围:
cache
是 Python 3.9+ 中新增的装饰器,与 lru_cache
类似,但没有大小限制。它适用于以下情况:
计算密集型函数缓存:与 lru_cache
一样,cache
适用于需要频繁调用的计算密集型函数,以避免多次计算相同的输入值所产生的性能损失。
结果不会变化的函数:对于给定的输入参数,函数的结果永远不会发生变化的情况下,cache
也是一个不错的选择。因为函数结果是永久缓存的,这种情况下不需要超时。
因此,对于数据库查询操作,不能使用这样的缓存,因为哪怕是相同参数,如果数据库变了,该缓存会导致返回同样的数据。
cached_property对标property,它将类的方法转化为一个属性,其值计算一次,然后作为实例的普通属性缓存一生。与property()
类似,但添加了缓存功能。对于实例的昂贵计算属性非常有用,这些属性在其他情况下基本上是不可变的。