perf: replace RelationProxy.__getattribute__ with explicit count/clear#1650
Merged
collerek merged 1 commit intocheck-optimisations-and-rsfrom May 5, 2026
Merged
Conversation
Profile-driven removal of the per-attribute-access Python override on
RelationProxy. The previous __getattribute__ existed solely to redirect
two list-method names ("count", "clear") to the QuerysetProxy versions;
every other attribute access paid the cost of a Python-level
__getattribute__ call only to fall through to super.
cProfile (all_with_related, 40 000 row hydrations): __getattribute__
was 0.354 s tottime / 6.2 % of scenario time, with 420 000 calls. After
this change it is no longer in the top 25 by tottime — attribute access
goes through the C-level lookup path.
Replacement: define count() and clear() as async methods directly on
RelationProxy. They shadow list.count / list.clear by virtue of MRO and
delegate to queryset_proxy after self._initialize_queryset(). The
observable async semantics are unchanged: callers always did
``await proxy.count(...)`` / ``await proxy.clear(...)``; the previous
override returned a bound async method, the new methods are themselves
async — both produce the same coroutine.
Behavior unchanged: same signatures (distinct=True / keep_reversed=True
defaults), same delegation path, same QuerysetProxy initialization
trigger. 628 tests pass at 100 % coverage.
Benchmark deltas (median, pytest-benchmark, --warmup=on, 10+ rounds):
- test_get_all_with_related_models[10] -17.2 %
- test_get_all_with_related_models[20] -15.6 %
- test_get_all_with_related_models[40] -7.7 %
- test_get_all[250] -13.1 %
- test_get_all[500] -9.9 %
- test_get_all[1000] -6.4 %
- test_iterate[*] within noise
- single-row get_one / first I/O-dominated, ±15 % noise
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Profile-driven removal of the per-attribute-access Python override on
RelationProxy. The previous__getattribute__existed solely to redirect two list-method names (count,clear) to theQuerysetProxyversions; every other attribute access paid the cost of a Python-level__getattribute__call only to fall through tosuper().cProfile (
all_with_related, 40 000 row hydrations):__getattribute__was 0.354 s tottime / 6.2 % of scenario time, with 420 000 calls. After this change it is no longer in the top 25 by tottime — attribute access goes through the C-level lookup path.Changes
count()andclear()as explicitasyncmethods directly onRelationProxy. They shadowlist.count/list.clearby virtue of MRO and delegate toqueryset_proxyafterself._initialize_queryset().__getattribute__override and the_QUERYSET_PROXY_METHODSfrozenset.The observable async semantics are unchanged: callers always did
await proxy.count(...)/await proxy.clear(...). The previous override returned a bound async method fromQuerysetProxy; the new methods are themselvesasync— both produce the same coroutine. Same signatures (distinct=True/keep_reversed=Truedefaults), same delegation path, sameQuerysetProxyinitialization trigger.Benchmark deltas (median, pytest-benchmark, --warmup=on, 10+ rounds)
test_get_all_with_related_models[10]test_get_all_with_related_models[20]test_get_all_with_related_models[40]test_get_all[250]test_get_all[500]test_get_all[1000]test_iterate[*]get_one/firstTest plan
make pre-commit— clean (one# type: ignore[override]forcountsince signature differs fromSequence.count)make coverage— 628 passed, 25 skipped, 100.00 % coveragetests/test_relations/,tests/test_queries/test_queryproxy_on_m2m_models.py,test_aggr_functions.py,test_reverse_fk_queryset.pyall greenall_with_relatedconfirms__getattribute__no longer in top-25 by tottime🤖 Generated with Claude Code