81
81
_object_setattr = _model_construction .object_setattr
82
82
83
83
84
+ def _model_field_setattr_handler (model : BaseModel , name : str , val : Any ) -> None :
85
+ model .__dict__ [name ] = val
86
+ model .__pydantic_fields_set__ .add (name )
87
+
88
+
89
+ _SIMPLE_SETATTR_HANDLERS : Mapping [str , Callable [[BaseModel , str , Any ], None ]] = {
90
+ 'model_field' : _model_field_setattr_handler ,
91
+ 'validate_assignment' : lambda model , name , val : model .__pydantic_validator__ .validate_assignment (model , name , val ), # pyright: ignore[reportAssignmentType]
92
+ 'private' : lambda model , name , val : model .__pydantic_private__ .__setitem__ (name , val ), # pyright: ignore[reportOptionalMemberAccess]
93
+ 'cached_property' : lambda model , name , val : model .__dict__ .__setitem__ (name , val ),
94
+ 'extra_known' : lambda model , name , val : _object_setattr (model , name , val ),
95
+ }
96
+
97
+
84
98
class BaseModel (metaclass = _model_construction .ModelMetaclass ):
85
99
"""Usage docs: https://docs.pydantic.dev/2.10/concepts/models/
86
100
@@ -169,6 +183,9 @@ class BaseModel(metaclass=_model_construction.ModelMetaclass):
169
183
This replaces `Model.__fields__` from Pydantic V1.
170
184
"""
171
185
186
+ __pydantic_setattr_handlers__ : ClassVar [Dict [str , Callable [[BaseModel , str , Any ], None ]]] # noqa: UP006
187
+ """`__setattr__` handlers. Memoizing the handlers leads to a dramatic performance improvement in `__setattr__`"""
188
+
172
189
__pydantic_computed_fields__ : ClassVar [Dict [str , ComputedFieldInfo ]] # noqa: UP006
173
190
"""A dictionary of computed field names and their corresponding [`ComputedFieldInfo`][pydantic.fields.ComputedFieldInfo] objects."""
174
191
@@ -890,53 +907,63 @@ def __getattr__(self, item: str) -> Any:
890
907
raise AttributeError (f'{ type (self ).__name__ !r} object has no attribute { item !r} ' )
891
908
892
909
def __setattr__ (self , name : str , value : Any ) -> None :
893
- if name in self .__class_vars__ :
910
+ if (setattr_handler := self .__pydantic_setattr_handlers__ .get (name )) is not None :
911
+ setattr_handler (self , name , value )
912
+ # if None is returned from _setattr_handler, the attribute was set directly
913
+ elif (setattr_handler := self ._setattr_handler (name , value )) is not None :
914
+ setattr_handler (self , name , value ) # call here to not memo on possibly unknown fields
915
+ self .__pydantic_setattr_handlers__ [name ] = setattr_handler # memoize the handler for faster access
916
+
917
+ def _setattr_handler (self , name : str , value : Any ) -> Callable [[BaseModel , str , Any ], None ] | None :
918
+ """Get a handler for setting an attribute on the model instance.
919
+
920
+ Returns:
921
+ A handler for setting an attribute on the model instance. Used for memoization of the handler.
922
+ Memoizing the handlers leads to a dramatic performance improvement in `__setattr__`
923
+ Returns `None` when memoization is not safe, then the attribute is set directly.
924
+ """
925
+ cls = self .__class__
926
+ if name in cls .__class_vars__ :
894
927
raise AttributeError (
895
- f'{ name !r} is a ClassVar of `{ self . __class__ .__name__ } ` and cannot be set on an instance. '
896
- f'If you want to set a value on the class, use `{ self . __class__ .__name__ } .{ name } = value`.'
928
+ f'{ name !r} is a ClassVar of `{ cls .__name__ } ` and cannot be set on an instance. '
929
+ f'If you want to set a value on the class, use `{ cls .__name__ } .{ name } = value`.'
897
930
)
898
931
elif not _fields .is_valid_field_name (name ):
899
- if self .__pydantic_private__ is None or name not in self .__private_attributes__ :
900
- _object_setattr (self , name , value )
901
- else :
902
- attribute = self .__private_attributes__ [name ]
932
+ if (attribute := cls .__private_attributes__ .get (name )) is not None :
903
933
if hasattr (attribute , '__set__' ):
904
- attribute .__set__ (self , value ) # type: ignore
934
+ return lambda model , _name , val : attribute .__set__ (model , val )
905
935
else :
906
- self .__pydantic_private__ [name ] = value
907
- return
936
+ return _SIMPLE_SETATTR_HANDLERS ['private' ]
937
+ else :
938
+ _object_setattr (self , name , value )
939
+ return None # Can not return memoized handler with possibly freeform attr names
908
940
909
- self ._check_frozen (name , value )
941
+ cls ._check_frozen (name , value )
910
942
911
- attr = getattr (self . __class__ , name , None )
943
+ attr = getattr (cls , name , None )
912
944
# NOTE: We currently special case properties and `cached_property`, but we might need
913
945
# to generalize this to all data/non-data descriptors at some point. For non-data descriptors
914
946
# (such as `cached_property`), it isn't obvious though. `cached_property` caches the value
915
947
# to the instance's `__dict__`, but other non-data descriptors might do things differently.
916
948
if isinstance (attr , property ):
917
- attr .__set__ (self , value )
949
+ return lambda model , _name , val : attr .__set__ (model , val )
918
950
elif isinstance (attr , cached_property ):
919
- self .__dict__ [name ] = value
920
- elif self .model_config .get ('validate_assignment' , None ):
921
- self .__pydantic_validator__ .validate_assignment (self , name , value )
922
- elif self .model_config .get ('extra' ) != 'allow' and name not in self .__pydantic_fields__ :
923
- # TODO - matching error
924
- raise ValueError (f'"{ self .__class__ .__name__ } " object has no field "{ name } "' )
925
- elif self .model_config .get ('extra' ) == 'allow' and name not in self .__pydantic_fields__ :
926
- if self .model_extra and name in self .model_extra :
927
- self .__pydantic_extra__ [name ] = value # type: ignore
951
+ return _SIMPLE_SETATTR_HANDLERS ['cached_property' ]
952
+ elif cls .model_config .get ('validate_assignment' ):
953
+ return _SIMPLE_SETATTR_HANDLERS ['validate_assignment' ]
954
+ elif name not in cls .__pydantic_fields__ :
955
+ if cls .model_config .get ('extra' ) != 'allow' :
956
+ # TODO - matching error
957
+ raise ValueError (f'"{ cls .__name__ } " object has no field "{ name } "' )
958
+ elif attr is None :
959
+ # attribute does not exist, so put it in extra
960
+ self .__pydantic_extra__ [name ] = value
961
+ return None # Can not return memoized handler with possibly freeform attr names
928
962
else :
929
- try :
930
- getattr (self , name )
931
- except AttributeError :
932
- # attribute does not already exist on instance, so put it in extra
933
- self .__pydantic_extra__ [name ] = value # type: ignore
934
- else :
935
- # attribute _does_ already exist on instance, and was not in extra, so update it
936
- _object_setattr (self , name , value )
963
+ # attribute _does_ exist, and was not in extra, so update it
964
+ return _SIMPLE_SETATTR_HANDLERS ['extra_known' ]
937
965
else :
938
- self .__dict__ [name ] = value
939
- self .__pydantic_fields_set__ .add (name )
966
+ return _SIMPLE_SETATTR_HANDLERS ['model_field' ]
940
967
941
968
def __delattr__ (self , item : str ) -> Any :
942
969
if item in self .__private_attributes__ :
@@ -964,10 +991,11 @@ def __delattr__(self, item: str) -> Any:
964
991
except AttributeError :
965
992
raise AttributeError (f'{ type (self ).__name__ !r} object has no attribute { item !r} ' )
966
993
967
- def _check_frozen (self , name : str , value : Any ) -> None :
968
- if self .model_config .get ('frozen' , None ):
994
+ @classmethod
995
+ def _check_frozen (cls , name : str , value : Any ) -> None :
996
+ if cls .model_config .get ('frozen' , None ):
969
997
typ = 'frozen_instance'
970
- elif getattr (self .__pydantic_fields__ .get (name ), 'frozen' , False ):
998
+ elif getattr (cls .__pydantic_fields__ .get (name ), 'frozen' , False ):
971
999
typ = 'frozen_field'
972
1000
else :
973
1001
return
@@ -976,7 +1004,7 @@ def _check_frozen(self, name: str, value: Any) -> None:
976
1004
'loc' : (name ,),
977
1005
'input' : value ,
978
1006
}
979
- raise pydantic_core .ValidationError .from_exception_data (self . __class__ .__name__ , [error ])
1007
+ raise pydantic_core .ValidationError .from_exception_data (cls .__name__ , [error ])
980
1008
981
1009
def __getstate__ (self ) -> dict [Any , Any ]:
982
1010
private = self .__pydantic_private__
0 commit comments