diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index b98f21dcbe9220..86f11b009f84b5 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -1052,6 +1052,9 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen, # Remember all of the fields on our class (including bases). This # also marks this class as being a dataclass. setattr(cls, _FIELDS, fields) + # Store field names. Excludes pseudo-fields. + cls.__dataclass_field_names__ = tuple(f.name for f in fields.values() + if f._field_type is _FIELD) # Was this class defined with an explicit __hash__? Note that if # __eq__ is defined in this class, then python will automatically @@ -1196,13 +1199,13 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen, # the code instead of iterating over fields. But that can be a project for # another day, if performance becomes an issue. def _dataclass_getstate(self): - return [getattr(self, f.name) for f in fields(self)] + return [getattr(self, name) for name in self.__dataclass_field_names__] def _dataclass_setstate(self, state): - for field, value in zip(fields(self), state): + for field_name, value in zip(self.__dataclass_field_names__, state): # use setattr because dataclass may be frozen - object.__setattr__(self, field.name, value) + object.__setattr__(self, field_name, value) def _get_slots(cls): @@ -1285,7 +1288,7 @@ def _add_slots(cls, is_frozen, weakref_slot, defined_fields): # Create a new dict for our new class. cls_dict = dict(cls.__dict__) - field_names = tuple(f.name for f in fields(cls)) + field_names = cls.__dataclass_field_names__ # Make sure slots don't overlap with those in base classes. inherited_slots = set( itertools.chain.from_iterable(map(_get_slots, cls.__mro__[1:-1])) @@ -1377,8 +1380,6 @@ def fields(class_or_instance): Accepts a dataclass or an instance of one. Tuple elements are of type Field. """ - - # Might it be worth caching this, per class? try: fields = getattr(class_or_instance, _FIELDS) except AttributeError: @@ -1433,13 +1434,13 @@ def _asdict_inner(obj, dict_factory): # dataclass instance: fast path for the common case if dict_factory is dict: return { - f.name: _asdict_inner(getattr(obj, f.name), dict) - for f in fields(obj) + name: _asdict_inner(getattr(obj, name), dict) + for name in obj_type.__dataclass_field_names__ } else: return dict_factory([ - (f.name, _asdict_inner(getattr(obj, f.name), dict_factory)) - for f in fields(obj) + (name, _asdict_inner(getattr(obj, name), dict_factory)) + for name in obj_type.__dataclass_field_names__ ]) # handle the builtin types first for speed; subclasses handled below elif obj_type is list: @@ -1522,8 +1523,8 @@ def _astuple_inner(obj, tuple_factory): return obj elif _is_dataclass_instance(obj): return tuple_factory([ - _astuple_inner(getattr(obj, f.name), tuple_factory) - for f in fields(obj) + _astuple_inner(getattr(obj, name), tuple_factory) + for name in obj.__dataclass_field_names__ ]) elif isinstance(obj, tuple) and hasattr(obj, '_fields'): # obj is a namedtuple. Recurse into it, but the returned diff --git a/Misc/NEWS.d/next/Library/2025-08-29-17-58-36.gh-issue-138232.-W4iaS.rst b/Misc/NEWS.d/next/Library/2025-08-29-17-58-36.gh-issue-138232.-W4iaS.rst new file mode 100644 index 00000000000000..ab63a205a4be7b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-08-29-17-58-36.gh-issue-138232.-W4iaS.rst @@ -0,0 +1 @@ +Improve performance of :func:`dataclasses.asdict` up to 40% by caching the field names on dataclass classes.