Skip to content

Commit 295f64d

Browse files
committed
gh-92810: Reduce memory usage by ABCMeta.__subclasscheck__
1 parent b1a574f commit 295f64d

File tree

4 files changed

+225
-59
lines changed

4 files changed

+225
-59
lines changed

Lib/_py_abc.py

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ def __new__(mcls, name, bases, namespace, /, **kwargs):
4949
cls._abc_cache = WeakSet()
5050
cls._abc_negative_cache = WeakSet()
5151
cls._abc_negative_cache_version = ABCMeta._abc_invalidation_counter
52+
53+
# Performance optimization for common case
54+
cls._abc_should_check_subclasses = False
55+
if "__subclasses__" in namespace:
56+
cls._abc_should_check_subclasses = True
57+
for base in bases:
58+
if hasattr(base, "_abc_should_check_subclasses"):
59+
base._abc_should_check_subclasses = True
5260
return cls
5361

5462
def register(cls, subclass):
@@ -65,8 +73,20 @@ def register(cls, subclass):
6573
if issubclass(cls, subclass):
6674
# This would create a cycle, which is bad for the algorithm below
6775
raise RuntimeError("Refusing to create an inheritance cycle")
76+
# Add registry entry
6877
cls._abc_registry.add(subclass)
6978
ABCMeta._abc_invalidation_counter += 1 # Invalidate negative cache
79+
# Recursively register the subclass in all ABC bases,
80+
# to avoid recursive lookups down the class tree.
81+
# >>> class Ancestor1(ABC): pass
82+
# >>> class Ancestor2(Ancestor1): pass
83+
# >>> class Other: pass
84+
# >>> Ancestor2.register(Other) # calls Ancestor1.register(Other)
85+
# >>> issubclass(Other, Ancestor2) is True
86+
# >>> issubclass(Other, Ancestor1) is True # already in registry
87+
for base in cls.__bases__:
88+
if hasattr(base, "_abc_registry"):
89+
base.register(subclass)
7090
return subclass
7191

7292
def _dump_registry(cls, file=None):
@@ -132,16 +152,25 @@ def __subclasscheck__(cls, subclass):
132152
if cls in getattr(subclass, '__mro__', ()):
133153
cls._abc_cache.add(subclass)
134154
return True
155+
# Fast path: check subclass is in weakset directly.
156+
if subclass in cls._abc_registry:
157+
cls._abc_cache.add(subclass)
158+
return True
135159
# Check if it's a subclass of a registered class (recursive)
136160
for rcls in cls._abc_registry:
137161
if issubclass(subclass, rcls):
138162
cls._abc_cache.add(subclass)
139163
return True
140-
# Check if it's a subclass of a subclass (recursive)
141-
for scls in cls.__subclasses__():
142-
if issubclass(subclass, scls):
143-
cls._abc_cache.add(subclass)
144-
return True
164+
# Check if it's a subclass of a subclass (recursive).
165+
# If __subclasses__ contain only ABCs,
166+
# calling issubclass(...) will trigger the same __subclasscheck__
167+
# on *every* element of class inheritance tree.
168+
# Performing that only in resence of `def __subclasses__()` classmethod
169+
if cls._abc_should_check_subclasses:
170+
for scls in cls.__subclasses__():
171+
if issubclass(subclass, scls):
172+
cls._abc_cache.add(subclass)
173+
return True
145174
# No dice; update negative cache
146175
cls._abc_negative_cache.add(subclass)
147176
return False

Lib/abc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ class ABCMeta(type):
104104
"""
105105
def __new__(mcls, name, bases, namespace, /, **kwargs):
106106
cls = super().__new__(mcls, name, bases, namespace, **kwargs)
107-
_abc_init(cls)
107+
_abc_init(cls, bases, namespace)
108108
return cls
109109

110110
def register(cls, subclass):

0 commit comments

Comments
 (0)