-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
construct()
for recursive models
#1168
Comments
no, I'm afraid not. For performance reasons If you want this behaviour you'll need to implement it yourself, either as a standalone function or as a model class method. |
I am trying to implement something but totally getting stuck with generic types
how would I find out if a a field type is a generic (in this case List)? |
This feature would be very useful! |
I agree, could we get support for construct() on recursive models? |
Hi guys from pydantic import BaseModel as PydanticBaseModel
class BaseModel(PydanticBaseModel):
@classmethod
def construct(cls, _fields_set = None, *, __recursive__ = False, **values):
if not __recursive__:
return super().construct(_fields_set, **values)
m = cls.__new__(cls)
fields_values = {}
for name, field in cls.__fields__.items():
if name in values:
if issubclass(field.type_, BaseModel):
fields_values[name] = field.outer_type_.construct(**values[name], __recursive__=True)
else:
fields_values[name] = values[name]
elif not field.required:
fields_values[name] = field.get_default()
object.__setattr__(m, '__dict__', fields_values)
if _fields_set is None:
_fields_set = set(values.keys())
object.__setattr__(m, '__fields_set__', _fields_set)
m._init_private_attributes()
return m
class X(BaseModel):
a: int
class Id(BaseModel):
id: int
x: X
class User(BaseModel):
id: Id
name = "test"
x = User(**{"id": {"id":2, "x": {"a": 1}}, "name": "not_test"})
y = User.construct(**x.dict())
z = User.construct(**x.dict(), __recursive__=True)
print(repr(x))
# User(id=Id(id=2, x=X(a=1)), name='not_test')
print(repr(y))
# User(name='not_test', id={'id': 2, 'x': {'a': 1}})
print(repr(z))
# User(id=Id(id=2, x=X(a=1)), name='not_test') Hope it helps |
Hi @PrettyWood , thank you for the prompt answer! Your code is amazing and does exactly what I was looking for minus a small detail, which is now aliases are not working anymore. To test it I used the following code, given your BaseClass:
As the use-case would be for production, where you know for certain the test passed, this feature is very useful, however, without aliases it breaks the current workflow. Is there any chance to include such checks in your solution? |
Ok I came up with this modification which works on this small use-case but I am not sure if its general enough? WDYT?
|
By default, if field.alias in values:
if issubclass(field.type_, BaseModel):
fields_values[name] = field.outer_type_.construct(**values[field.alias], __recursive__=True)
else:
fields_values[name] = values[field.alias] |
but what about the speed concern? To my understanding the construct() method is mostly advantageous for not paying the prices of the validation operations. How come that the recursive construct is slower than the recursive validator? |
I don't understand how it can take longer than plain from pydantic import BaseModel as PydanticBaseModel
class BaseModel(PydanticBaseModel):
@classmethod
def construct(cls, _fields_set = None, *, __recursive__ = False, **values):
if not __recursive__:
return super().construct(_fields_set, **values)
m = cls.__new__(cls)
fields_values = {}
for name, field in cls.__fields__.items():
if name in values:
if issubclass(field.type_, BaseModel):
fields_values[name] = field.outer_type_.construct(**values[name], __recursive__=True)
else:
fields_values[name] = values[name]
elif not field.required:
fields_values[name] = field.get_default()
object.__setattr__(m, '__dict__', fields_values)
if _fields_set is None:
_fields_set = set(values.keys())
object.__setattr__(m, '__fields_set__', _fields_set)
m._init_private_attributes()
return m
class X(BaseModel):
a: int
class Id(BaseModel):
id: int
x: X
class User(BaseModel):
id: Id
name = "test"
values = {"id": {"id":2, "x": {"a": 1}}, "name": "not_test"}
def f():
# User(**values)
User.construct(**values, __recursive__=False)
if __name__ == '__main__':
import timeit
print(timeit.timeit("f()", number=1_000_000, setup="from __main__ import f"))
# __init__: 16.027352597
# recursive construct: 7.86188907
# non recursive construct: 2.8425855930000004 @Renthal can you please share your code and test results please? |
Unfortunately with shallow/small models the effect is minimal, I upgraded the example with a more complex object to showcase the issue at hand.
Note that the BaseModel is not the exact same as yours as mine also needs to handle lists of models. The idea is to have a construct version which can handle models as complex as those handled by the regular init but faster for production/trusted data. |
Try to use the from pydantic import BaseModel as PydanticBaseModel, Field
from typing import List
class BaseModel(PydanticBaseModel):
@classmethod
def construct(cls, _fields_set=None, **values):
m = cls.__new__(cls)
fields_values = {}
for name, field in cls.__fields__.items():
key = field.alias # this is the current behaviour of `__init__` by default
if key:
if issubclass(field.type_, BaseModel):
if field.shape == 2: # the field is a `list`. You could check other shapes to handle `tuple`, ...
fields_values[name] = [
field.type_.construct(**e)
for e in values[key]
]
else:
fields_values[name] = field.outer_type_.construct(**values[key])
else:
if values[key] is None and not field.required:
fields_values[name] = field.get_default()
else:
fields_values[name] = values[key]
elif not field.required:
fields_values[name] = field.get_default()
object.__setattr__(m, '__dict__', fields_values)
if _fields_set is None:
_fields_set = set(values.keys())
object.__setattr__(m, '__fields_set__', _fields_set)
m._init_private_attributes()
return m
class F(BaseModel):
f: List[str]
class E(BaseModel):
e: List[str]
class D(BaseModel):
d_one: List[E]
d_two: List[F]
class C(BaseModel):
c: List[D]
class B(BaseModel):
b: List[int]
class A(BaseModel):
one: List[B] = Field(
default=None,
alias='aliased_one',
)
two: List[C]
d = {
"aliased_one": [{"b": [i]} for i in range(500)],
"two": [{
"c": [
{
"d_one": [{'e': ['' for _ in range(20)]} for _ in range(50)],
"d_two": [{'f': ['' for _ in range(20)]} for _ in range(50)]
} for _ in range(5)]
} for _ in range(5)],
}
def f(values):
return A(**values)
def g(values):
return A.construct(**values)
if __name__ == '__main__':
import timeit
from functools import partial
assert A(**d) == A.construct(**d) # just to be sure!
print(timeit.timeit(partial(f, values=d), number=100))
# 11.651661356
print(timeit.timeit(partial(g, values=d), number=100))
# 0.6944440649999999 Hope it helps! |
It does thank you! Finally, since in my use-case most of the fields are in fact BaseModels I found that this solutions works better for me. Unfortunately the final version is only 20-30% faster than the validating one whereas with the small demo above it seems to be a lot faster. I think this is due to the nature of the model one is building class BaseModel(PydanticBaseModel):
@classmethod
def construct(cls, _fields_set=None, **values):
m = cls.__new__(cls)
fields_values = {}
for name, field in cls.__fields__.items():
key = field.alias
if key in values: # this check is necessary or Optional fields will crash
try:
#if issubclass(field.type_, BaseModel): # this is cleaner but slower
if field.shape == 2:
fields_values[name] = [
field.type_.construct(**e)
for e in values[key]
]
else:
fields_values[name] = field.outer_type_.construct(**values[key])
except AttributeError:
if values[key] is None and not field.required:
fields_values[name] = field.get_default()
else:
fields_values[name] = values[key]
elif not field.required:
fields_values[name] = field.get_default()
object.__setattr__(m, '__dict__', fields_values)
if _fields_set is None:
_fields_set = set(values.keys())
object.__setattr__(m, '__fields_set__', _fields_set)
m._init_private_attributes()
return m |
Hey guys, thank you for your suggested solutions, very helpful! I ran into performance issues when using FastAPI with pydantic because my data was being validated multiple times before being returned to the client. Very similar to the problem stated here tiangolo/fastapi#1359 (comment) Reading about the In my opinion I ended up using the suggested solutions in this issue, but with a few modifications to make it work for me.
|
@samuelcolvin this issue seems to be a common concern across Pydantic / FastAPI implementations operating at scale. Would it make sense to offer both construct options (high-performance non-recursive, slightly less performant recursive) directly in Pydantic? |
When is this going to be added? It's been two years now. |
Would love to have recursive model construction! |
Been tracking this for so long, decided to try a different approach myself. The following solution tries to achieve
class Model(pydantic.BaseModel):
""" Allow any attr to be missing """
def __init_subclass__(cls):
""" Set all defaults to None """
for key in cls.__annotations__:
if not hasattr(cls, key):
setattr(cls, key, None)
else:
attr = getattr(cls, key)
if isinstance(attr, pydantic.fields.FieldInfo) and attr.is_required():
attr.default = None
def __init__(self, **kwargs):
""" Delete all None attributes (Mostly to clean up repr) """
pydantic.BaseModel.__init__(self, **kwargs)
for key in self.model_fields:
attr = getattr(self, key, None)
if attr is None:
delattr(self, key) Seems to work rather well, thought I'd share. |
Question
Output of
python -c "import pydantic.utils; print(pydantic.utils.version_info())"
:I want to use the
construct()
method to build a nested model faster, with trusted data. But it's not working :Is it possible to make it work ?
The text was updated successfully, but these errors were encountered: