Skip to content

Commit 7369ad9

Browse files
eendebakptpre-commit-ci[bot]hynek
authored
Improve performance of asdict in the common case (#1463)
* Improve performance of asdict in the common case * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix for python 3.9 * fix for python3.9 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * coverage * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * docstring * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * add benchmark for asdict * Remove now-redundant benchmark * Fix test docstring * Add news fragment --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Hynek Schlawack <hs@ox.cx>
1 parent 57f0d54 commit 7369ad9

File tree

3 files changed

+49
-7
lines changed

3 files changed

+49
-7
lines changed

changelog.d/1463.change.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The performance of `attrs.asdict()` has been improved by 45–260%.

src/attr/_funcs.py

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,23 @@
88
from .exceptions import AttrsAttributeNotFoundError
99

1010

11+
_ATOMIC_TYPES = frozenset(
12+
{
13+
type(None),
14+
bool,
15+
int,
16+
float,
17+
str,
18+
complex,
19+
bytes,
20+
type(...),
21+
type,
22+
range,
23+
property,
24+
}
25+
)
26+
27+
1128
def asdict(
1229
inst,
1330
recurse=True,
@@ -71,7 +88,10 @@ def asdict(
7188
v = value_serializer(inst, a, v)
7289

7390
if recurse is True:
74-
if has(v.__class__):
91+
value_type = type(v)
92+
if value_type in _ATOMIC_TYPES:
93+
rv[a.name] = v
94+
elif has(value_type):
7595
rv[a.name] = asdict(
7696
v,
7797
recurse=True,
@@ -80,8 +100,8 @@ def asdict(
80100
retain_collection_types=retain_collection_types,
81101
value_serializer=value_serializer,
82102
)
83-
elif isinstance(v, (tuple, list, set, frozenset)):
84-
cf = v.__class__ if retain_collection_types is True else list
103+
elif issubclass(value_type, (tuple, list, set, frozenset)):
104+
cf = value_type if retain_collection_types is True else list
85105
items = [
86106
_asdict_anything(
87107
i,
@@ -101,7 +121,7 @@ def asdict(
101121
# Workaround for TypeError: cf.__new__() missing 1 required
102122
# positional argument (which appears, for a namedturle)
103123
rv[a.name] = cf(*items)
104-
elif isinstance(v, dict):
124+
elif issubclass(value_type, dict):
105125
df = dict_factory
106126
rv[a.name] = df(
107127
(
@@ -142,7 +162,12 @@ def _asdict_anything(
142162
"""
143163
``asdict`` only works on attrs instances, this works on anything.
144164
"""
145-
if getattr(val.__class__, "__attrs_attrs__", None) is not None:
165+
val_type = type(val)
166+
if val_type in _ATOMIC_TYPES:
167+
rv = val
168+
if value_serializer is not None:
169+
rv = value_serializer(None, None, rv)
170+
elif getattr(val_type, "__attrs_attrs__", None) is not None:
146171
# Attrs class.
147172
rv = asdict(
148173
val,
@@ -152,7 +177,7 @@ def _asdict_anything(
152177
retain_collection_types=retain_collection_types,
153178
value_serializer=value_serializer,
154179
)
155-
elif isinstance(val, (tuple, list, set, frozenset)):
180+
elif issubclass(val_type, (tuple, list, set, frozenset)):
156181
if retain_collection_types is True:
157182
cf = val.__class__
158183
elif is_key:
@@ -173,7 +198,7 @@ def _asdict_anything(
173198
for i in val
174199
]
175200
)
176-
elif isinstance(val, dict):
201+
elif issubclass(val_type, dict):
177202
df = dict_factory
178203
rv = df(
179204
(

tests/test_funcs.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,22 @@ def test_recurse(self, C, dict_class):
6666
C(C(1, 2), C(3, 4)), dict_factory=dict_class
6767
)
6868

69+
def test_non_atomic_types(self, C):
70+
"""
71+
Non-atomic types that don't have special treatment for are serialized
72+
and the types are retained.
73+
"""
74+
75+
class Int(int):
76+
pass
77+
78+
c = C(Int(10), [Int(1), 2])
79+
expected = {"x": 10, "y": [1, 2]}
80+
81+
assert expected == asdict(c)
82+
assert type(asdict(c)["x"]) is Int
83+
assert type(asdict(c)["y"][0]) is Int
84+
6985
def test_nested_lists(self, C):
7086
"""
7187
Test unstructuring deeply nested lists.

0 commit comments

Comments
 (0)