Skip to content

Commit f9d59d0

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

File tree

4 files changed

+227
-58
lines changed

4 files changed

+227
-58
lines changed

Lib/_py_abc.py

Lines changed: 32 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,22 @@ 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)
78+
# Automatically include cache entry
79+
cls._abc_cache.add(subclass)
6980
ABCMeta._abc_invalidation_counter += 1 # Invalidate negative cache
81+
# Recursively register the subclass in all ABC bases,
82+
# to avoid recursive lookups down the class tree.
83+
# >>> class Ancestor1(ABC): pass
84+
# >>> class Ancestor2(Ancestor1): pass
85+
# >>> class Other: pass
86+
# >>> Ancestor2.register(Other) # calls Ancestor1.register(Other)
87+
# >>> issubclass(Other, Ancestor2) is True
88+
# >>> issubclass(Other, Ancestor1) is True # already in registry
89+
for base in cls.__bases__:
90+
if hasattr(base, "_abc_registry"):
91+
base.register(subclass)
7092
return subclass
7193

7294
def _dump_registry(cls, file=None):
@@ -137,11 +159,16 @@ def __subclasscheck__(cls, subclass):
137159
if issubclass(subclass, rcls):
138160
cls._abc_cache.add(subclass)
139161
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
162+
# Check if it's a subclass of a subclass (recursive).
163+
# If __subclasses__ contain only ABCs,
164+
# calling issubclass(...) will trigger the same __subclasscheck__
165+
# on *every* element of class inheritance tree.
166+
# Performing that only in resence of `def __subclasses__()` classmethod
167+
if cls._abc_should_check_subclasses:
168+
for scls in cls.__subclasses__():
169+
if issubclass(subclass, scls):
170+
cls._abc_cache.add(subclass)
171+
return True
145172
# No dice; update negative cache
146173
cls._abc_negative_cache.add(subclass)
147174
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)