Skip to content

Commit fa9d669

Browse files
AlexWaygoodmiss-islington
authored andcommitted
pythongh-118013: Use weakrefs for the cache key in inspect._shadowed_dict (pythonGH-118202)
(cherry picked from commit 8227883) Co-authored-by: Alex Waygood <[email protected]>
1 parent f86b17a commit fa9d669

File tree

5 files changed

+59
-8
lines changed

5 files changed

+59
-8
lines changed

Doc/whatsnew/3.12.rst

+3-4
Original file line numberDiff line numberDiff line change
@@ -734,8 +734,7 @@ inspect
734734

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

740739
itertools
741740
---------
@@ -1006,8 +1005,8 @@ typing
10061005
:func:`runtime-checkable protocols <typing.runtime_checkable>` has changed
10071006
significantly. Most ``isinstance()`` checks against protocols with only a few
10081007
members should be at least 2x faster than in 3.11, and some may be 20x
1009-
faster or more. However, ``isinstance()`` checks against protocols with fourteen
1010-
or more members may be slower than in Python 3.11. (Contributed by Alex
1008+
faster or more. However, ``isinstance()`` checks against protocols with many
1009+
members may be slower than in Python 3.11. (Contributed by Alex
10111010
Waygood in :gh:`74690` and :gh:`103193`.)
10121011

10131012
* All :data:`typing.TypedDict` and :data:`typing.NamedTuple` classes now have the

Lib/inspect.py

+22-3
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@
160160
from keyword import iskeyword
161161
from operator import attrgetter
162162
from collections import namedtuple, OrderedDict
163+
from weakref import ref as make_weakref
163164

164165
# Create constants for the compiler flags in Include/code.h
165166
# We try to get them from dis to avoid duplication
@@ -1798,9 +1799,16 @@ def _check_class(klass, attr):
17981799
return entry.__dict__[attr]
17991800
return _sentinel
18001801

1802+
18011803
@functools.lru_cache()
1802-
def _shadowed_dict_from_mro_tuple(mro):
1803-
for entry in mro:
1804+
def _shadowed_dict_from_weakref_mro_tuple(*weakref_mro):
1805+
for weakref_entry in weakref_mro:
1806+
# Normally we'd have to check whether the result of weakref_entry()
1807+
# is None here, in case the object the weakref is pointing to has died.
1808+
# In this specific case, however, we know that the only caller of this
1809+
# function is `_shadowed_dict()`, and that therefore this weakref is
1810+
# guaranteed to point to an object that is still alive.
1811+
entry = weakref_entry()
18041812
dunder_dict = _get_dunder_dict_of_class(entry)
18051813
if '__dict__' in dunder_dict:
18061814
class_dict = dunder_dict['__dict__']
@@ -1810,8 +1818,19 @@ def _shadowed_dict_from_mro_tuple(mro):
18101818
return class_dict
18111819
return _sentinel
18121820

1821+
18131822
def _shadowed_dict(klass):
1814-
return _shadowed_dict_from_mro_tuple(_static_getmro(klass))
1823+
# gh-118013: the inner function here is decorated with lru_cache for
1824+
# performance reasons, *but* make sure not to pass strong references
1825+
# to the items in the mro. Doing so can lead to unexpected memory
1826+
# consumption in cases where classes are dynamically created and
1827+
# destroyed, and the dynamically created classes happen to be the only
1828+
# objects that hold strong references to other objects that take up a
1829+
# significant amount of memory.
1830+
return _shadowed_dict_from_weakref_mro_tuple(
1831+
*[make_weakref(entry) for entry in _static_getmro(klass)]
1832+
)
1833+
18151834

18161835
def getattr_static(obj, attr, default=_sentinel):
18171836
"""Retrieve attributes without triggering dynamic lookup via the

Lib/test/libregrtest/utils.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ def clear_caches():
275275
except KeyError:
276276
pass
277277
else:
278-
inspect._shadowed_dict_from_mro_tuple.cache_clear()
278+
inspect._shadowed_dict_from_weakref_mro_tuple.cache_clear()
279279
inspect._filesbymodname.clear()
280280
inspect.modulesbyfile.clear()
281281

Lib/test/test_inspect/test_inspect.py

+24
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import collections
44
import datetime
55
import functools
6+
import gc
67
import importlib
78
import inspect
89
import io
@@ -20,6 +21,7 @@
2021
import unittest
2122
import unittest.mock
2223
import warnings
24+
import weakref
2325

2426
try:
2527
from concurrent.futures import ThreadPoolExecutor
@@ -2131,6 +2133,13 @@ def __dict__(self):
21312133
self.assertEqual(inspect.getattr_static(foo, 'a'), 3)
21322134
self.assertFalse(test.called)
21332135

2136+
class Bar(Foo): pass
2137+
2138+
bar = Bar()
2139+
bar.a = 5
2140+
self.assertEqual(inspect.getattr_static(bar, 'a'), 3)
2141+
self.assertFalse(test.called)
2142+
21342143
def test_mutated_mro(self):
21352144
test = self
21362145
test.called = False
@@ -2235,6 +2244,21 @@ def __getattribute__(self, attr):
22352244

22362245
self.assertFalse(test.called)
22372246

2247+
def test_cache_does_not_cause_classes_to_persist(self):
2248+
# regression test for gh-118013:
2249+
# check that the internal _shadowed_dict cache does not cause
2250+
# dynamically created classes to have extended lifetimes even
2251+
# when no other strong references to those classes remain.
2252+
# Since these classes can themselves hold strong references to
2253+
# other objects, this can cause unexpected memory consumption.
2254+
class Foo: pass
2255+
Foo.instance = Foo()
2256+
weakref_to_class = weakref.ref(Foo)
2257+
inspect.getattr_static(Foo.instance, 'whatever', 'irrelevant')
2258+
del Foo
2259+
gc.collect()
2260+
self.assertIsNone(weakref_to_class())
2261+
22382262

22392263
class TestGetGeneratorState(unittest.TestCase):
22402264

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Fix regression introduced in gh-103193 that meant that calling
2+
:func:`inspect.getattr_static` on an instance would cause a strong reference
3+
to that instance's class to persist in an internal cache in the
4+
:mod:`inspect` module. This caused unexpected memory consumption if the
5+
class was dynamically created, the class held strong references to other
6+
objects which took up a significant amount of memory, and the cache
7+
contained the sole strong reference to the class. The fix for the regression
8+
leads to a slowdown in :func:`getattr_static`, but the function should still
9+
be signficantly faster than it was in Python 3.11. Patch by Alex Waygood.

0 commit comments

Comments
 (0)