Annotations différées
Les annotations reportées (telles que décrites dans PEP563 ) "fonctionnent simplement".
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'
En interne, Pydantic appellera une méthode similaire à typing.get_type_hints
pour résoudre les annotations.
Même sans utiliser from __future__ import annotations
, dans les cas où le type référencé n'est pas encore défini, un ForwardRef
ou une chaîne peut être utilisé:
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)
Modèles auto-référencés (ou «récursifs»)¶
Les modèles avec des champs auto-référencés sont également pris en charge. Les champs d'auto-référencement seront automatiquement résolus après la création du modèle.
Dans le modèle, vous pouvez faire référence au modèle non encore construit à l'aide d'une chaîne:
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)
Si vous utilisez from __future__ import annotations
, vous pouvez également simplement faire référence au modèle par son nom de type:
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)
Références cycliques¶
Lorsque vous travaillez avec des modèles récursifs auto-référentiels, il est possible que vous rencontriez des références cycliques dans les entrées de validation. Par exemple, cela peut se produire lors de la validation d'instances ORM avec des références arrière à partir d'attributs.
Plutôt que de générer une RecursionError
Python en essayant de valider des données avec des références cycliques, Pydantic est capable de détecter la référence cyclique et de générer une ValidationError
appropriée :
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]
"""
Étant donné que cette erreur est générée sans dépasser réellement la profondeur de récursion maximale, vous pouvez détecter et gérer la ValidationError
générée sans avoir à vous soucier de la profondeur de récursion restante limitée:
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=[])])]
De même, si Pydantic rencontre une référence récursive lors de la sérialisation , plutôt que d'attendre que la profondeur de récursion maximale soit dépassée, une ValueError
est immédiatement levée:
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)
"""
Cela peut également être géré si vous le souhaitez:
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}]}]}],
}
"""
本文总阅读量次