Skip to content

Commit 3df7bbe

Browse files
authored
Speedup uses of object.__setattr__ (#898)
* Speedup uses of `object.__setattr__` * Add changelog fragment
1 parent c162c78 commit 3df7bbe

File tree

4 files changed

+15
-25
lines changed

4 files changed

+15
-25
lines changed

changelog.d/898.change.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Speedup instantiation of frozen slotted classes.

docs/how-does-it-work.rst

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,16 +87,16 @@ This is (still) slower than a plain assignment:
8787
$ pyperf timeit --rigorous \
8888
-s "import attr; C = attr.make_class('C', ['x', 'y', 'z'], slots=True)" \
8989
"C(1, 2, 3)"
90-
........................................
91-
Median +- std dev: 378 ns +- 12 ns
90+
.........................................
91+
Mean +- std dev: 228 ns +- 18 ns
9292
9393
$ pyperf timeit --rigorous \
9494
-s "import attr; C = attr.make_class('C', ['x', 'y', 'z'], slots=True, frozen=True)" \
9595
"C(1, 2, 3)"
96-
........................................
97-
Median +- std dev: 676 ns +- 16 ns
96+
.........................................
97+
Mean +- std dev: 450 ns +- 26 ns
9898
99-
So on a laptop computer the difference is about 300 nanoseconds (1 second is 1,000,000,000 nanoseconds).
99+
So on a laptop computer the difference is about 230 nanoseconds (1 second is 1,000,000,000 nanoseconds).
100100
It's certainly something you'll feel in a hot loop but shouldn't matter in normal code.
101101
Pick what's more important to you.
102102

src/attr/_make.py

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -807,7 +807,7 @@ def _patch_original_class(self):
807807
cls.__attrs_own_setattr__ = False
808808

809809
if not self._has_custom_setattr:
810-
cls.__setattr__ = object.__setattr__
810+
cls.__setattr__ = _obj_setattr
811811

812812
return cls
813813

@@ -835,7 +835,7 @@ def _create_slots_class(self):
835835
if not self._has_custom_setattr:
836836
for base_cls in self._cls.__bases__:
837837
if base_cls.__dict__.get("__attrs_own_setattr__", False):
838-
cd["__setattr__"] = object.__setattr__
838+
cd["__setattr__"] = _obj_setattr
839839
break
840840

841841
# Traverse the MRO to collect existing slots
@@ -2159,7 +2159,6 @@ def _make_init(
21592159
cache_hash,
21602160
base_attr_map,
21612161
is_exc,
2162-
needs_cached_setattr,
21632162
has_cls_on_setattr,
21642163
attrs_init,
21652164
)
@@ -2172,7 +2171,7 @@ def _make_init(
21722171
if needs_cached_setattr:
21732172
# Save the lookup overhead in __init__ if we need to circumvent
21742173
# setattr hooks.
2175-
globs["_cached_setattr"] = _obj_setattr
2174+
globs["_setattr"] = _obj_setattr
21762175

21772176
init = _make_method(
21782177
"__attrs_init__" if attrs_init else "__init__",
@@ -2189,15 +2188,15 @@ def _setattr(attr_name, value_var, has_on_setattr):
21892188
"""
21902189
Use the cached object.setattr to set *attr_name* to *value_var*.
21912190
"""
2192-
return "_setattr('%s', %s)" % (attr_name, value_var)
2191+
return "_setattr(self, '%s', %s)" % (attr_name, value_var)
21932192

21942193

21952194
def _setattr_with_converter(attr_name, value_var, has_on_setattr):
21962195
"""
21972196
Use the cached object.setattr to set *attr_name* to *value_var*, but run
21982197
its converter first.
21992198
"""
2200-
return "_setattr('%s', %s(%s))" % (
2199+
return "_setattr(self, '%s', %s(%s))" % (
22012200
attr_name,
22022201
_init_converter_pat % (attr_name,),
22032202
value_var,
@@ -2296,7 +2295,6 @@ def _attrs_to_init_script(
22962295
cache_hash,
22972296
base_attr_map,
22982297
is_exc,
2299-
needs_cached_setattr,
23002298
has_cls_on_setattr,
23012299
attrs_init,
23022300
):
@@ -2312,14 +2310,6 @@ def _attrs_to_init_script(
23122310
if pre_init:
23132311
lines.append("self.__attrs_pre_init__()")
23142312

2315-
if needs_cached_setattr:
2316-
lines.append(
2317-
# Circumvent the __setattr__ descriptor to save one lookup per
2318-
# assignment.
2319-
# Note _setattr will be used again below if cache_hash is True
2320-
"_setattr = _cached_setattr.__get__(self, self.__class__)"
2321-
)
2322-
23232313
if frozen is True:
23242314
if slots is True:
23252315
fmt_setter = _setattr
@@ -2535,7 +2525,7 @@ def fmt_setter_with_converter(
25352525
if post_init:
25362526
lines.append("self.__attrs_post_init__()")
25372527

2538-
# because this is set only after __attrs_post_init is called, a crash
2528+
# because this is set only after __attrs_post_init__ is called, a crash
25392529
# will result if post-init tries to access the hash code. This seemed
25402530
# preferable to setting this beforehand, in which case alteration to
25412531
# field values during post-init combined with post-init accessing the
@@ -2544,7 +2534,7 @@ def fmt_setter_with_converter(
25442534
if frozen:
25452535
if slots:
25462536
# if frozen and slots, then _setattr defined above
2547-
init_hash_cache = "_setattr('%s', %s)"
2537+
init_hash_cache = "_setattr(self, '%s', %s)"
25482538
else:
25492539
# if frozen and not slots, then _inst_dict defined above
25502540
init_hash_cache = "_inst_dict['%s'] = %s"

tests/test_functional.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -784,7 +784,6 @@ class D(C):
784784

785785
src = inspect.getsource(D.__init__)
786786

787-
assert "_setattr = _cached_setattr" in src
788-
assert "_setattr('x', x)" in src
789-
assert "_setattr('y', y)" in src
787+
assert "_setattr(self, 'x', x)" in src
788+
assert "_setattr(self, 'y', y)" in src
790789
assert object.__setattr__ != D.__setattr__

0 commit comments

Comments
 (0)