从字典中简单创建数据类(dacite的分支)。
项目描述
dacite2
这是dacite
的分支: https://github.com/konradhalas/dacite
此模块简化了从字典创建数据类(PEP 557)的过程。
安装
要安装dacite,只需使用pip
$ pip install dacite2
要求
dacite2
支持的最低Python版本是3.7。
快速开始
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
参数将具有给定类型的数据类的输入数据转换为新值,您必须传递类型为 Mapping[Type, 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
转换每个项。
要针对所有类型,请使用 Any
。针对没有其子类型的集合将针对该类型的所有集合进行目标定位,例如,List
和 Dict
。
@dataclass
class ShoppingCart:
store: str
item_ids: List[int]
data = {
'store': '7-Eleven',
'item_ids': [1, 2, 3],
}
def print_value(value):
print(value)
return value
def print_collection(collection):
for item in collection:
print(item)
return collection
result = from_dict(
data_class=ShoppingCart,
data=data,
config=Config(
type_hooks={
Any: print_value,
List: print_collection
}
)
)
打印
7-Eleven
[1, 2, 3]
1
2
3
如果数据类字段类型是 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
文件中找到当前的配置。
最后但并非最不重要的是,如果您想引入新功能,请首先在问题中进行讨论。
如何开始
克隆 dacite
仓库
$ git clone git@github.com:idanmiara/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 创建。由 Idan Miara 维护。
项目详细信息
下载文件
下载适合您平台的文件。如果您不确定选择哪个,请了解更多关于安装包的信息。