【python】一文带你了解什么是dataclass?

为什么需要dataclass数据类

Python 3.7(PEP 557)后引入一个新功能是装饰器@dataclass,它通过自动生成特殊方法(如__init__() 和 __repr__() ...等魔术方法 )来简化数据类的创建。

数据类和普通类一样,但设计用于存储数据、结构简单、用于将相关的数据组织在一起、具有清晰字段的类。

这种类,也称为数据结构,非常常见。例如,用于存储点坐标的类只是一个具有 3 个字段(x、y、z)的类。

而如果不使用类来表示,python中也有其它可替换的数据结构。


假设我们现在遇到一个场景, 需要一个数据对象来保存一些运动员信息,信息包括球员姓名,号码,位置,年龄。

使用tuple

harden = ('James Harden', 1, 'PG', 34)
print(harden[2])  # PG

劣势: 不灵活,创建和取值基于位置,需要记住坐标对应的信息。

使用namedtuple

from collections import namedtuplePlayer = namedtuple('Player', ['name', 'number', 'position', 'age', 'grade'])jordan = Player('James Harden', 1, 'PG', 1, 'S+')print(jordan)  # Player(name='James Harden', number=1, position='PG', age=1, grade='S+')print(jordan.name)  # James Harden

使用namedtuple可以使用.获取数据的属性, 可以明确数据的属性名称,但是仍然存在一些问题,比如:

  • 数据无法修改。
  • 无法自定义数据比较,没有默认值,没有函数支持。

使用typing.NamedTuple

from typing import NamedTupleclass Player(NamedTuple):name: strnumber: intposition: strage: intgrade: strjordan = Player('James Harden', 1, 'PG', 1, 'S+')print(jordan)        # Player(name='James Harden', number=1, position='PG', age=1, grade='S+')
print(jordan.name)   # James Harden

通过类型提示让代码更具可读性和可维护性。但同样有namedtuple的一些问题,如不可变性等。

使用dict

使用dict来存放一些参数,配置信息,相比tuple来说可以支持更复杂的嵌套结构。

jordan = {'name': 'James Harden', 'number': 1, 'position': 'PG', 'age': 34}
print(jordan['position'])  # PG

劣势: 无法对数据属性名进行控制。

使用typing.TypedDict

可以更多的利用类型检查来帮助减少错误发生的可能,同时也能帮助其他开发者理解复杂数据结构。

from typing import TypedDictclass Player(TypedDict):name: strnumber: intposition: strage: intjordan: Player = {'name': 'James Harden', 'number': 1, 'position': 'PG', 'age': 34}print(jordan['position'])  # Output: PG

总的来说,对于一些简单的场景,tuplenamedtupledict还是有一席用武之地的,但是在一些更复杂的场景中,这三者就显得没那么好用了,比如:数据比较,设置默认值等。

因此,我们一般会通过自定义类来实现复杂场景的数据类。

class Player:def __init__(self, name, number, position, age, grade):self.name = nameself.number = numberself.position = positionself.age = ageself.grade = gradeharden = Player('James Harden', 1, 'PG', 34, 'S+')
bryant = Player(name='Kobe Bryant', number=24, position='PG', age=41, grade='S+')print(harden.name)  # James Harden
print(bryant.name)  # Kobe Bryantprint(harden)  # <__main__.Player object at 0x000002431AFC6E00>print(harden < bryant)

结果:

James Harden
Kobe Bryant
<__main__.Player object at 0x000002431AFC6E00>
Traceback (most recent call last):File "F:\study\django-restframesork-jwt-demo\test\1.py", line 33, in <module>print(harden < bryant)
TypeError: '<' not supported between instances of 'Player' and 'Player'

然而,这样定义的类还是有以下问题:

  1. 不支持比较
  2. 对于对象的描述不太友好

为了解决上面两个问题,可以通过实现__repr__方法来自定义描述, 实现__gt__方法来支持比较的功能。

假设比较的属性为age, 更新代码如下:

class Player:def __init__(self, name, number, position, age, grade):self.name = nameself.number = numberself.position = positionself.age = ageself.grade = gradedef __repr__(self):return f'Player: {self.name} : {self.age}'def __gt__(self, other):return self.age > other.agedef __eq__(self, other):return self.age == other.ageharden = Player('James Harden', 1, 'PG', 34, 'S+')
bryant = Player(name='Kobe Bryant', number=24, position='PG', age=41, grade='S+')print(harden.name)  # James Harden
print(bryant.name)  # Kobe Bryantprint(harden)  # Player: James Harden : 34print(harden < bryant)  # True

这样,这个数据对象有了更直观的描述, 支持了对比。

我们经常需要添加构造函数表示方法比较函数等。这些函数很麻烦,而这正是语言应该透明地处理的。

