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

简单介绍 Python 中的 `dataclass`:使用场景与代码示例 阅读详情
AI摘要

本文介绍了Python中数据类的相关内容。对比了tuple、namedtuple等数据结构在表示数据时的优劣,重点阐述了dataclass装饰器简化数据类创建的原理和使用方法,包括类型提示、数据嵌套等。还给出了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 namedtuple

Player = 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 NamedTuple

class Player(NamedTuple):
    name: str
    number: int
    position: str
    age: int
    grade: str

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的一些问题,如不可变性等。

使用dict

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

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

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

使用typing.TypedDict

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

from typing import TypedDict

class Player(TypedDict):
    name: str
    number: int
    position: str
    age: int

jordan: Player = {'name': 'James Harden', 'number': 1, 'position': 'PG', 'age': 34}

print(jordan['position'])  # Output: PG

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

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

class Player:

    def __init__(self, name, number, position, age, grade):
        self.name = name
        self.number = number
        self.position = position
        self.age = age
        self.grade = grade


harden = 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 Bryant

print(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 = name
        self.number = number
        self.position = position
        self.age = age
        self.grade = grade

    def __repr__(self):
        return f'Player: {self.name} : {self.age}'

    def __gt__(self, other):
        return self.age > other.age

    def __eq__(self, other):
        return self.age == other.age


harden = 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 Bryant

print(harden)  # Player: James Harden : 34

print(harden < bryant)  # True

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

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

from dataclasses import dataclass


@dataclass(order=True)
class Player:
    name: str
    number: int
    position: str

    grade: str
    age: 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 Bryant

print(harden)  # Player(name='James Harden', number=1, position='PG', grade='S+', age=34)

# 比较, 默认按照属性定义的顺序比较的
print(harden < bryant)  # True

dataclass相较于dict和tuple具有明显优势。它能更精确地指定每个成员变量的类型,同时提供字段名的检查,大大降低了出错的可能性。相对于传统的类定义,使用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 methods
    added 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. If
    repr is true, a __repr__() method is added. If order is true, rich
    comparison dunder methods are added. If unsafe_hash is true, a
    __hash__() method function is added. If frozen is true, fields may
    not be assigned to after instance creation. If match_args is true,
    the __match_args__ tuple is added. If kw_only is true, then by
    default 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: str
    unit_price: float
    quantity_on_hand: int = 0

    def 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 = name
    self.unit_price = unit_price
    self.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: str
    number: int
    position: str
    grade: str
    age: int = 18

    def __eq__(self, other):
        return self.age == other.age  # 只比较age

    def __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: str
    number: int
    position: str
    grade: str
    age: int = 18
    team: 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: str
    number: int
    position: str
    grade: str
    age: int = 18


@dataclass
class Team:
    name: str
    players: 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)  # 18

