Skip to content

Commit e2293d9

Browse files
committed
gh-144764: Made dataclasses construct automatic docstrings lazily
Move the default docstring construction to occur on-access through a descriptor. Verified with [tprof](https://github.com/adamchainz/tprof) and this script that generates 10k dataclasses: ```py from dataclasses import dataclass for i in range(10_000): @DataClass class Example: field1: int field2: str field3: float ``` **Before:** ``` $ tprof -t dataclasses._process_class example.py 🎯 tprof results: function calls total mean ± σ min … max dataclasses._process_class() 10000 5s 485μs ± 120μs 458μs … 6ms ``` After: ``` $ PYTHONPATH=Lib/ uvx tprof -t dataclasses._process_class example.py 🎯 tprof results: function calls total mean ± σ min … max dataclasses._process_class() 10000 3s 275μs ± 131μs 245μs … 6ms ``` The mean time spent in `_process_class()` has dropped from 485μs to 275μs, a ~42% time saving (admittedly skewed due to the small size of the dataclass).
1 parent 66da7bf commit e2293d9

File tree

1 file changed

+18
-15
lines changed

1 file changed

+18
-15
lines changed

Lib/dataclasses.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1209,23 +1209,11 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen,
12091209
if hash_action:
12101210
cls.__hash__ = hash_action(cls, field_list, func_builder)
12111211

1212-
# Generate the methods and add them to the class. This needs to be done
1213-
# before the __doc__ logic below, since inspect will look at the __init__
1214-
# signature.
1212+
# Generate the methods and add them to the class.
12151213
func_builder.add_fns_to_class(cls)
12161214

1217-
if not getattr(cls, '__doc__'):
1218-
# Create a class doc-string.
1219-
try:
1220-
# In some cases fetching a signature is not possible.
1221-
# But, we surely should not fail in this case.
1222-
text_sig = str(inspect.signature(
1223-
cls,
1224-
annotation_format=annotationlib.Format.FORWARDREF,
1225-
)).replace(' -> None', '')
1226-
except (TypeError, ValueError):
1227-
text_sig = ''
1228-
cls.__doc__ = (cls.__name__ + text_sig)
1215+
if not cls.__doc__:
1216+
cls.__doc__ = _DocDescriptor()
12291217

12301218
if match_args:
12311219
# I could probably compute this once.
@@ -1243,6 +1231,21 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen,
12431231
return cls
12441232

12451233

1234+
class _DocDescriptor:
1235+
def __get__(self, obj, owner):
1236+
# Create a class doc-string.
1237+
try:
1238+
# In some cases fetching a signature is not possible.
1239+
# But, we surely should not fail in this case.
1240+
text_sig = str(inspect.signature(
1241+
owner,
1242+
annotation_format=annotationlib.Format.FORWARDREF,
1243+
)).replace(' -> None', '')
1244+
except (TypeError, ValueError):
1245+
text_sig = ''
1246+
owner.__doc__ = (owner.__name__ + text_sig)
1247+
return owner.__doc__
1248+
12461249
# _dataclass_getstate and _dataclass_setstate are needed for pickling frozen
12471250
# classes with slots. These could be slightly more performant if we generated
12481251
# the code instead of iterating over fields. But that can be a project for

0 commit comments

Comments
 (0)