from dataclasses import dataclass@dataclass(order=True)
class Player:name: strnumber: intposition: strgrade: strage: int = 18  # 默认值,跟函数定义一样,需要往后放harden = Player('James Harden', 1, 'PG', 'S+', 34)
bryant = Player(name='Kobe Bryant', number=24, position='PG', grade='S+', age=41)print(harden.name)  # James Harden
print(bryant.name)  # Kobe Bryantprint(harden)  # Player(name='James Harden', number=1, position='PG', grade='S+', age=34)# 比较, 默认按照属性定义的顺序比较的
print(harden < bryant)  # True

dataclass相较于dicttuple具有明显优势。它能更精确地指定每个成员变量的类型,同时提供字段名的检查,大大降低了出错的可能性。相对于传统的类定义,使用dataclass更加简洁,省去了冗长的__init__方法等,只需直接列出成员变量即可。

数据类更易于阅读和理解,类型提示使得读者更自然地理解数据的组织结构。当数据类清晰明了时,读者更容易形成准确的假设,也更容易发现并修复潜在的错误。

使用dataclass改造了之后,看起来结果也是符合预期的,但是我们需要了解下其中的原理,不然也是会不经意间遗留下bug😂。

你是否好奇dataclass加上的这些魔术方法是什么样的?比如说比较的逻辑是什么?

接下来我们看一下源码及官方的介绍,那样你就知道上面的代码是否有问题啦!

dataclass如何装饰类

def dataclass(cls=None, /, *, init=True, repr=True, eq=True, order=False,unsafe_hash=False, frozen=False, match_args=True,kw_only=False, slots=False):"""Returns the same class as was passed in, with dunder methodsadded based on the fields defined in the class.Examines PEP 526 __annotations__ to determine fields.If init is true, an __init__() method is added to the class. Ifrepr is true, a __repr__() method is added. If order is true, richcomparison dunder methods are added. If unsafe_hash is true, a__hash__() method function is added. If frozen is true, fields maynot be assigned to after instance creation. If match_args is true,the __match_args__ tuple is added. If kw_only is true, then bydefault all fields are keyword-only. If slots is true, an__slots__ attribute is added."""def wrap(cls):return _process_class(cls, init, repr, eq, order, unsafe_hash,frozen, match_args, kw_only, slots)# See if we're being called as @dataclass or @dataclass().if cls is None:# We're called with parens.return wrap# We're called as @dataclass without parens.return wrap(cls)

dataclass提供了一些字段,使用这些字段,装饰器将生成的方法定义添加到类中,以支持实例初始化repr比较方法以及规范部分中所述的其他方法(可选)。

  • init :如果为 True,则生成__init__方法。
  • repr :如果为 True,则生成__repr__方法。
  • eq :如果为 True,则通过比较字段作为元组来生成__eq__方法。
  • order :如果为 True,则生成__lt____le____gt____ge__方法。
  • unsafe_hash :如果为True,则将生成函数__hash__
  • frozen :如果为True,则实例将是不可变的(只读)。

这样的类称为Data类,但该类实际上并没有什么特别之处,装饰器将生成的方法添加到类中,并返回给定的相同类。

举个例子:

@dataclass
class InventoryItem:'''Class for keeping track of an item in inventory.'''name: strunit_price: floatquantity_on_hand: int = 0def total_cost(self) -> float:return self.unit_price * self.quantity_on_hand

@dataclass装饰器可以将这些方法的等效项添加到InventoryItem类中,可以通过参数控制:

def __init__(self, name: str, unit_price: float, quantity_on_hand: int = 0) -> None:self.name = nameself.unit_price = unit_priceself.quantity_on_hand = quantity_on_hand
def __repr__(self):return f'InventoryItem(name={self.name!r}, unit_price={self.unit_price!r}, quantity_on_hand={self.quantity_on_hand!r})'
def __eq__(self, other):if other.__class__ is self.__class__:return (self.name, self.unit_price, self.quantity_on_hand) == (other.name, other.unit_price, other.quantity_on_hand)return NotImplemented
def __ne__(self, other):if other.__class__ is self.__class__:return (self.name, self.unit_price, self.quantity_on_hand) != (other.name, other.unit_price, other.quantity_on_hand)return NotImplemented
def __lt__(self, other):if other.__class__ is self.__class__:return (self.name, self.unit_price, self.quantity_on_hand) < (other.name, other.unit_price, other.quantity_on_hand)return NotImplemented
def __le__(self, other):if other.__class__ is self.__class__:return (self.name, self.unit_price, self.quantity_on_hand) <= (other.name, other.unit_price, other.quantity_on_hand)return NotImplemented
def __gt__(self, other):if other.__class__ is self.__class__:return (self.name, self.unit_price, self.quantity_on_hand) > (other.name, other.unit_price, other.quantity_on_hand)return NotImplemented
def __ge__(self, other):if other.__class__ is self.__class__:return (self.name, self.unit_price, self.quantity_on_hand) >= (other.name, other.unit_price, other.quantity_on_hand)return NotImplemented

