Отложенные аннотации
Отложенные аннотации (как описано в PEP563 ) «просто работают».
from __future__ import annotations
from typing import Any
from pydantic import BaseModel
class Model(BaseModel):
a: list[int]
b: Any
print(Model(a=('1', 2, 3), b='ok'))
#> a=[1, 2, 3] b='ok'
Внутри Pydantic будет вызывать метод, аналогичный typing.get_type_hints
для разрешения аннотаций.
Даже без использования from __future__ import annotations
, в случаях, когда ссылочный тип еще не определен, можно использовать ForwardRef
или строку:
from typing import ForwardRef
from pydantic import BaseModel
Foo = ForwardRef('Foo')
class Foo(BaseModel):
a: int = 123
b: Foo = None
print(Foo())
#> a=123 b=None
print(Foo(b={'a': '321'}))
#> a=123 b=Foo(a=321, b=None)
Самоссылающиеся (или «рекурсивные») модели¶
Также поддерживаются модели с ссылающимися на себя полями. Поля, ссылающиеся на себя, будут автоматически разрешены после создания модели.
Внутри модели вы можете обратиться к еще не построенной модели, используя строку:
from pydantic import BaseModel
class Foo(BaseModel):
a: int = 123
#: The sibling of `Foo` is referenced by string
sibling: 'Foo' = None
print(Foo())
#> a=123 sibling=None
print(Foo(sibling={'a': '321'}))
#> a=123 sibling=Foo(a=321, sibling=None)
Если вы используете from __future__ import annotations
вы также можете просто обратиться к модели по имени ее типа:
from __future__ import annotations
from pydantic import BaseModel
class Foo(BaseModel):
a: int = 123
#: The sibling of `Foo` is referenced directly by type
sibling: Foo = None
print(Foo())
#> a=123 sibling=None
print(Foo(sibling={'a': '321'}))
#> a=123 sibling=Foo(a=321, sibling=None)
Циклические ссылки¶
При работе с самоссылающимися рекурсивными моделями вы можете столкнуться с циклическими ссылками во входных данных проверки. Например, это может произойти при проверке экземпляров ORM с обратными ссылками из атрибутов.
Вместо того, чтобы вызывать Python RecursionError
при попытке проверить данные с помощью циклических ссылок, Pydantic может обнаружить циклическую ссылку и вызвать соответствующую ошибку ValidationError
:
from typing import Optional
from pydantic import BaseModel, ValidationError
class ModelA(BaseModel):
b: 'Optional[ModelB]' = None
class ModelB(BaseModel):
a: Optional[ModelA] = None
cyclic_data = {}
cyclic_data['a'] = {'b': cyclic_data}
print(cyclic_data)
#> {'a': {'b': {...}}}
try:
ModelB.model_validate(cyclic_data)
except ValidationError as exc:
print(exc)
"""
1 validation error for ModelB
a.b
Recursion error - cyclic reference detected [type=recursion_loop, input_value={'a': {'b': {...}}}, input_type=dict]
"""
Поскольку эта ошибка возникает без фактического превышения максимальной глубины рекурсии, вы можете перехватить и обработать возникшую ValidationError
не беспокоясь об ограниченной оставшейся глубине рекурсии:
from contextlib import contextmanager
from dataclasses import field
from typing import Iterator, List
from pydantic import BaseModel, ValidationError, field_validator
def is_recursion_validation_error(exc: ValidationError) -> bool:
errors = exc.errors()
return len(errors) == 1 and errors[0]['type'] == 'recursion_loop'
@contextmanager
def suppress_recursion_validation_error() -> Iterator[None]:
try:
yield
except ValidationError as exc:
if not is_recursion_validation_error(exc):
raise exc
class Node(BaseModel):
id: int
children: List['Node'] = field(default_factory=list)
@field_validator('children', mode='wrap')
@classmethod
def drop_cyclic_references(cls, children, h):
try:
return h(children)
except ValidationError as exc:
if not (
is_recursion_validation_error(exc)
and isinstance(children, list)
):
raise exc
value_without_cyclic_refs = []
for child in children:
with suppress_recursion_validation_error():
value_without_cyclic_refs.extend(h([child]))
return h(value_without_cyclic_refs)
# Create data with cyclic references representing the graph 1 -> 2 -> 3 -> 1
node_data = {'id': 1, 'children': [{'id': 2, 'children': [{'id': 3}]}]}
node_data['children'][0]['children'][0]['children'] = [node_data]
print(Node.model_validate(node_data))
#> id=1 children=[Node(id=2, children=[Node(id=3, children=[])])]
Аналогично, если Pydantic обнаруживает рекурсивную ссылку во время сериализации , вместо того, чтобы ждать превышения максимальной глубины рекурсии, немедленно возникает ValueError
:
from pydantic import TypeAdapter
# Create data with cyclic references representing the graph 1 -> 2 -> 3 -> 1
node_data = {'id': 1, 'children': [{'id': 2, 'children': [{'id': 3}]}]}
node_data['children'][0]['children'][0]['children'] = [node_data]
try:
# Try serializing the circular reference as JSON
TypeAdapter(dict).dump_json(node_data)
except ValueError as exc:
print(exc)
"""
Error serializing to JSON: ValueError: Circular reference detected (id repeated)
"""
При желании это также можно сделать:
from dataclasses import field
from typing import Any, List
from pydantic import (
SerializerFunctionWrapHandler,
TypeAdapter,
field_serializer,
)
from pydantic.dataclasses import dataclass
@dataclass
class NodeReference:
id: int
@dataclass
class Node(NodeReference):
children: List['Node'] = field(default_factory=list)
@field_serializer('children', mode='wrap')
def serialize(
self, children: List['Node'], handler: SerializerFunctionWrapHandler
) -> Any:
"""
Serialize a list of nodes, handling circular references by excluding the children.
"""
try:
return handler(children)
except ValueError as exc:
if not str(exc).startswith('Circular reference'):
raise exc
result = []
for node in children:
try:
serialized = handler([node])
except ValueError as exc:
if not str(exc).startswith('Circular reference'):
raise exc
result.append({'id': node.id})
else:
result.append(serialized)
return result
# Create a cyclic graph:
nodes = [Node(id=1), Node(id=2), Node(id=3)]
nodes[0].children.append(nodes[1])
nodes[1].children.append(nodes[2])
nodes[2].children.append(nodes[0])
print(nodes[0])
#> Node(id=1, children=[Node(id=2, children=[Node(id=3, children=[...])])])
# Serialize the cyclic graph:
print(TypeAdapter(Node).dump_python(nodes[0]))
"""
{
'id': 1,
'children': [{'id': 2, 'children': [{'id': 3, 'children': [{'id': 1}]}]}],
}
"""
本文总阅读量次