从字典中简单创建数据类。
项目描述
dacite
此模块简化了从字典创建数据类(PEP 557)的过程。
安装
要安装dacite,只需使用pip
$ pip install dacite
要求
dacite
支持的最低Python版本为3.6。
快速入门
from dataclasses import dataclass
from dacite import from_dict
@dataclass
class User:
name: str
age: int
is_active: bool
data = {
'name': 'John',
'age': 30,
'is_active': True,
}
user = from_dict(data_class=User, data=data)
assert user == User(name='John', age=30, is_active=True)
特性
Dacite支持以下特性
- 嵌套结构
- (基本)类型检查
- 可选字段(即
typing.Optional
) - 联合
- 前向引用
- 集合
- 自定义类型钩子
动机
在函数或方法之间传递普通字典作为数据容器不是一种好的做法。当然,你可以始终创建自己的自定义类,但如果你只想在单个对象中合并几个字段,这种解决方案就是一种过度。
幸运的是,Python有很好的解决方案来解决这个问题 - 数据类。感谢@dataclass
装饰器,你可以以声明的方式轻松创建一个具有给定字段列表的新自定义类型。数据类按设计支持类型提示。
然而,即使您在使用数据类,您也必须以某种方式创建它们的实例。在许多此类情况下,您的输入是一个字典 - 它可以来自 HTTP 请求的有效负载或数据库的原始数据。如果您想将这些字典转换为数据类,dacite
就是您的最佳选择。
这个库最初是为了简化创建类型提示的数据传输对象(DTO)而创建的,这些对象可以在应用程序架构的边界之间进行交互。
重要的是要提到,dacite
不是一个数据验证库。有几十个出色的数据验证项目,在dacite
中重复此功能是没有意义的。如果您想首先验证您的数据,您应该将dacite
与数据验证库之一结合使用。
请查看用例部分以获取实际示例。
使用方法
Dacite基于一个单一函数 - dacite.from_dict
。此函数接受3个参数
data_class
- 数据类类型data
- 输入数据的字典config
(可选)- 创建过程的配置,dacite.Config
类的一个实例
配置是一个(数据)类,具有以下字段
type_hooks
cast
forward_references
check_types
strict
strict_unions_match
以下示例展示了from_dict
函数的所有特性和所有Config
参数的使用。
嵌套结构
您可以通过传递嵌套字典的数据来创建合适的结果。
@dataclass
class A:
x: str
y: int
@dataclass
class B:
a: A
data = {
'a': {
'x': 'test',
'y': 1,
}
}
result = from_dict(data_class=B, data=data)
assert result == B(a=A(x='test', y=1))
可选字段
每当您的数据类有一个Optional
字段并且您不会为此字段提供输入数据时,它将取None
值。
from typing import Optional
@dataclass
class A:
x: str
y: Optional[int]
data = {
'x': 'test',
}
result = from_dict(data_class=A, data=data)
assert result == A(x='test', y=None)
联合
如果您的字段可以接受多种类型,您应该使用Union
。Dacite将按顺序尝试将数据与提供的类型匹配。如果没有匹配,它将引发UnionMatchError
异常。
from typing import Union
@dataclass
class A:
x: str
@dataclass
class B:
y: int
@dataclass
class C:
u: Union[A, B]
data = {
'u': {
'y': 1,
},
}
result = from_dict(data_class=C, data=data)
assert result == C(u=B(y=1))
集合
Dacite支持定义为集合的字段。它适用于基本类型和数据类。
@dataclass
class A:
x: str
y: int
@dataclass
class B:
a_list: List[A]
data = {
'a_list': [
{
'x': 'test1',
'y': 1,
},
{
'x': 'test2',
'y': 2,
}
],
}
result = from_dict(data_class=B, data=data)
assert result == B(a_list=[A(x='test1', y=1), A(x='test2', y=2)])
类型钩子
如果您想将数据类字段的输入数据转换为给定类型的新值,可以使用Config.type_hooks
参数。您必须传递以下映射:{Type: callable}
,其中callable
是Callable[[Any], Any]
。
@dataclass
class A:
x: str
data = {
'x': 'TEST',
}
result = from_dict(data_class=A, data=data, config=Config(type_hooks={str: str.lower}))
assert result == A(x='test')
如果数据类字段类型是Optional[T]
,您可以将Optional[T]
或仅T
作为type_hooks
中的键传递。对于泛型集合也是如此,例如,当字段类型为List[T]
时,您可以使用List[T]
来转换整个集合或T
来转换每个项。
转换
这是一个非常常见的情况,您希望仅通过调用您的类型并传入输入值来从输入数据创建字段类型的实例。当然,您可以使用type_hooks={T: T}
来实现此目标,但cast=[T]
是一个更简单、更易于表达的方法。它还适用于基类 - 如果T
是类型S
的基类,则所有类型为S
的字段也将被“转换”。
from enum import Enum
class E(Enum):
X = 'x'
Y = 'y'
Z = 'z'
@dataclass
class A:
e: E
data = {
'e': 'x',
}
result = from_dict(data_class=A, data=data, config=Config(cast=[E]))
# or
result = from_dict(data_class=A, data=data, config=Config(cast=[Enum]))
assert result == A(e=E.X)
向前引用
可以以{'name': Type}
映射的形式将向前引用的定义传递给Config.forward_references
。当评估每个字段的类型时,此字典作为globalns
参数传递给typing.get_type_hints()
。
@dataclass
class X:
y: "Y"
@dataclass
class Y:
s: str
data = from_dict(X, {"y": {"s": "text"}}, Config(forward_references={"Y": Y}))
assert data == X(Y("text"))
类型检查
在很少的情况下,dacite
内置类型检查器无法验证您的类型(例如,自定义泛型类)或您使用其他库覆盖了此类功能,并且您不想重复验证您的类型。在这种情况下,您可以使用Config(check_types=False)
禁用类型检查。默认情况下启用类型检查。
T = TypeVar('T')
class X(Generic[T]):
pass
@dataclass
class A:
x: X[str]
x = X[str]()
assert from_dict(A, {'x': x}, config=Config(check_types=False)) == A(x=x)
严格模式
默认情况下,from_dict
会忽略输入数据中的额外键(不匹配数据类字段)。如果您想改变这种行为,请将 Config.strict
设置为 True
。在遇到意外的键时,from_dict
将引发 UnexpectedDataError
异常。
严格联合匹配
Union
允许为给定的字段定义多个可能的类型。默认情况下,dacite
尝试为提供的数据找到第一个匹配的类型,并返回该类型的实例。这意味着还有可能在 Union
类型列表中存在其他匹配的类型。使用 strict_unions_match
只允许匹配单个类型,否则 dacite
将引发 StrictUnionMatchError
。
异常
每当出现错误时,from_dict
将引发适当的异常。其中有一些
WrongTypeError
- 当输入值的类型与数据类字段的类型不匹配时引发MissingValueError
- 当您未为必填字段提供值时引发UnionMatchError
- 当提供的数据不匹配Union
的任何类型时引发ForwardReferenceError
- 当在数据类中遇到未定义的前向引用时引发UnexpectedDataError
- 当启用strict
模式且输入数据没有匹配的键时引发StrictUnionMatchError
- 当启用strict_unions_match
模式且输入数据具有模糊的Union
匹配时引发
开发
首先,如果您想提交您的拉取请求,非常感谢!我非常感激您的支持。
请记住,每个新功能、错误修复或改进都应该进行测试。100% 的代码覆盖率是必须的。
我们使用一些静态代码分析工具来提高代码质量(black
、mypy
、pylint
)。请在提交 PR 之前确保您没有生成任何错误/警告。您可以在 .travis.yml
文件中找到当前配置。
最后但同样重要的是,如果您想引入新功能,请首先在 issue 中进行讨论。
如何开始
克隆 dacite
仓库
$ git clone git@github.com:konradhalas/dacite.git
以您喜欢的方式创建和激活虚拟环境
$ python3 -m venv dacite-env
$ source dacite-env/bin/activate
安装所有 dacite
依赖项
$ pip install -e .[dev]
要运行测试,只需执行
$ pytest
用例
在我们收到许多“原始”数据(Python 字典)作为系统输入的情况下有很多用例。HTTP 请求有效负载是一个非常常见的用例。在大多数 Web 框架中,我们以简单字典的形式接收请求数据。与其将此字典传递到您的“业务”代码,不如创建一些更“健壮”的代码。
以下是一个简单的 flask
应用 - 它有一个单个的 /products
端点。您可以使用此端点在系统中“创建”产品。我们的核心 create_product
函数期望数据类作为参数。多亏了 dacite
,我们可以轻松地从 POST
请求有效负载构建这样的数据类。
from dataclasses import dataclass
from typing import List
from flask import Flask, request, Response
import dacite
app = Flask(__name__)
@dataclass
class ProductVariantData:
code: str
description: str = ''
stock: int = 0
@dataclass
class ProductData:
name: str
price: float
variants: List[ProductVariantData]
def create_product(product_data: ProductData) -> None:
pass # your business logic here
@app.route("/products", methods=['POST'])
def products():
product_data = dacite.from_dict(
data_class=ProductData,
data=request.get_json(),
)
create_product(product_data=product_data)
return Response(status=201)
如果我们想验证我们的数据(例如,检查 code
是否有 6 个字符)怎么办?这样的功能超出了 dacite
的范围,但我们可以轻松地将其与数据验证库之一结合使用。让我们尝试使用 marshmallow。
首先,我们必须定义我们的数据验证模式
from marshmallow import Schema, fields, ValidationError
def validate_code(code):
if len(code) != 6:
raise ValidationError('Code must have 6 characters.')
class ProductVariantDataSchema(Schema):
code = fields.Str(required=True, validate=validate_code)
description = fields.Str(required=False)
stock = fields.Int(required=False)
class ProductDataSchema(Schema):
name = fields.Str(required=True)
price = fields.Decimal(required=True)
variants = fields.Nested(ProductVariantDataSchema(many=True))
然后在端点中使用它们
@app.route("/products", methods=['POST'])
def products():
schema = ProductDataSchema()
result, errors = schema.load(request.get_json())
if errors:
return Response(
response=json.dumps(errors),
status=400,
mimetype='application/json',
)
product_data = dacite.from_dict(
data_class=ProductData,
data=result,
)
create_product(product_data=product_data)
return Response(status=201)
仍然,dacite
帮助我们从“原始”字典创建具有验证数据的数据类。
更新日志
在 更新日志 中跟踪 dacite
的更新。
作者
由 Konrad Hałas 创建。
项目详细信息
下载文件
下载您平台对应的文件。如果您不确定选择哪个,请了解更多关于 安装包 的信息。