看完上面的例子,我们也就对其原理有了一定了解,dataclass在一定程度上帮我们简化了数据类的定义,但是如果我们需要精准控制我们的程序,还是需要我们重写其中的相关魔术方法的。

我们再来看下运动员的例子,使用dataclass改造如下,以实现更精准的控制:

from dataclasses import dataclass@dataclass
class Player:name: strnumber: intposition: strgrade: strage: int = 18def __eq__(self, other):return self.age == other.age  # 只比较agedef __lt__(self, other):return self.age < other.age  # 只比较 age# 示例使用
harden = Player('James Harden', 1, 'PG', 'S+', 34)
bryant = Player(name='Kobe Bryant', number=24, position='PG', grade='S+', age=41)result = harden < bryant  # 按照 age 进行比较
print(result)  # 输出 True,因为 34 < 41
print(harden.name)  # James Harden
print(bryant.name)  # Kobe Bryant
print(bryant == harden)  # False

当然,如果都要自己重载实现,那dataclass看起来也是不太聪明的样子。不想全部的字段都参与,dataclass也是提供了其它机制用于简化。

dataclass 的使用

通过上面的示例,我们了解到,dataclass帮我们模板化的实现了一批魔术方法,而我们要做的仅仅是根据需求调整dataclass的参数或者在适当的时候进行部分重载以满足我们的实际场景。

类型提示和默认值

与函数参数规则一样,具有默认值的属性必须出现在没有默认值的属性之后。

from dataclasses import dataclass
from typing import Any@dataclass
class Player:name: strnumber: intposition: strgrade: strage: int = 18team: Any = "nba"# 示例使用
harden = Player('James Harden', 1, 'PG', 'S+', 34)
bryant = Player(name='Kobe Bryant', number=24, position='PG', grade='S+')print(harden.name)  # James Harden
print(bryant.name)  # Kobe Bryant
print(bryant.age)  # 18
print(bryant.team)  # nba

数据嵌套

数据类可以嵌套为其他数据类的字段,可以简单创建一个有2个队员的球队。快船队包含:哈登和小卡。

from dataclasses import dataclass
from typing import List@dataclass
class Player:name: strnumber: intposition: strgrade: strage: int = 18@dataclass
class Team:name: strplayers: List[Player]# 示例使用
harden = Player('James Harden', 1, 'PG', 'S+', 34)
leonard = Player(name='Kawhi Leonard', number=2, position='SF', grade='S+')clippers = Team("clippers", [harden, leonard])
print(harden.name)  # James Harden
print(leonard.name)  # Kawhi Leonard
print(leonard.age)  # 18print(clippers)  # Team(name='clippers', players=[Player(name='James Harden', number=1, position='PG', grade='S+', age=34), Player(name='Kawhi Leonard', number=2, position='SF', grade='S+', age=18)])

继承

from dataclasses import dataclass, field@dataclass(order=True)
class Person:name: strage: int@dataclass(order=True)
class Player(Person):number: intposition: strgrade: strteam: str = "nba"# 示例使用
harden = Player(name='James Harden', age=34, number=1, position='PG', grade='S+')
bryant = Player(name='Kobe Bryant', age=41, number=24, position='PG', grade='S+')print(harden.name)  # James Harden
print(bryant.name)  # Kobe Bryant
print(bryant.age)  # 41
print(bryant.team)  # nba# 使用 order 参数,可以比较对象的大小(用于排序)
print(harden < bryant)  # True

类中定义的字段的顺序(先父类,再当前类)。

不定长参数

数据类一般建议是显示声明属性。如果你想额外接收一些参数,可能以下方法可以满足你。

from dataclasses import dataclass, field@dataclass
class Player:name: strnumber: intposition: strgrade: strage: int = 18args: tuple = ()kwargs: dict = field(default_factory=dict)# 示例使用
harden = Player('James Harden', 1, 'PG', 'S+', 34)
bryant = Player(name='Kobe Bryant', number=24, position='PG', grade='S+', args=(1, 2), kwargs={"hello": "world"})print(bryant)

输出:

Player(name='Kobe Bryant', number=24, position='PG', grade='S+', age=18, args=(1, 2), kwargs={'hello': 'world'})

field对象

如果数据类的属性是不可变类型,可以直接为其赋默认值,然而当属性是不可变类型时,直接给定默认值时会报错。

from dataclasses import dataclass
from typing import List@dataclass
class Player:name: strnumber: intposition: strgrade: strage: int = 18# 示例使用
harden = Player('James Harden', 1, 'PG', 'S+', 34)
leonard = Player(name='Kawhi Leonard', number=2, position='SF', grade='S+')@dataclass
class Team:name: strplayers: List[Player] = [leonard]  # 这里会报错clippers = Team("clippers", [harden, leonard])
print(harden.name)  
print(leonard.name)  
print(leonard.age) print(clippers)  

输出:

ValueError: mutable default <class 'list'> for field players is not allowed: use default_factory

dataclass默认阻止使用可变数据做默认值

