mind pieces

Stay curious. Keep learning!

dataclasses-attrs-pydantic

Dataclasses vs Attrs vs Pydantic

分类:数据科学 • 阅读时间:6分钟

Python 3.7引入了dataclasses,这是一个方便的装饰器,可以使创建类变得更加容易和无缝。本文将比较普通类、”dataclass”和使用attrs的类。Dataclasses基于attrs开发,attrs是一个Python包,同样旨在使创建类变得更加愉快。Dataclasses包含在标准库中(需要Python 3.7+),而要使用attrs,必须通过pip安装(例如,pip install attrs)。它们主要用于自动化编写魔术方法(有时)痛苦的体验。您可以在之前的文章中阅读更多关于魔术方法的内容:https://jackmckew.dev/dunders-in-python.html

Dataclasses vs Attrs vs Pydantic: 功能对比

功能 Dataclass Attrs Pydantic
冻结实例
默认值
转元组
转字典
验证器
转换器
槽类优化
编程式创建

何时使用Dataclasses

Dataclasses主要用于将变量”分组”在一起。在以下情况下选择dataclasses:

  • 主要关注的是变量的类型,而不是值
  • 添加另一个包依赖不是小事

何时使用Attrs

Attrs既涉及分组又涉及验证。在以下情况下选择attrs:

  • 您关心性能(attrs支持槽类生成,针对CPython进行了优化)

何时使用Pydantic

Pydantic是关于彻底的数据验证。在以下情况下选择pydantic:

  • 您想要验证每个类中的值
  • 您想要清理输入

示例类

首先,让我们从Python中默认实现的示例类开始。

我们将在类定义中使用类型提示,这是确保变量是我们预期类型的最佳实践。类型提示也集成到attrs中用于创建类。

1
2
3
4
5
6
7
import typing

class Data:
def __init__(self, x: float=None, y:float=None, kwargs:typing.Dict=None):
self.x = x
self.y = y
self.kwargs = kwargs

传递给__init__构造函数的参数在实例化类的参数时会重复。如果参数和参数不匹配,通常不会出现这种情况。幸运的是,dataclasses和attrs都可以帮助我们解决这个问题(我们将在后面看到)。

现在为了演示dataclasses和attrs为我们自动化的所有不同功能,让我们定义一个函数,该函数接受类构造函数并为我们每个类打印出所有不同的元素。

1
2
3
4
5
6
7
8
9
def class_tester(class_constructor):
test_class_1 = class_constructor()
test_class_2 = class_constructor()

print(f"Repr/str魔术方法表示: {test_class_1}")
print(f"相等性魔术方法(使用==)(如果实现应为True): {test_class_1 == test_class_2}")
print(f"相等性魔术方法(使用is)(如果实现应为True): {test_class_1 is test_class_2}")

class_tester(Data)

输出:

1
2
3
Repr/str魔术方法表示: <__main__.Data object at 0x00000269A5758A90>
相等性魔术方法(使用==)(如果实现应为True): False
相等性魔术方法(使用is)(如果实现应为True): False

Dataclasses

Dataclasses默认自动为类初始化一堆魔术方法,例如:

  • __init__ 类的初始化方法
  • __repr__ 类在使用print()时的表示方式
  • __str__ 类作为字符串的表示方式(与__repr__一起调用)
  • __eq__ 使用相等运算符时使用(例如,==)
  • __hash__ 类的哈希值(与__eq__一起调用)

还有其他一堆也可以自动化的魔术方法,详情见:https://docs.python.org/3/library/dataclasses.html

感谢Twitter上的Michael Kosher:值得注意的是,可以使用__post_init__钩子向dataclasses添加验证。但是,相对于attrs/#pydantic来说,它相当低级。我做了类似的比较:https://mpkocher.github.io/2019/05/22/Dataclasses-in-Python-3-7/

1
2
3
4
5
6
7
8
9
from dataclasses import dataclass

@dataclass
class Data:
x: float = None
y: float = None
kwargs: typing.Dict = None

class_tester(Data)

输出:

1
2
3
Repr/str魔术方法表示: Data(x=None, y=None, kwargs=None)
相等性魔术方法(使用==)(如果实现应为True): True
相等性魔术方法(使用is)(如果实现应为True): False

Attrs

