Skip to content

Commit a819155

Browse files
davfsahynek
andauthored
Speedup _setattr usage and fix slight performance regressions (#991)
* Speedup `_setattr` usage and fix performance regressions * Add changelog file Co-authored-by: Hynek Schlawack <hs@ox.cx>
1 parent 983c2c4 commit a819155

4 files changed

Lines changed: 23 additions & 11 deletions

File tree

changelog.d/991.change.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix slight performance regression in classes with custom ``__setattr__`` and speedup even more.

docs/how-does-it-work.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,9 @@ This is (still) slower than a plain assignment:
9494
-s "import attr; C = attr.make_class('C', ['x', 'y', 'z'], slots=True, frozen=True)" \
9595
"C(1, 2, 3)"
9696
.........................................
97-
Mean +- std dev: 450 ns +- 26 ns
97+
Mean +- std dev: 425 ns +- 16 ns
9898
99-
So on a laptop computer the difference is about 230 nanoseconds (1 second is 1,000,000,000 nanoseconds).
99+
So on a laptop computer the difference is about 200 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: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -915,7 +915,7 @@ def slots_setstate(self, state):
915915
"""
916916
Automatically created by attrs.
917917
"""
918-
__bound_setattr = _obj_setattr.__get__(self, Attribute)
918+
__bound_setattr = _obj_setattr.__get__(self)
919919
for name, value in zip(state_attr_names, state):
920920
__bound_setattr(name, value)
921921

@@ -2007,6 +2007,7 @@ def _make_init(
20072007
cache_hash,
20082008
base_attr_map,
20092009
is_exc,
2010+
needs_cached_setattr,
20102011
has_cls_on_setattr,
20112012
attrs_init,
20122013
)
@@ -2019,7 +2020,7 @@ def _make_init(
20192020
if needs_cached_setattr:
20202021
# Save the lookup overhead in __init__ if we need to circumvent
20212022
# setattr hooks.
2022-
globs["_setattr"] = _obj_setattr
2023+
globs["_cached_setattr_get"] = _obj_setattr.__get__
20232024

20242025
init = _make_method(
20252026
"__attrs_init__" if attrs_init else "__init__",
@@ -2036,15 +2037,15 @@ def _setattr(attr_name, value_var, has_on_setattr):
20362037
"""
20372038
Use the cached object.setattr to set *attr_name* to *value_var*.
20382039
"""
2039-
return "_setattr(self, '%s', %s)" % (attr_name, value_var)
2040+
return "_setattr('%s', %s)" % (attr_name, value_var)
20402041

20412042

20422043
def _setattr_with_converter(attr_name, value_var, has_on_setattr):
20432044
"""
20442045
Use the cached object.setattr to set *attr_name* to *value_var*, but run
20452046
its converter first.
20462047
"""
2047-
return "_setattr(self, '%s', %s(%s))" % (
2048+
return "_setattr('%s', %s(%s))" % (
20482049
attr_name,
20492050
_init_converter_pat % (attr_name,),
20502051
value_var,
@@ -2086,6 +2087,7 @@ def _attrs_to_init_script(
20862087
cache_hash,
20872088
base_attr_map,
20882089
is_exc,
2090+
needs_cached_setattr,
20892091
has_cls_on_setattr,
20902092
attrs_init,
20912093
):
@@ -2101,6 +2103,14 @@ def _attrs_to_init_script(
21012103
if pre_init:
21022104
lines.append("self.__attrs_pre_init__()")
21032105

2106+
if needs_cached_setattr:
2107+
lines.append(
2108+
# Circumvent the __setattr__ descriptor to save one lookup per
2109+
# assignment.
2110+
# Note _setattr will be used again below if cache_hash is True
2111+
"_setattr = _cached_setattr_get(self)"
2112+
)
2113+
21042114
if frozen is True:
21052115
if slots is True:
21062116
fmt_setter = _setattr
@@ -2315,7 +2325,7 @@ def fmt_setter_with_converter(
23152325
if frozen:
23162326
if slots:
23172327
# if frozen and slots, then _setattr defined above
2318-
init_hash_cache = "_setattr(self, '%s', %s)"
2328+
init_hash_cache = "_setattr('%s', %s)"
23192329
else:
23202330
# if frozen and not slots, then _inst_dict defined above
23212331
init_hash_cache = "_inst_dict['%s'] = %s"
@@ -2428,7 +2438,7 @@ def __init__(
24282438
)
24292439

24302440
# Cache this descriptor here to speed things up later.
2431-
bound_setattr = _obj_setattr.__get__(self, Attribute)
2441+
bound_setattr = _obj_setattr.__get__(self)
24322442

24332443
# Despite the big red warning, people *do* instantiate `Attribute`
24342444
# themselves.
@@ -2525,7 +2535,7 @@ def __setstate__(self, state):
25252535
self._setattrs(zip(self.__slots__, state))
25262536

25272537
def _setattrs(self, name_values_pairs):
2528-
bound_setattr = _obj_setattr.__get__(self, Attribute)
2538+
bound_setattr = _obj_setattr.__get__(self)
25292539
for name, value in name_values_pairs:
25302540
if name != "metadata":
25312541
bound_setattr(name, value)

tests/test_functional.py

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

746746
src = inspect.getsource(D.__init__)
747747

748-
assert "_setattr(self, 'x', x)" in src
749-
assert "_setattr(self, 'y', y)" in src
748+
assert "_setattr = _cached_setattr_get(self)" in src
749+
assert "_setattr('x', x)" in src
750+
assert "_setattr('y', y)" in src
750751
assert object.__setattr__ != D.__setattr__

0 commit comments

Comments
 (0)