正如报错提示的一样,这时候field对象就登场了。

from dataclasses import dataclass, field, fields
from typing import List@dataclass
class Player:name: strnumber: intposition: strgrade: strage: int = 18# 示例使用
harden = Player('James Harden', 1, 'PG', 'S+', 34)
leonard = Player(name='Kawhi Leonard', number=2, position='SF', grade='S+')@dataclass
class Team:name: str = field(metadata={'unit': 'name'})players: List[Player] = field(default_factory=lambda: [leonard], metadata={'unit': 'players'})clippers = Team("clippers", [harden])
clippers1 = Team("clippers")
print(harden.name)
print(leonard.name)
print(leonard.age)print(clippers.players)
print(clippers1.players)print(fields(clippers))
print(fields(clippers)[1].metadata)

输出:

James Harden
Kawhi Leonard
18
[Player(name='James Harden', number=1, position='PG', grade='S+', age=34)]
[Player(name='Kawhi Leonard', number=2, position='SF', grade='S+', age=18)]
(Field(name='name',type=<class 'str'>,default=<dataclasses._MISSING_TYPE object at 0x0000029523A65060>,default_factory=<dataclasses._MISSING_TYPE object at 0x0000029523A65060>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({'unit': 'name'}),kw_only=False,_field_type=_FIELD), Field(name='players',type=typing.List[__main__.Player],default=<dataclasses._MISSING_TYPE object at 0x0000029523A65060>,default_factory=<function Team.<lambda> at 0x0000029523B44B80>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({'unit': 'players'}),kw_only=False,_field_type=_FIELD))
{'unit': 'players'}

我们来看一下field对象的签名:

def field(*, default=MISSING, default_factory=MISSING, init=True, repr=True,hash=None, compare=True, metadata=None, kw_only=MISSING):"""Return an object to identify dataclass fields.default is the default value of the field.  default_factory is a0-argument function called to initialize a field's value.  If initis true, the field will be a parameter to the class's __init__()function.  If repr is true, the field will be included in theobject's repr().  If hash is true, the field will be included in theobject's hash().  If compare is true, the field will be used incomparison functions.  metadata, if specified, must be a mappingwhich is stored but not otherwise examined by dataclass.  If kw_onlyis true, the field will become a keyword-only parameter to__init__().It is an error to specify both default and default_factory."""if default is not MISSING and default_factory is not MISSING:raise ValueError('cannot specify both default and default_factory')return Field(default, default_factory, init, repr, hash, compare,metadata, kw_only)

参数描述默认值
default指定字段的默认值。
default_factory与 default 相似,但是是一个可调用对象,用于提供默认值。每次创建实例时,都会重新调用工厂函数以获取新的默认值。
init控制是否在__init__方法中包含该字段True
repr是否在__repr__()方法中使用字段True
compare是否在比较对象时, 包括该字段True
hash计算hash时, 是否包括字段True
metadata包含字段信息的映射

如不想name加入比较,则可以设置:name: str = field(compare = False)

元数据(metadata)

可以基于元数据进行数据校验:

from dataclasses import dataclass, field, fields
from datetime import datetimeclass ValidationError(Exception):def __init__(self, field_name, condition, actual_value):self.field_name = field_nameself.condition = conditionself.actual_value = actual_valuesuper().__init__(f"{field_name} validation failed: {condition} (Actual value: {actual_value})")class Color:RED = '\033[91m'END = '\033[0m'@dataclass
class Player:name: str = field(default="", metadata={"validation": [lambda x: len(x) == 0]})number: int = field(default=0, metadata={"validation": [lambda x: not 0 < x <= 100]})position: str = field(default="", metadata={"validation": [lambda x: len(x) == 0]})grade: str = field(default="", metadata={"validation": [lambda x: x in {'S+', 'S', 'A', 'B', 'C'}]})age: int = field(default=0, metadata={"validation": [lambda x: not 0 < x <= 150]})foundation_date: datetime = field(default_factory=datetime.now)def validation(self):for field_ in fields(self):validations = field_.metadata.get("validation", [])for validation in validations:if validation(getattr(self, field_.name)):raise ValidationError(field_.name, str(validation), getattr(self, field_.name))harden = Player(name='James Harden', number=13, position='PG', grade='S+', age=32)
bryant = Player(name='Kobe Bryant', number=24, position='SG', grade='S', age=41)# 无效的数据,引发异常
try:harden.validation()
except ValidationError as e:print(f"{Color.RED}{e}{Color.END}")try:bryant.validation()
except ValidationError as e:print(f"{Color.RED}{e}{Color.END}")

输出:

grade validation failed: <function Player.<lambda> at 0x00000197FD6B4CA0> (Actual value: S+)
grade validation failed: <function Player.<lambda> at 0x00000197FD6B4CA0> (Actual value: S)

自定义属性

通过对field()对象的剖析,我们可以指定属性:是否参与比较,是否参与hash计算等等。