print(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: str
    age: int


@dataclass(order=True)
class Player(Person):
    number: int
    position: str
    grade: str
    team: 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: str
    number: int
    position: str
    grade: str
    age: int = 18
    args: 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: str
    number: int
    position: str
    grade: str
    age: 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
    players: 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: str
    number: int
    position: str
    grade: str
    age: 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 a
    0-argument function called to initialize a field's value.  If init
    is true, the field will be a parameter to the class's __init__()
    function.  If repr is true, the field will be included in the
    object's repr().  If hash is true, the field will be included in the
    object's hash().  If compare is true, the field will be used in
    comparison functions.  metadata, if specified, must be a mapping
    which is stored but not otherwise examined by dataclass.  If kw_only
    is 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 datetime


class ValidationError(Exception):
    def __init__(self, field_name, condition, actual_value):
        self.field_name = field_name
        self.condition = condition
        self.actual_value = actual_value
        super().__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: str
    number: int
    position: str
    grade: str
    age: int = 18

    def __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: str
    number: int
    position: str
    grade: str
    age: 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: str
    number: int
    position: str = field(hash=False)  # 不参与hash
    grade: str
    age: 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: str
    number: int
    position: str
    grade: str
    age: int = 18
    args: 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: str
    number: int
    position: str
    grade: str
    age: 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数据类可以配合一些校验工具包和数据提取工具包以实现数据提取或参数校验的工作,以下是配合marshmallow、desert实现数据校验提取工作的示例:

import requests
from dataclasses import dataclass
import dataclasses
from marshmallow import fields, EXCLUDE, validate
import desert


@dataclass
class Activity:
    activity: str
    participants: 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 * 100


def 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._instance

    def 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)

    @classmethod
    def 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 = 8080
    print("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, Union

class Grade(Enum):
    S_PLUS = 'S+'
    # 定义其他等级...

@dataclass
class Player:
    name: str
    number: int
    position: str
    grade: Grade
    age: int = 18

def 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

python:dataclasses --- 数据类 阅读详情

相关推荐

Python中的@dataclass装饰器

简单来说,就是你定义一个很普通的类,@dataclass装饰器可以帮你生成__repr__、__init__等特殊方法,就不用自己写一遍了。为了解决实例化时,参数过多时的痛点,我们可以在MyClass这个类的上面添加一个装饰器@dataclass,如下所示。其它参数类似,从上面可以看到,如果不加参数,默认情况下,@dataclass装饰器会生成。方法,如果不想让装饰器生成对应的方法,可以在入参时候将其改为False。方法,如果不想让装饰器生成对应的方法,可以在入参时候将其改为False。..........

北辰 2455

Python中的数据类dataclass详解

1. 为什么需要数据类 1.1 ☹️内置数据类型的局限 假设遇到一个场景, 需要保存一些运动员信息的数据对象. 可以使用基本的数据类型tuple和dict实现.如需要创建一个球员jordan, 信息包括球员姓名, 号码, 位置, 年龄. 使用tuple In [1]: jordan = ('Micheal Jordan', 23, 'PG', 29) In [2]: jordan[0] Out[2]: 'Micheal Jordan' 劣势: 创建和取值基于位置, 需要记住坐标对应的....

be5yond的博客 3万+

【Docker】从零构建达梦DM8轻量级服务镜像:实战与优化

本文详细介绍了从零开始构建达梦DM8轻量级Docker镜像的完整实战流程。通过分析传统安装方式的痛点,阐述了使用Docker封装数据库的优势,并重点讲解了如何利用静默安装、精简组件、多阶段构建等优化技巧,打造一个体积小巧、启动迅速、配置灵活的DM8服务镜像,助力实现开发、测试与部署的环境一致性与高效性。

ooo22的博客 518

[python]数据类(dataclass)简介

介绍了数据类装饰器dataclasses,和field的用法;以及如何自定义数据类比较和后处理

飞鹰技术 5179

某盾blackBox逆向——纯算

某盾blackBox逆向——纯算

Lzyydsb的博客 5891

IAR9.30以上版本安装、注册、新建工程和配置过程详细介绍

IAR 一般是指一款嵌入式软件的集成开发环境,类似于 MDK-Keil 这款软件。IAR 对于不同的内核处理器,是对应不同的 IAR 软件的,IAR 到目前为止支持大部分的MCU,比如8051系列、ARM架构系列、MSP430系列、AVR系列等等这些常用的芯片架构。对于 ARM 架构的芯片,有对应的 IAR Embedded Workbench for ARM 软件平台,因为我主要是使用 ARM 架构芯片,下面安装、注册和使用都是基于这个版本进行介绍的。

luobeihai的博客 6万+

[python]一文带你了解什么是dataclass?

通过对field()对象的剖析,我们可以指定属性:是否参与比较,是否参与hash计算等等。不过我们知道默认的比较顺序,我们也可以通过增加属性以实现按需比较的功能。而这个用于比较的属性位于数据类的第一个属性,并可以借助魔法函数实现灵活赋值。sort_index: tuple = field(init=False) # 添加一个 sort_index 字段,并设置为不在 __init__ 方法中初始化name: strgrade: str。

weixin_68789096的博客 2652

Laravel 结合 jwt-auth 实现无状态 API 认证(实战解析)

本文详细解析了如何在Laravel框架中结合jwt-auth实现无状态API认证,包括JWT的优势、环境搭建、Token签发与刷新、API路由保护等实战内容。通过具体代码示例和优化技巧,帮助开发者高效解决跨域认证问题,提升系统安全性。

weixin_29315569的博客 209

一文带你理解Python的dataclass装饰器

理解Python的dataclass装饰器

代码说,代码让世界更美好。 5167

用SOLIDWORKS从零画一个履带机器人,手把手教你搞定底盘和传动

本文详细介绍了如何使用SOLIDWORKS从零开始设计履带式机器人,重点讲解底盘结构优化和传动系统设计的关键步骤。通过七个阶段的实战指南,包括前期规划、底盘建模、驱动系统装配、履带生成、动态模拟等,帮助工程师和爱好者掌握履带机器人的核心设计技巧,提升地形适应能力和动力效率。

weixin_30872499的博客 632

YOLO26优化:特征融合 | 一种新颖的多尺度特征融合iAFF,适配小目标检测

在YOLO26中如何使用:iAFF加入Neck替代Concat;

①答疑群聊服务;②YOLO大模型知识问答系统;③计算机视觉论文生成智能体; 266

一文了解 Python3.7 新特性——dataclass装饰器

本文首发于微信公众号:wanpython,文末可以直接扫码关注 关注即可获取 Python 网络爬虫、数据分析、机器学习、大数据等学习资料,期待你的加入! 如有建议或意见,欢迎留言 原文链接:mp.weixin.qq.com/s/XC8cDsMH8… 引言 Python 3.7中一个令人兴奋的新特性是 data classes 。 数据类通常是一个主要包含数据的类,尽管实际上没有任何限制。 它...

weixin_34318956的博客 709

把核心变干净的代价与回报:为什么推动 Clean Core,几乎绕不开 SAP Integration Suite 与 API Management

Clean Core的核心价值在于构建可持续升级的ERP系统架构,其关键不在于减少代码或限制扩展,而是通过解耦和治理实现系统边界的清晰划分。这一理念将推动企业从传统的"私搭乱建"式集成向平台化工程转型,而SAP Integration Suite和API Management正是实现这一转型的核心组件。 Integration Suite作为集成中枢,通过统一平台、可复用内容库和事件驱动架构,将碎片化连接转变为标准化能力;API Management则负责建立接口治理体系,通过统一入口、

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 241

python的dataclass详解

dataclass是python3.7引入的装饰器。dataclass 是一个代码生成器,它能帮你自动编写那些用于存储数据的类中“枯燥”的、重复性的方法,比如()、()、() 等。这样就可以让存储数据的类变动更简洁、更易读。1 不使用dataclass如果定义中不适用dataclass,我们需要如何定义一个存储数据的类呢?

Criss@陈磊 1128

《QGIS快速入门与应用基础》009:国内镜像源(清华大学/阿里云)下载技巧

针对国内用户下载QGIS速度慢、易中断的问题,推荐使用清华大学和阿里云两大国内镜像源,提供3-10MB/s的极速下载方案。详细解析镜像源核心优势、分步骤下载指南及校验方法,对比清华(同步快、北方优选)与阿里云(南方节点多)的特点。针对常见问题如版本延迟、404错误等提供解决方案,并介绍搭配IDM等工具实现多线程加速的进阶技巧。通过镜像源+下载工具组合,用户可高效获取QGIS安装包,为后续系统安装奠定基础。

LuckyHanMo的博客 1729

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

python dataclass_Python3.7 dataclass使用指南

本文将带你走进python3.7的新特性dataclass,通过本文你将学会dataclass的使用并避免踏入某些陷阱。dataclass简介dataclass的定义位于[PEP-557](https://www.python.org/dev/peps/pep-0557/),根据定义一个dataclass是指“一个带有默认值的可变的namedtuple”,广义的定义就是有一个类,它的属性均可公开访...

weixin_32192681的博客 2132

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

Python的dataclass特性

dataclass 是 Python 的一个装饰器,它可以自动为类添加一些特殊方法,如__init__()__repr__()__eq__()等。它的主要目的是简化创建主要用于存储数据的类。@dataclasswidth: float # 未定义默认值的字段,实例化时赋值height: float = 10.0 # 直接定义默认值的字段,类定义时执行color: str = field(default="white") # 使用field定义默认值的字段,类定义时执行。

昊大侠的博客 1066

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

掌握python的dataclass,让你的代码更简洁优雅!

init@dataclassid: str@dataclass@dataclass最后希望你编程学习上不急不躁,按照计划有条不紊推进,把任何一件事做到极致,都是不容易的,加油,努力!相信自己!文末福利最后这里免费分享给大家一份Python全套学习资料,希望能帮到那些不满现状,想提升自己却又没有方向的朋友,也可以和我一起来学习交流呀。包含编程资料、学习路线图、源代码、软件安装包等![点击这里]】领取!① Python所有方向的学习路线图,清楚各个方向要学什么东西。

zhang120529的博客 1117

python dataclass_理解 Python 的 Dataclasses(一)

如果你正在阅读本文,那么你已经意识到了 Python 3.7 以及它所包含的新特性。就我个人而言,我对 Dataclasses 感到非常兴奋,因为我等了它一段时间了。本系列包含两部分:Dataclass 特点概述在下一篇文章概述 Dataclass 的 fields介绍Dataclasses 是 Python 的类(LCTT 译注:更准确的说,它是一个模块),适用于存储数据对象。你可能会问什么是数...

weixin_31227315的博客 2963

68、Python之函数高级:一文搞懂dataclass,快速定义数据类

dataclass的特性是在Python3.7之后开始引入的,如果自己的Python环境是相对较老的版本需要注意一下。在开始使用dataclass之前,我们先来看下dataclass的定义:从定义中,我们应该有如下认知:1、dataclass是一个函数装饰器,这个函数装饰器用于对类进行装饰增强。2、这个装饰器的实现是基于类中定义的字段,自动生成相应的方法。

南宫理的日知录 2004

Python中dataclass库

dataclass的定义位于PEP-557,根据定义一个dataclass是指“一个带有默认值的可变的namedtuple”,广义的定义就是有一个类,它的属性均可公开访问,可以带有默认值并能被修改,而且类中含有与这些属性相关的类方法,那么这个类就可以称为dataclass,再通俗点讲,dataclass就是一个含有数据及操作数据方法的容器。相比普通class,dataclass通常不包含私有属性,数据可以直接访问dataclass的repr方法通常有固定格式,会打印出类型名以及属性名和它的值。

一个专注于机器学习基础与实战的技术博客,内容涵盖算法推导、模型实现、数学原理与代码实践。用通俗的语言解析复杂概念,记录学习过程中的思考与总结,适合机器学习爱好者和从业者参考。 1203

python dataclass_Python 3.7中dataclass的终极指南(一)

python3.7 的dataclass新特性大大简化了定义类对象的代码量,代码简洁明晰。通过使用@dataclass装饰器来修饰类的设计,例如from dataclasses import dataclass@dataclassclass DataClassCard:rank: strsuit: str#生成实例queen_of_hearts = DataClassCard('Q', 'Hear...

weixin_42451611的博客 3049

python dataclass_Python 3.7 将引入 dataclass 装饰器

简评:Python 3.7 将于今年夏天发布,Python 3.7 中将会有许多新东西,最激动人心的新功能之一是 dataclass 装饰器。什么是 Data Class大多数 Python 开发人员编写过很多像下面这样的类:dataclass 可以为简单的情况自动生成方法,例如,一个init接受这些参数并将其分配给自己,之前的小例子可以重写为:那么通过一个例子来看看如何使用吧星球大战 API可以...

weixin_39617405的博客 117

python dataclass_Python dataclass 源码阅读与分析

Python dataclass 源码阅读与分析先来看看如何使用In [1]: class FooDataClass:...: def __init__(self, name, num=1):...: self.name = name...: self.num = num...:In [2]: from dataclasses import dataclas...

weixin_39607935的博客 446

python dataclass与dataclass_json修饰器的理解

python dataclass及dataclass_json修饰器的理解 dataclass可用于自动生成类的魔术方法 dataclass_json可以用于将json格式数据转化为类的实例变量 dataclass_json必须结合dataclass使用 dataclass_json可以通过添加类型注释和循环定义,实现深层json调用 实例: from dataclasses import dataclass from dataclasses_json import dataclass_json from

Peanut_ZHANG的博客 4505

掌握Python的dataclass,让你的代码更简洁优雅!

随着"Python"版本的不断更新,"dataclass"也逐步发展和完善,为"Python"开发者提供了更加便捷的数据类创建和管理方式。"dataclass"的主要功能在于帮助我们简化数据类的定义过程。

m0_75067629的博客 1148
上一篇: python面试题大全(四)
下一篇: 【python3】多线程详解
海哥python
博客等级 码龄9年 1487粉丝 74原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

海哥python

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值