最后我们有了attrs类,attrs有两个主要的”函数”:attr.sattr.ib()attr.s是放在类上的装饰器,用于让包为我们初始化魔术方法,而attr.ib()可以(可选)用于定义类的参数。attr.sattr.ib()都有许多可选参数,文档在:https://www.attrs.org/en/stable/api.html。主要可选参数用于启用/禁用类中的不同魔术方法。

1
2
3
4
5
6
7
8
9
import attr

@attr.s
class Data:
x: float = attr.ib(default=None)
y: float = attr.ib(default=None)
kwargs: typing.Dict = attr.ib(default=None)

class_tester(Data)

输出:

1
2
3
Repr/str魔术方法表示: Data(x=None, y=None, kwargs=None)
相等性魔术方法(使用==)(如果实现应为True): True
相等性魔术方法(使用is)(如果实现应为True): False

Attrs深入

Attrs中的验证器

attrs有一个主要功能而dataclasses没有,那就是验证器。这使我们能够确保在创建类时验证输入到任何特定值。让我们构建一个示例,确保我们的参数x大于42,如果不是,则向用户引发错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import attr

@attr.s
class ValidatedData:
x: float = attr.ib(default=None,validator=attr.validators.instance_of(int))
y: float = attr.ib(default=None)
kwargs: typing.Dict = attr.ib(default=None)

@x.validator
def more_than_the_meaning_of_life(self, attribute, value):
if not value >= 42:
raise ValueError("必须大于生命的意义!")

test_data_point_1 = ValidatedData(42)
test_data_point_2 = ValidatedData(-35) # 这将引发ValueError

Attrs中的转换器

转换器用于在创建类时清理输入数据。如果我们想支持用户创建我们打算为整数的参数,我们可以使用转换器清理此输入。这使我们的类对用户更加灵活,同时仍保持参数背后类型的稳定性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import attr

@attr.s
class ConvertedData:
x: float = attr.ib(default=None,converter=int)
y: float = attr.ib(default=None)
kwargs: typing.Dict = attr.ib(default=None)

@x.validator
def more_than_the_meaning_of_life(self, attribute, value):
if not value >= 42:
raise ValueError("必须大于生命的意义!")

test_data_point_1 = ConvertedData(42)
print(test_data_point_1)

test_data_point_2 = ConvertedData("42") # 字符串"42"将被转换为整数42
print(test_data_point_2)

输出:

1
2
ConvertedData(x=42, y=None, kwargs=None)
ConvertedData(x=42, y=None, kwargs=None)

Attrs的编程式创建

在某些情况下,您可能希望以编程方式创建类,好吧,attrs不会让我们失望,并为我们提供了一种方法!我们可以轻松地传递所需的所有参数字典。

1
2
3
4
5
6
7
8
ProgrammaticData = attr.make_class("Data",
{'x': attr.ib(default=None),
'y': attr.ib(default=None),
'kwargs': attr.ib(default=None)}
)

print(Data())
print(ProgrammaticData())

输出:

1
2
Data(x=None, y=None, kwargs=None)
Data(x=None, y=None, kwargs=None)

Pydantic Dataclasses

Pydantic是一个使用Python类型注解进行数据验证和设置管理的Python包。完美,这正是我们试图用dataclasses和attrs做的事情。更重要的是,pydantic提供了一个dataclass装饰器,以在我们的dataclasses上启用数据验证。这使我们能够创建具有数据验证的可扩展类,甚至比attrs更容易!

这里的最大好处是,默认情况下,类型注解在运行时被强制执行,任何无效数据都会引发格式良好的错误。

1
2
3
4
5
6
7
8
9
10
from pydantic.dataclasses import dataclass
import typing

@dataclass
class Data:
x: float = None
y: float = None
kwargs: typing.Dict = None

class_tester(Data)

输出:

1
2
3
Repr/str魔术方法表示: Data(x=None, y=None, kwargs=None)
相等性魔术方法(使用==)(如果实现应为True): True
相等性魔术方法(使用is)(如果实现应为True): False

pydantic还自动实现转换和数据验证,让我们测试一下。

1
2
3
4
test_data_point = Data(x='123')  # 字符串'123'被转换为浮点数123.0
print(test_data_point)

test_data_point_2 = Data(x='t') # 这将引发ValidationError

输出:

1
2
3
4
Data(x=123.0, y=None, kwargs=None)
ValidationError: 1 validation error for Data
x
value is not a valid float (type=type_error.float)

正如我们在上面看到的,当数据验证失败时,它向开发人员提供了一个格式良好的错误消息,并在需要时平滑地清理输入。