不过我们知道默认的比较顺序,我们也可以通过增加属性以实现按需比较的功能。而这个用于比较的属性位于数据类的第一个属性,并可以借助__post_init__魔法函数实现灵活赋值。

from dataclasses import dataclass, field@dataclass(order=True)
class Player:sort_index: tuple = field(init=False)  # 添加一个 sort_index 字段,并设置为不在 __init__ 方法中初始化name: strnumber: intposition: strgrade: strage: int = 18def __post_init__(self):self.sort_index = (self.age, self.grade)  # 在 __post_init__ 方法中计算 sort_index# 示例使用
harden = Player('James Harden', 1, 'PG', 'S+', 34)
bryant = Player(name='Kobe Bryant', number=24, position='PG', grade='S+', age=41)result = harden < bryant  # 按照 age 进行比较
print(result)  # 输出 True,因为 34 < 41
print(harden.name)  # James Harden
print(bryant.name)  # Kobe Bryant
print(bryant == harden)  # False

不可变数据类

def dataclass(cls=None, /, *, init=True, repr=True, eq=True, order=False,unsafe_hash=False, frozen=False, match_args=True,kw_only=False, slots=False):

使用dataclass实现的数据类默认是可变的,要使数据类不可变,需要在创建类时设置frozen=True

from dataclasses import dataclass, field@dataclass(order=True, frozen=True)
class Player:name: strnumber: intposition: strgrade: strage: int = 18# 示例使用
harden = Player('James Harden', 1, 'PG', 'S+', 34)harden.age = 33  # dataclasses.FrozenInstanceError: cannot assign to field 'age'

实现数据类去重

unsafe_hash=True时,可以实现数据类的去重。参与的字段同样可由field对象控制。

from dataclasses import dataclass, field@dataclass(order=True, unsafe_hash=True)
class Player:name: strnumber: intposition: str = field(hash=False)  # 不参与hashgrade: strage: int = 18# 示例使用
harden = Player('James Harden', 1, 'PG', 'S+', 34)
harden2 = Player('James Harden', 1, 'PG', 'S+', 34)harden3 = Player('James Harden', 1, 'SG', 'S+', 34)print({harden, harden2})
print({harden, harden3})

输出:

{Player(name='James Harden', number=1, position='PG', grade='S+', age=34)}
{Player(name='James Harden', number=1, position='PG', grade='S+', age=34), Player(name='James Harden', number=1, position='SG', grade='S+', age=34)}

数据类转换为元组或字典

from dataclasses import dataclass, field, asdict, astuple@dataclass
class Player:name: strnumber: intposition: strgrade: strage: int = 18args: tuple = ()kwargs: dict = field(default_factory=dict)# 示例使用
harden = Player('James Harden', 1, 'PG', 'S+', 34)
bryant = Player(name='Kobe Bryant', number=24, position='PG', grade='S+', args=(1, 2), kwargs={"hello": "world"})alist = [harden, bryant]
print(sorted(alist, key=lambda x: x.age))print(asdict(bryant))
print(astuple(harden))

输出:

[Player(name='Kobe Bryant', number=24, position='PG', grade='S+', age=18, args=(1, 2), kwargs={'hello': 'world'}), Player(name='James Harden', number=1, position='PG', grade='S+', age=34, args=(), kwargs={})]
{'name': 'Kobe Bryant', 'number': 24, 'position': 'PG', 'grade': 'S+', 'age': 18, 'args': (1, 2), 'kwargs': {'hello': 'world'}}
('James Harden', 1, 'PG', 'S+', 34, (), {})

replace方法

这个方法允许你创建一个新的实例,其中某些字段的值被更改,而其他字段的值保持不变。

from dataclasses import dataclass, field, fields, replace
from typing import List@dataclass
class Player:name: strnumber: intposition: strgrade: strage: int = 18# 示例使用
harden = Player('James Harden', 1, 'PG', 'S+', 34)
leonard = Player(name='Kawhi Leonard', number=2, position='SF', grade='S+')@dataclass
class Team:name: str = field(metadata={'unit': 'name'})players: List[Player] = field(default_factory=lambda: [leonard], metadata={'unit': 'players'})clippers = Team("clippers", [leonard])# 使用 replace() 替换 Team 实例中的字段值
new_clippers = replace(clippers, name="new_clippers", players=[leonard, harden])print("Original Clippers:", clippers)
print("New Clippers:", new_clippers)

输出:

Original Clippers: Team(name='clippers', players=[Player(name='Kawhi Leonard', number=2, position='SF', grade='S+', age=18)])
New Clippers: Team(name='new_clippers', players=[Player(name='Kawhi Leonard', number=2, position='SF', grade='S+', age=18), Player(name='James Harden', number=1, position='PG', grade='S+', age=34)])

Python中dataclass的应用示例

数据提取[参数校验]:

