-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Make @computed_field (optionally) initializable #9131
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
Comments
Seems like a reasonable feature request! PRs welcome! |
hello! is anyone working on this? I'd like to help here, it's a nice feature |
Go for it! |
@sydney-runkle In this new feature, what's expected behavior when initializing a computed property from pydantic import BaseModel, computed_property
class Model(BaseModel):
@computed_property(init_var = True)
@property
def foo(self) -> str:
return "bar"
The existing code issues a warning on serialization class Model(BaseModel):
@computed_property
@property
def foo(self) -> str:
return 1
>>> Model().foo
# returns 1 without any warn
>>> Model().model_dump()
# returns the dictionary and issues a warning `Expected `str` but got `int` - serialized value may not be as expected`
|
I'd like to raise some concerns about this feature request. If I understand correctly, OP wants something like this: from pydantic import BaseModel, computed_field
class Circle(BaseModel):
radius: int
@computed_field
@property
def diameter(self) -> int:
return self.radius * 2
c = Circle(radius=2, diameter=4) This would allow invalid state to be loaded: c = Circle(radius=2, diameter=4)
c.model_dump() # According to the feature request, `diameter` should use the initialized value
c.radius = 4
c.model_dump() # What should happen? Should `diameter` be computed again? Imo, it doesn't really make sense to "initialize" a However, it would make sense to have something similar for |
@Viicos, I agree. My use case is for The goal is in a serialization/deserialization process where the (potentially long) recomputation of I agree that it could lead to "invalid" objects as the As far as I understand, the only difference between a |
Thanks for your concerns - great points. Given that, what do you think makes sense re #9131 (comment)? |
I'll look into it to see if a simple helper/dedicated decorator snippet can be used for this specific use case, so that it can be used downstream. As this would not be specific to Pydantic (but mainly dataclasses and dataclasses-like implementations), I'm wondering if it should live in Pydantic itself. I'll get back to this issue once I figure out a nice implementation :) |
Taking a deeper look at it: having a separate helper isn't really feasible, as the models can be initialized in many ways: class A(BaseModel):
def model_post_init(self, context: Any) -> None:
assert self.model_extra is not None # Guaranteed thanks to extra="allow"
self.__dict__["test"] = self.model_extra.pop("test")
@computed_field
@cached_property
def test(self) -> int:
return 1
model_config = {
"extra": "allow"
}
a = A.model_validate({"test": 2})
print(a.test) Which isn't ideal, as it won't work with other from typing import TypeVar
BaseModelT = TypeVar("BaseModelT", bound=BaseModel)
def init_cached(cls: type[BaseModelT], /) -> type[BaseModelT]:
if not cls.model_config.get("extra") == "allow":
raise ValueError("'init_cached' only works with models with `extra` set to `\"allow\"`.")
cached_computed_fields = [
name
for name in cls.model_computed_fields.keys()
if isinstance(getattr(cls, name), cached_property)
]
original_model_post_init = cls.model_post_init
def model_post_init(self: BaseModelT, context: Any) -> None:
assert self.model_extra is not None # Guaranteed thanks to extra="allow"
for prop_name in cached_computed_fields:
init_val = self.model_extra.pop(prop_name, PydanticUndefined)
if init_val is not PydanticUndefined:
self.__dict__[prop_name] = init_val
original_model_post_init(self, context)
cls.model_post_init = model_post_init
cls.__pydantic_post_init__ = "model_post_init"
cls.model_rebuild(force=True)
@init_cached
class A(BaseModel):
@computed_field
@cached_property
def test(self) -> int:
return 1
model_config = {
"extra": "allow"
}
a = A.model_validate({"test": 2})
print(a.test)
#> 2 So I think the feature request is valid, as we just saw the workaround is a bit fragile and does not cover all uses of However, supporting this in Pydantic (and inevitably pydantic-core) might require some work, and it raises a couple questions (should this only apply to |
That does look like invalid state. But I have a use-case that resembles this form, and that use-case is fully intended:
|
Uh oh!
There was an error while loading. Please reload this page.
Initial Checks
Description
Dumping an object containing a @computed_field will compute this property and include it in the dict/json. Great!
However, using this dict/json to initialize an object will not initialize the field, and the (potentially long) computation must be done again.
Would it be possible to add this feature?
Adding an option like
@computed_field(..., init_var: bool = False,...)
would allow this behaviour without breaking any code.Thanks
Affected Components
.model_dump()
and.model_dump_json()
model_construct()
, pickling, private attributes, ORM modeThe text was updated successfully, but these errors were encountered: