如何使用数据类在 Pydantic 中构建自引用模型?
How to build a self-referencing model in Pydantic with dataclasses?
我正在使用 FastAPI 和 pydantic 构建一个 API。
由于我遵循 DDD / clean 架构,它将模型的定义与持久层的定义分开,我在我的模型中使用标准库数据classes,然后使用将它们映射到 SQLAlchemy 表命令式映射(即 classical 映射)。
这非常有效:
@dataclass
class User:
name: str
age: int
@pydantic.dataclasses.dataclass
class PydanticUser(User):
...
但是,我在使用自引用定义 class 时遇到了问题。
✅ class 继承自 Pydantic 的 BaseModel 可以自引用
继承 pydantic 的 BaseModel 是可行的,但是 this is not compatible 使用 SQLAlchemy 命令式映射,我想用它来坚持干净的架构/DDD 原则。
class BaseModelPerson(BaseModel):
name: str
age: int
parent_person: BaseModelPerson = None
BaseModelPerson.update_forward_refs()
john = BaseModelPerson(name="John", age=49, parent_person=None)
tim = BaseModelPerson(name="Tim", age=14, parent_person=john)
print(john)
# BaseModelPerson(name='John', age=49, parent_person=None)
print(tim)
# BaseModelPerson(name='Tim', age=14, parent_person=BaseModelPerson(name='John', age=49, parent_person=None))
✅ 标准库数据classes也可以自引用
from __future__ import annotations
from dataclasses import dataclass
@dataclass
class StdlibPerson:
name: str
age: int
parent: StdlibPerson
john = StdlibPerson(name="John", age=49, parent=None)
tim = StdlibPerson(name="Tim", age=14, parent=john)
print(john)
# StdlibPerson(name='John', age=49, parent=None)
print(tim)
# StdlibPerson(name='Tim', age=14, parent=StdlibPerson(name='John', age=49, parent=None))
❌ Pydantic 数据class 转换导致递归错误
当我尝试将标准库数据class 转换为 pydantic 数据class 时出现问题。
像这样定义 Pydantic 数据class:
PydanticPerson = pydantic.dataclasses.dataclass(StdlibPerson)
returns一个错误:
# output (hundreds of lines - that is recursive indeed)
# The name of an attribute on the class where we store the Field
File "pydantic/main.py", line 990, in pydantic.main.create_model
File "pydantic/main.py", line 299, in pydantic.main.ModelMetaclass.__new__
File "pydantic/fields.py", line 411, in pydantic.fields.ModelField.infer
File "pydantic/fields.py", line 342, in pydantic.fields.ModelField.__init__
File "pydantic/fields.py", line 456, in pydantic.fields.ModelField.prepare
File "pydantic/fields.py", line 673, in pydantic.fields.ModelField.populate_validators
File "pydantic/class_validators.py", line 255, in pydantic.class_validators.prep_validators
File "pydantic/class_validators.py", line 238, in pydantic.class_validators.make_generic_validator
File "/usr/lib/python3.9/inspect.py", line 3111, in signature
return Signature.from_callable(obj, follow_wrapped=follow_wrapped)
File "/usr/lib/python3.9/inspect.py", line 2860, in from_callable
return _signature_from_callable(obj, sigcls=cls,
File "/usr/lib/python3.9/inspect.py", line 2323, in _signature_from_callable
return _signature_from_function(sigcls, obj,
File "/usr/lib/python3.9/inspect.py", line 2155, in _signature_from_function
if _signature_is_functionlike(func):
File "/usr/lib/python3.9/inspect.py", line 1883, in _signature_is_functionlike
if not callable(obj) or isclass(obj):
File "/usr/lib/python3.9/inspect.py", line 79, in isclass
return isinstance(object, type)
RecursionError: maximum recursion depth exceeded while calling a Python object
这样定义 StdlibPerson 并不能解决问题:
@dataclass
class StdlibPerson
name: str
age: int
parent: "Person" = None
也不使用 pydantic 文档提供的第二种方式:
@pydantic.dataclasses.dataclass
class PydanticPerson(StdlibPerson)
...
❌直接使用 Pydantic 数据classes
from __future__ import annotations
from pydantic.dataclasses import dataclass
from typing import Optional
@pydantic.dataclasses.dataclass
class PydanticDataclassPerson:
name: str
age: int
parent: Optional[PydanticDataclassPerson] = None
john = PydanticDataclassPerson(name="John", age=49, parent=None)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 6, in __init__
File "pydantic/dataclasses.py", line 97, in pydantic.dataclasses._generate_pydantic_post_init._pydantic_post_init
# | False | | |
File "pydantic/main.py", line 1040, in pydantic.main.validate_model
File "pydantic/fields.py", line 699, in pydantic.fields.ModelField.validate
pydantic.errors.ConfigError: field "parent" not yet prepared so type is still a ForwardRef, you might need to call PydanticDataclassPerson.update_forward_refs().
>>> PydanticDataclassPerson.update_forward_refs()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: type object 'PydanticDataclassPerson' has no attribute 'update_forward_refs'
问题
我如何定义一个带有自引用对象的 pydantic 模型,以便它与 SQLAlchemy 命令式映射兼容?
似乎没有简单的解决方案来构建具有 FastAPI 和 self-referencing 对象的 REST API。
我决定切换到 FastAPI / GraphQL 堆栈,Strawberry library which is explicitly recommended in the FastAPI documentation。
到目前为止没问题,Strawberry 可以轻松构建 GraphQL 服务器,并且可以轻而易举地处理 self-referencing 对象。
#!/usr/bin/env python3.10
# src/my_app/entrypoints/api/schema.py
import typing
import strawberry
@strawberry.type
class Person:
name: str
age: int
parent: Person | None
我正在使用 FastAPI 和 pydantic 构建一个 API。
由于我遵循 DDD / clean 架构,它将模型的定义与持久层的定义分开,我在我的模型中使用标准库数据classes,然后使用将它们映射到 SQLAlchemy 表命令式映射(即 classical 映射)。
这非常有效:
@dataclass
class User:
name: str
age: int
@pydantic.dataclasses.dataclass
class PydanticUser(User):
...
但是,我在使用自引用定义 class 时遇到了问题。
✅ class 继承自 Pydantic 的 BaseModel 可以自引用
继承 pydantic 的 BaseModel 是可行的,但是 this is not compatible 使用 SQLAlchemy 命令式映射,我想用它来坚持干净的架构/DDD 原则。
class BaseModelPerson(BaseModel):
name: str
age: int
parent_person: BaseModelPerson = None
BaseModelPerson.update_forward_refs()
john = BaseModelPerson(name="John", age=49, parent_person=None)
tim = BaseModelPerson(name="Tim", age=14, parent_person=john)
print(john)
# BaseModelPerson(name='John', age=49, parent_person=None)
print(tim)
# BaseModelPerson(name='Tim', age=14, parent_person=BaseModelPerson(name='John', age=49, parent_person=None))
✅ 标准库数据classes也可以自引用
from __future__ import annotations
from dataclasses import dataclass
@dataclass
class StdlibPerson:
name: str
age: int
parent: StdlibPerson
john = StdlibPerson(name="John", age=49, parent=None)
tim = StdlibPerson(name="Tim", age=14, parent=john)
print(john)
# StdlibPerson(name='John', age=49, parent=None)
print(tim)
# StdlibPerson(name='Tim', age=14, parent=StdlibPerson(name='John', age=49, parent=None))
❌ Pydantic 数据class 转换导致递归错误
当我尝试将标准库数据class 转换为 pydantic 数据class 时出现问题。
像这样定义 Pydantic 数据class:
PydanticPerson = pydantic.dataclasses.dataclass(StdlibPerson)
returns一个错误:
# output (hundreds of lines - that is recursive indeed)
# The name of an attribute on the class where we store the Field
File "pydantic/main.py", line 990, in pydantic.main.create_model
File "pydantic/main.py", line 299, in pydantic.main.ModelMetaclass.__new__
File "pydantic/fields.py", line 411, in pydantic.fields.ModelField.infer
File "pydantic/fields.py", line 342, in pydantic.fields.ModelField.__init__
File "pydantic/fields.py", line 456, in pydantic.fields.ModelField.prepare
File "pydantic/fields.py", line 673, in pydantic.fields.ModelField.populate_validators
File "pydantic/class_validators.py", line 255, in pydantic.class_validators.prep_validators
File "pydantic/class_validators.py", line 238, in pydantic.class_validators.make_generic_validator
File "/usr/lib/python3.9/inspect.py", line 3111, in signature
return Signature.from_callable(obj, follow_wrapped=follow_wrapped)
File "/usr/lib/python3.9/inspect.py", line 2860, in from_callable
return _signature_from_callable(obj, sigcls=cls,
File "/usr/lib/python3.9/inspect.py", line 2323, in _signature_from_callable
return _signature_from_function(sigcls, obj,
File "/usr/lib/python3.9/inspect.py", line 2155, in _signature_from_function
if _signature_is_functionlike(func):
File "/usr/lib/python3.9/inspect.py", line 1883, in _signature_is_functionlike
if not callable(obj) or isclass(obj):
File "/usr/lib/python3.9/inspect.py", line 79, in isclass
return isinstance(object, type)
RecursionError: maximum recursion depth exceeded while calling a Python object
这样定义 StdlibPerson 并不能解决问题:
@dataclass
class StdlibPerson
name: str
age: int
parent: "Person" = None
也不使用 pydantic 文档提供的第二种方式:
@pydantic.dataclasses.dataclass
class PydanticPerson(StdlibPerson)
...
❌直接使用 Pydantic 数据classes
from __future__ import annotations
from pydantic.dataclasses import dataclass
from typing import Optional
@pydantic.dataclasses.dataclass
class PydanticDataclassPerson:
name: str
age: int
parent: Optional[PydanticDataclassPerson] = None
john = PydanticDataclassPerson(name="John", age=49, parent=None)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 6, in __init__
File "pydantic/dataclasses.py", line 97, in pydantic.dataclasses._generate_pydantic_post_init._pydantic_post_init
# | False | | |
File "pydantic/main.py", line 1040, in pydantic.main.validate_model
File "pydantic/fields.py", line 699, in pydantic.fields.ModelField.validate
pydantic.errors.ConfigError: field "parent" not yet prepared so type is still a ForwardRef, you might need to call PydanticDataclassPerson.update_forward_refs().
>>> PydanticDataclassPerson.update_forward_refs()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: type object 'PydanticDataclassPerson' has no attribute 'update_forward_refs'
问题
我如何定义一个带有自引用对象的 pydantic 模型,以便它与 SQLAlchemy 命令式映射兼容?
似乎没有简单的解决方案来构建具有 FastAPI 和 self-referencing 对象的 REST API。
我决定切换到 FastAPI / GraphQL 堆栈,Strawberry library which is explicitly recommended in the FastAPI documentation。
到目前为止没问题,Strawberry 可以轻松构建 GraphQL 服务器,并且可以轻而易举地处理 self-referencing 对象。
#!/usr/bin/env python3.10
# src/my_app/entrypoints/api/schema.py
import typing
import strawberry
@strawberry.type
class Person:
name: str
age: int
parent: Person | None