dataclass数据类可以配合一些校验工具包和数据提取工具包以实现数据提取或参数校验的工作,以下是配合marshmallowdesert实现数据校验提取工作的示例:

import requests
from dataclasses import dataclass
import dataclasses
from marshmallow import fields, EXCLUDE, validate
import desert@dataclass
class Activity:activity: strparticipants: int = dataclasses.field(metadata=desert.metadata(fields.Int(required=True,validate=validate.Range(min=1, max=50,error="Participants must be between 1 and 50 people"))))price: float = dataclasses.field(metadata=desert.metadata(fields.Float(required=True,validate=validate.Range(min=0, max=50,error="Price must be between $1 and $50"))))def __post_init__(self):self.price = self.price * 100def get_activity():# resp = requests.get("https://www.boredapi.com/api/activity").json()resp = {"activity": "Improve your touch typing","type": "busywork","participants": 1,"price": 1.0,# "price": 51,"link": "https://en.wikipedia.org/wiki/Touch_typing","key": "2526437","accessibility": 0.8}# 只提取关心的部分,未知内容选择忽略schema = desert.schema(Activity, meta={"unknown": EXCLUDE})return schema.load(resp)print(get_activity())

输出:

Activity(activity='Improve your touch typing', participants=1, price=100.0)

如果你修改一下resp的值,比如使price大于50,这时候你会得到校验失败的提示:

marshmallow.exceptions.ValidationError: {'price': ['Price must be between $1 and $50']}

存储数据的简单对象

dataclasses 在许多情境下都表现出色,尤其是在定义用于存储数据的简单对象时。它特别适用于处理配置信息、数据传输对象(DTO)、领域对象以及其他仅包含数据的结构。

需求:程序退出前自动持久化配置对象到配置文件。

import json
import atexit
import logging
import threading
from pathlib import Path
from dataclasses import dataclass, asdict@dataclass
class Config(object):name: str = "mysql"port: int = 3306_instance = None_lock = threading.Lock()_registered = False  # 新增类属性def __new__(cls, *args, **kw):with cls._lock:if cls._instance is None:cls._instance = super().__new__(cls)return cls._instancedef load_from_file(self, file_path):"""从配置文件加载配置,如果文件不存在或加载失败,保持默认值。"""if file_path.exists():try:with file_path.open() as f:json_data = json.load(f)for key, value in json_data.items():setattr(self, key, value)except Exception as err:logging.error(f"Failed to load config from file: {err}")else:logging.warning(f"Config file '{file_path}' not exists. Using default values.")def save_to_file(self, file_path):"""保存配置到文件"""json_str = json.dumps(asdict(self), indent=4)with file_path.open('w') as f:logging.warning(f"Saving configs to '{file_path}'")f.write(json_str)@classmethoddef register_atexit(cls):"""注册在程序退出时保存配置到配置文件"""with cls._lock:if not cls._registered:atexit.register(cls._instance.save_to_file, Path("./config.json"))cls._registered = True# 读取配置文件和保存配置的逻辑分离def __post_init__(self):config_file = Path("./config.json")# 从配置文件加载配置self.load_from_file(config_file)# 注册在程序退出时保存配置到配置文件self.register_atexit()if __name__ == "__main__":# 创建一个 Config 实例config_instance = Config(name="redis", port=6379)# 打印当前配置print("Current Config:", config_instance)# 修改配置并再次打印config_instance.port = 8080print("Updated Config:", config_instance)# 创建另一个 Config 实例,演示单例模式another_instance = Config()print("Another Instance Config:", another_instance)# 保存配置到文件another_instance.save_to_file(Path("./another_config.json"))# 从文件加载配置another_instance.load_from_file(Path("./another_config.json"))print("Loaded Config from File:", another_instance)

输出:

Current Config: Config(name='mysql', port=3306)
Updated Config: Config(name='mysql', port=8080)
Another Instance Config: Config(name='mysql', port=3306)
Loaded Config from File: Config(name='mysql', port=3306)
WARNING:root:Saving configs to 'another_config.json'
WARNING:root:Saving configs to 'config.json'

让函数返回值更明确清晰

from dataclasses import dataclass
from enum import Enum
from typing import Tuple, Dict, Unionclass Grade(Enum):S_PLUS = 'S+'# 定义其他等级...@dataclass
class Player:name: strnumber: intposition: strgrade: Gradeage: int = 18def create_player(name: str, number: int, position: str, grade: Grade, age: int) -> Player:return Player(name, number, position, grade, age)# 示例使用
harden = create_player('詹姆斯·哈登', 1, '控球后卫', Grade.S_PLUS, 34)
bryant = create_player('科比·布莱恩特', 24, '得分后卫', Grade.S_PLUS, 41)print(harden)
print(bryant)

输出:

Player(name='詹姆斯·哈登', number=1, position='控球后卫', grade=<Grade.S_PLUS: 'S+'>, age=34)
Player(name='科比·布莱恩特', number=24, position='得分后卫', grade=<Grade.S_PLUS: 'S+'>, age=41)

