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>
This commit is contained in:
parent
983c2c4293
commit
a8191556c0
|
@ -0,0 +1 @@
|
||||||
|
Fix slight performance regression in classes with custom ``__setattr__`` and speedup even more.
|
|
@ -94,9 +94,9 @@ This is (still) slower than a plain assignment:
|
||||||
-s "import attr; C = attr.make_class('C', ['x', 'y', 'z'], slots=True, frozen=True)" \
|
-s "import attr; C = attr.make_class('C', ['x', 'y', 'z'], slots=True, frozen=True)" \
|
||||||
"C(1, 2, 3)"
|
"C(1, 2, 3)"
|
||||||
.........................................
|
.........................................
|
||||||
Mean +- std dev: 450 ns +- 26 ns
|
Mean +- std dev: 425 ns +- 16 ns
|
||||||
|
|
||||||
So on a laptop computer the difference is about 230 nanoseconds (1 second is 1,000,000,000 nanoseconds).
|
So on a laptop computer the difference is about 200 nanoseconds (1 second is 1,000,000,000 nanoseconds).
|
||||||
It's certainly something you'll feel in a hot loop but shouldn't matter in normal code.
|
It's certainly something you'll feel in a hot loop but shouldn't matter in normal code.
|
||||||
Pick what's more important to you.
|
Pick what's more important to you.
|
||||||
|
|
||||||
|
|
|
@ -915,7 +915,7 @@ class _ClassBuilder:
|
||||||
"""
|
"""
|
||||||
Automatically created by attrs.
|
Automatically created by attrs.
|
||||||
"""
|
"""
|
||||||
__bound_setattr = _obj_setattr.__get__(self, Attribute)
|
__bound_setattr = _obj_setattr.__get__(self)
|
||||||
for name, value in zip(state_attr_names, state):
|
for name, value in zip(state_attr_names, state):
|
||||||
__bound_setattr(name, value)
|
__bound_setattr(name, value)
|
||||||
|
|
||||||
|
@ -2007,6 +2007,7 @@ def _make_init(
|
||||||
cache_hash,
|
cache_hash,
|
||||||
base_attr_map,
|
base_attr_map,
|
||||||
is_exc,
|
is_exc,
|
||||||
|
needs_cached_setattr,
|
||||||
has_cls_on_setattr,
|
has_cls_on_setattr,
|
||||||
attrs_init,
|
attrs_init,
|
||||||
)
|
)
|
||||||
|
@ -2019,7 +2020,7 @@ def _make_init(
|
||||||
if needs_cached_setattr:
|
if needs_cached_setattr:
|
||||||
# Save the lookup overhead in __init__ if we need to circumvent
|
# Save the lookup overhead in __init__ if we need to circumvent
|
||||||
# setattr hooks.
|
# setattr hooks.
|
||||||
globs["_setattr"] = _obj_setattr
|
globs["_cached_setattr_get"] = _obj_setattr.__get__
|
||||||
|
|
||||||
init = _make_method(
|
init = _make_method(
|
||||||
"__attrs_init__" if attrs_init else "__init__",
|
"__attrs_init__" if attrs_init else "__init__",
|
||||||
|
@ -2036,7 +2037,7 @@ def _setattr(attr_name, value_var, has_on_setattr):
|
||||||
"""
|
"""
|
||||||
Use the cached object.setattr to set *attr_name* to *value_var*.
|
Use the cached object.setattr to set *attr_name* to *value_var*.
|
||||||
"""
|
"""
|
||||||
return "_setattr(self, '%s', %s)" % (attr_name, value_var)
|
return "_setattr('%s', %s)" % (attr_name, value_var)
|
||||||
|
|
||||||
|
|
||||||
def _setattr_with_converter(attr_name, value_var, has_on_setattr):
|
def _setattr_with_converter(attr_name, value_var, has_on_setattr):
|
||||||
|
@ -2044,7 +2045,7 @@ def _setattr_with_converter(attr_name, value_var, has_on_setattr):
|
||||||
Use the cached object.setattr to set *attr_name* to *value_var*, but run
|
Use the cached object.setattr to set *attr_name* to *value_var*, but run
|
||||||
its converter first.
|
its converter first.
|
||||||
"""
|
"""
|
||||||
return "_setattr(self, '%s', %s(%s))" % (
|
return "_setattr('%s', %s(%s))" % (
|
||||||
attr_name,
|
attr_name,
|
||||||
_init_converter_pat % (attr_name,),
|
_init_converter_pat % (attr_name,),
|
||||||
value_var,
|
value_var,
|
||||||
|
@ -2086,6 +2087,7 @@ def _attrs_to_init_script(
|
||||||
cache_hash,
|
cache_hash,
|
||||||
base_attr_map,
|
base_attr_map,
|
||||||
is_exc,
|
is_exc,
|
||||||
|
needs_cached_setattr,
|
||||||
has_cls_on_setattr,
|
has_cls_on_setattr,
|
||||||
attrs_init,
|
attrs_init,
|
||||||
):
|
):
|
||||||
|
@ -2101,6 +2103,14 @@ def _attrs_to_init_script(
|
||||||
if pre_init:
|
if pre_init:
|
||||||
lines.append("self.__attrs_pre_init__()")
|
lines.append("self.__attrs_pre_init__()")
|
||||||
|
|
||||||
|
if needs_cached_setattr:
|
||||||
|
lines.append(
|
||||||
|
# Circumvent the __setattr__ descriptor to save one lookup per
|
||||||
|
# assignment.
|
||||||
|
# Note _setattr will be used again below if cache_hash is True
|
||||||
|
"_setattr = _cached_setattr_get(self)"
|
||||||
|
)
|
||||||
|
|
||||||
if frozen is True:
|
if frozen is True:
|
||||||
if slots is True:
|
if slots is True:
|
||||||
fmt_setter = _setattr
|
fmt_setter = _setattr
|
||||||
|
@ -2315,7 +2325,7 @@ def _attrs_to_init_script(
|
||||||
if frozen:
|
if frozen:
|
||||||
if slots:
|
if slots:
|
||||||
# if frozen and slots, then _setattr defined above
|
# if frozen and slots, then _setattr defined above
|
||||||
init_hash_cache = "_setattr(self, '%s', %s)"
|
init_hash_cache = "_setattr('%s', %s)"
|
||||||
else:
|
else:
|
||||||
# if frozen and not slots, then _inst_dict defined above
|
# if frozen and not slots, then _inst_dict defined above
|
||||||
init_hash_cache = "_inst_dict['%s'] = %s"
|
init_hash_cache = "_inst_dict['%s'] = %s"
|
||||||
|
@ -2428,7 +2438,7 @@ class Attribute:
|
||||||
)
|
)
|
||||||
|
|
||||||
# Cache this descriptor here to speed things up later.
|
# Cache this descriptor here to speed things up later.
|
||||||
bound_setattr = _obj_setattr.__get__(self, Attribute)
|
bound_setattr = _obj_setattr.__get__(self)
|
||||||
|
|
||||||
# Despite the big red warning, people *do* instantiate `Attribute`
|
# Despite the big red warning, people *do* instantiate `Attribute`
|
||||||
# themselves.
|
# themselves.
|
||||||
|
@ -2525,7 +2535,7 @@ class Attribute:
|
||||||
self._setattrs(zip(self.__slots__, state))
|
self._setattrs(zip(self.__slots__, state))
|
||||||
|
|
||||||
def _setattrs(self, name_values_pairs):
|
def _setattrs(self, name_values_pairs):
|
||||||
bound_setattr = _obj_setattr.__get__(self, Attribute)
|
bound_setattr = _obj_setattr.__get__(self)
|
||||||
for name, value in name_values_pairs:
|
for name, value in name_values_pairs:
|
||||||
if name != "metadata":
|
if name != "metadata":
|
||||||
bound_setattr(name, value)
|
bound_setattr(name, value)
|
||||||
|
|
|
@ -745,6 +745,7 @@ class TestFunctional:
|
||||||
|
|
||||||
src = inspect.getsource(D.__init__)
|
src = inspect.getsource(D.__init__)
|
||||||
|
|
||||||
assert "_setattr(self, 'x', x)" in src
|
assert "_setattr = _cached_setattr_get(self)" in src
|
||||||
assert "_setattr(self, 'y', y)" in src
|
assert "_setattr('x', x)" in src
|
||||||
|
assert "_setattr('y', y)" in src
|
||||||
assert object.__setattr__ != D.__setattr__
|
assert object.__setattr__ != D.__setattr__
|
||||||
|
|
Loading…
Reference in New Issue