Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-103193: cache calls to inspect._shadowed_dict in inspect.getattr_static #104267

Merged
merged 4 commits into from
May 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions Doc/whatsnew/3.12.rst
Original file line number Diff line number Diff line change
Expand Up @@ -342,8 +342,9 @@ inspect
(Contributed by Thomas Krennwallner in :issue:`35759`.)

* The performance of :func:`inspect.getattr_static` has been considerably
improved. Most calls to the function should be around 2x faster than they
were in Python 3.11. (Contributed by Alex Waygood in :gh:`103193`.)
improved. Most calls to the function should be at least 2x faster than they
were in Python 3.11, and some may be 6x faster or more. (Contributed by Alex
Waygood in :gh:`103193`.)

pathlib
-------
Expand Down Expand Up @@ -597,7 +598,7 @@ typing
:func:`runtime-checkable protocols <typing.runtime_checkable>` has changed
significantly. Most ``isinstance()`` checks against protocols with only a few
members should be at least 2x faster than in 3.11, and some may be 20x
faster or more. However, ``isinstance()`` checks against protocols with seven
faster or more. However, ``isinstance()`` checks against protocols with fourteen
or more members may be slower than in Python 3.11. (Contributed by Alex
Waygood in :gh:`74690` and :gh:`103193`.)

Expand Down
8 changes: 6 additions & 2 deletions Lib/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -1794,8 +1794,9 @@ def _check_class(klass, attr):
return entry.__dict__[attr]
return _sentinel

def _shadowed_dict(klass):
for entry in _static_getmro(klass):
@functools.lru_cache()
def _shadowed_dict_from_mro_tuple(mro):
for entry in mro:
dunder_dict = _get_dunder_dict_of_class(entry)
if '__dict__' in dunder_dict:
class_dict = dunder_dict['__dict__']
Expand All @@ -1805,6 +1806,9 @@ def _shadowed_dict(klass):
return class_dict
return _sentinel

def _shadowed_dict(klass):
return _shadowed_dict_from_mro_tuple(_static_getmro(klass))
Comment on lines +1809 to +1810
Copy link
Member Author

@AlexWaygood AlexWaygood May 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I considered inlining the calls to _static_getmro, but I thought this made it more readable. It also preserves git blame better, and having to pass through this function doesn't seem to have much impact on performance


def getattr_static(obj, attr, default=_sentinel):
"""Retrieve attributes without triggering dynamic lookup via the
descriptor protocol, __getattr__ or __getattribute__.
Expand Down
22 changes: 22 additions & 0 deletions Lib/test/test_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -2111,6 +2111,28 @@ def __dict__(self):
self.assertEqual(inspect.getattr_static(foo, 'a'), 3)
self.assertFalse(test.called)

def test_mutated_mro(self):
test = self
test.called = False

class Foo(dict):
a = 3
@property
def __dict__(self):
test.called = True
return {}

class Bar(dict):
a = 4

class Baz(Bar): pass

baz = Baz()
self.assertEqual(inspect.getattr_static(baz, 'a'), 4)
Baz.__bases__ = (Foo,)
self.assertEqual(inspect.getattr_static(baz, 'a'), 3)
self.assertFalse(test.called)

def test_custom_object_dict(self):
test = self
test.called = False
Expand Down