dataclasses 的替代方案

dataclasses提供了许多方便的功能,但是PEP 557中还提到一个同样强大的数据类库attrs,并且这个库支持验证器等功能。

import attr@attr.s
class Point:x = attr.ib(type=int)y = attr.ib(type=int)p = Point(1, 2)
print(p)  # Output: Point(x=1, y=2)

在选择使用dataclasses还是attrs时,取决于项目的需求和个人喜好。dataclasses更简单直观,而attrs提供了更多的扩展性。如果只需要一些基本的自动生成特殊方法的功能,dataclasses是个不错的选择。如果你需要更高级的功能和更多的定制选项,可以考虑使用attrs

总结

dataclass 是一个强大的工具,使得创建和管理类变得更加简单和高效。

在实际应用中,特别是在数据处理和对象建模方面,使用@dataclass装饰器能够极大地提升代码的清晰度,减少冗余的样板代码。

深入理解dataclass的各项特性将帮助我们更灵活地运用这一功能,从而提高代码的质量和开发效率。

更多使用技巧请查阅官方文档!


如果你觉得文章还不错,请大家点赞、关注、分享、在看下,因为这将是我持续输出更多优质文章的最强动力!

参考

https://peps.python.org/pep-0557/
https://realpython.com/python-data-classes/#more-flexible-data-classes
https://docs.python.org/zh-cn/3/library/dataclasses.html#module-contents
https://www.pythontutorial.net/python-oop/python-dataclass/
https://github.com/python-desert/desert
https://glyph.twistedmatrix.com/2016/08/attrs.html
https://github.com/pviafore/RobustPython

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/648880.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

muduo库的模拟实现——muduo库的介绍

文章目录 一、muduo库介绍二、背景知识1.epoll2.Reactor模式 三、功能模块划分1.工具部分2.Reactor部分3.TCPServer部分 一、muduo库介绍 muduo库是在Linux环境下使用C实现的一个多Reactor多线程的高性能网络服务器&#xff0c;作者陈硕&#xff0c;他还出了一本书《Linux多线…

基于差分进化算法(Differential Evolution Algorithm,DE)的移动边缘计算的任务卸载与资源调度研究(提供MATLAB代码)

一、优化模型介绍 移动边缘计算的任务卸载与资源调度是指在移动设备和边缘服务器之间&#xff0c;将部分计算任务从移动设备卸载到边缘服务器&#xff0c;并合理分配资源以提高系统性能和降低能耗。 在本文所研究的区块链网络中&#xff0c;优化的变量为&#xff1a;挖矿决策&…

热门应用滥用苹果 iPhone 推送通知,暗中窃取用户数据

移动研究人员 Tommy Mysk 近日揭露&#xff0c;部分热门应用利用 iPhone 推送通知功能秘密发送用户数据&#xff0c;这引发了用户隐私安全担忧。 许多 iOS 应用程序正在使用由推送通知触发的后台进程来收集设备的用户数据&#xff0c;从而有可能创建用于跟踪的指纹档案。 Mys…

Azure AI - 沉浸式阅读器,阅读障碍用户福音

目录 一、什么是沉浸式阅读器将内容划分开来提高可读性显示常用字词的图片突出显示语音的各个部分朗读内容实时翻译内容将单词拆分为音节 二、沉浸式阅读器如何工作&#xff1f;环境准备创建 Web 应用项目设置身份验证配置身份验证值安装标识客户端 NuGet 包更新控制器以获取令…

《ORANGE’S:一个操作系统的实现》读书笔记(三十八)尾声(三)

这篇文章是尾声的第三部分&#xff0c;也是《ORANGE’S&#xff1a;一个操作系统的实现》读书笔记的最后一篇文章&#xff0c;本篇文章记录如何将我们开发的OS安装到真实的计算机&#xff08;建议在虚拟机中进行&#xff09;。 将OS安装到真实的计算机 其实安装到真实的硬盘和…

JS slice() 方法总结

在JavaScript中&#xff0c;有一种数组方法叫做slice()&#xff0c;它基于给定的起始和结束位置&#xff0c;创建一个新的数组副本。该方法能够将数组的一部分切成另一个数组。 语法 array.slice(start, end) start: 可选参数&#xff0c;表示切片起始位置的索引。如果没有指…

《Linux C编程实战》笔记:信号的屏蔽

在《Linux C编程实战》笔记&#xff1a;信号的捕捉和处理-CSDN博客的sigaction的sa_mask成员&#xff0c;它的类型就是一个信号集&#xff0c;下面我们来介绍它 信号集 信号的总数目达64个&#xff0c;所以不能用一个整数表示它们的集合&#xff0c;int类型通常是4字节32位&a…

CKA考试练习题

一&#xff1a;为部署管道创建一个新的 ClusterRole 并将其绑定到范围为特定 namespace 的特定 ServiceAccount 要求&#xff1a;创建一个名字为 deployment-clusterrole 且仅允许创建以下&#xff08;Deployment&#xff0c;StatefulSet &#xff0c;DaemonSet&#xff09;资源…

Linux | makefile简单教程 | Makefile的工作原理

前言 在学习完了Linux的基本操作之后&#xff0c;我们知道在linux中编写代码&#xff0c;编译代码都是要手动gcc命令&#xff0c;来执行这串代码的。 但是我们难道在以后运行代码的时候&#xff0c;难道都要自己敲gcc命令嘛&#xff1f;这是不是有点太烦了&#xff1f; 在vs中…

力扣646. 最长数对链

动态规划 思路&#xff1a; 思路与 力扣354. 俄罗斯套娃信封问题 类似将序列进行排序&#xff0c;然后假设 dp[i] 为第 i 个元素的最长数对链个数&#xff1b;则其状态转移方程&#xff1a; 第 i 个元素之前的某一个元素&#xff08;假设是下标是 j&#xff09;&#xff0c;如…

SPEC CPU 2017 Qemu RISCV

SPEC CPU 2017 Qemu RISCV 以下是 SPEC CPU 2017 的官方描述, 据说在 1.1.9 版本之后支持 RISCV SPEC CPU 2017 may be updated from time to time. To update your copy, use runcpu --update. History: v1.1.9, Nov-2022: Add RISC-V Linux toolset; update sysinfo.v1.1.8, …

KVM部署Alibaba Cloud Linux操作系统

下载镜像文件 下载链接&#xff1a;https://mirrors.aliyun.com/alinux/image/?spma2c4g.11186623.0.0.79ed5af6pehv54 下载文件&#xff1a;aliyun_3_x64_20G_nocloud_alibase_20230727.qcow2 部署KVM虚拟化环境 yum -y install qemu libvirt rr-testsuite systemctl star…

[SUCTF 2019]CheckIn1

黑名单过滤后缀’ph&#xff0c;并且白名单image类型要有对应文件头 对<?过滤&#xff0c;改用GIF89a<script languagephp>eval($_POST[cmd]);</script>&#xff0c;成功把getshell.gif上传上去了 尝试用.htaccess将上传的gif当作php解析&#xff0c;但是失败…

常见的前端打包构建工具有哪些

Webpack&#xff1a; Webpack 是一个模块打包工具&#xff0c;它能够将各种资源&#xff08;JavaScript、CSS、图片等&#xff09;打包成一个或多个静态文件&#xff0c;以优化加载性能。 Parcel&#xff1a; Parcel 是一个零配置的前端打包工具&#xff0c;可以自动识别项目中…

Flutter Text文字下方出现黄色双下划线

在Flutter中&#xff0c;Text组件是属于Material风格的&#xff0c;这就要求我们的根组件最好也是Material风格的&#xff0c;否则UI展示可能会有一些问题。刚刚提到的启动页&#xff0c;根组件直接使用的层叠布局Stack&#xff0c;而Stack就不属于Material风格&#xff0c;当S…

美工前端和数据对接一起做的可视化大屏开发项目工期一周足矣

hello宝子们...我们是艾斯视觉擅长ui设计和前端开发10年经验&#xff01;希望我的分享能帮助到您&#xff01;如需帮助可以评论关注私信我们一起探讨&#xff01;致敬感谢感恩&#xff01; 可视化大屏已经成为企业和组织中不可或缺的一部分。它不仅可以帮助企业更好地展示业务数…

[docker] Docker的私有仓库部署——Harbor

一、Docker原生私有仓库—— Registry 1.1 Registry的简单了解 关于Docker的仓库分为私有库和公有仓库&#xff0c;共有仓库只要在官方注册用户&#xff0c;登录即可使用。但对于仓库的使用&#xff0c;企业还是会有自己的专属镜像&#xff0c;所以私有库的搭建也是很有必要的…

jQuery实现选择方法和保护信息方法

最近呢&#xff01;一直在学习jQuery语法&#xff0c;也没时间发布文章&#xff0c;现在学的差不多了&#xff0c;先跟大家分享下学习感受吧&#xff01;JavaScript学过后&#xff0c;再学习jQuery语法&#xff0c;应该是简单的&#xff0c;但我总是容易把它们搞混&#xff0c;…

开源模型部署及使用

开源模型部署及使用 1.Langchain-Chatchat1.环境2.运行3.效果 2.facefusion1.环境2.运行3.效果 3.Aquila1.环境2.运行 1.Langchain-Chatchat Langchain-Chatchat这里面可以调用许多模型&#xff0c;我本地下载了chatglm3模型文件&#xff0c;所以就用这个模型。 1.环境 根据…

牛客网---------[USACO 2016 Jan S]Angry Cows

题目描述 Bessie the cow has designed what she thinks will be the next big hit video game: "Angry Cows". The premise, which she believes is completely original, is that the player shoots cows with a slingshot into a one-dimensional scene consistin…