Skip to content

gh-92810: Reduce memory usage by ABCMeta.__subclasscheck__ #131914

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

Open
wants to merge 28 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
675bec5
gh-92810: Avoid O(n^2) complexity in ABCMeta.__subclasscheck__
dolfinus Mar 30, 2025
701ecc9
gh-92810: Apply fixes
dolfinus Mar 31, 2025
041f109
gh-92810: Apply fixes
dolfinus Mar 31, 2025
9bc4385
gh-92810: Apply fixes
dolfinus Mar 31, 2025
3d80b1e
gh-92810: Apply fixes
dolfinus Mar 31, 2025
b7603e0
gh-92810: Return __subclasses__clause back
dolfinus Apr 21, 2025
dd0d18c
gh-92810: Revert _abc.c changes
dolfinus Apr 21, 2025
8d695fd
gh-92810: Fix linter errors
dolfinus Apr 21, 2025
a2650b6
gh-92810: Add recursive issubclass check to _abc.c
dolfinus Jun 13, 2025
7afa5ea
gh-92810: Remove WeakKeyDictionary from _py_abc
dolfinus Jun 13, 2025
57980d3
gh-92810: Add news entry
dolfinus Jun 13, 2025
bbaf38a
gh-92810: Fix news entry
dolfinus Jun 13, 2025
6fc994d
gh-92810: Fixes after review
dolfinus Jun 22, 2025
b3b5895
gh-92810: Fixes after review
dolfinus Jun 22, 2025
69c5038
gh-92810: Fixes after review
dolfinus Jun 23, 2025
dc1b6d5
gh-92810: Fixes after review
dolfinus Jun 23, 2025
cd097ab
gh-92810: Introduce FT wrappers for uint64_t atomics
dolfinus Jun 23, 2025
f3a21a7
gh-92810: Use FT atomic wrappers for ABC invalidation counter
dolfinus Jun 23, 2025
e24e815
gh-92810: Fix missing FT wrapper
dolfinus Jun 23, 2025
b723912
gh-92810: Address review fixes
dolfinus Aug 4, 2025
0295846
Merge branch 'main' into improvement/ABCMeta_subclasscheck
dolfinus Aug 4, 2025
16f39bd
gh-92810: Address review fixes
dolfinus Aug 4, 2025
2dc6453
gh-92810: Add What's New entry
dolfinus Aug 4, 2025
968766d
gh-92810: Fix What's New entry syntax
dolfinus Aug 4, 2025
a6e4461
gh-92810: Address review fixes
dolfinus Aug 4, 2025
80d3281
gh-92810: Address review fixes
dolfinus Aug 4, 2025
ff38b9e
gh-92810: Properly reset recursion check
dolfinus Aug 4, 2025
23df287
Merge branch 'main' into improvement/ABCMeta_subclasscheck
dolfinus Aug 4, 2025
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
30 changes: 25 additions & 5 deletions Lib/_py_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ def __new__(mcls, name, bases, namespace, /, **kwargs):
cls._abc_cache = WeakSet()
cls._abc_negative_cache = WeakSet()
cls._abc_negative_cache_version = ABCMeta._abc_invalidation_counter
cls._abc_issubclasscheck_recursive = False
return cls

def register(cls, subclass):
Expand All @@ -66,7 +67,8 @@ def register(cls, subclass):
# This would create a cycle, which is bad for the algorithm below
raise RuntimeError("Refusing to create an inheritance cycle")
cls._abc_registry.add(subclass)
ABCMeta._abc_invalidation_counter += 1 # Invalidate negative cache
# Invalidate negative cache
ABCMeta._abc_invalidation_counter += 1
return subclass

def _dump_registry(cls, file=None):
Expand Down Expand Up @@ -137,11 +139,29 @@ def __subclasscheck__(cls, subclass):
if issubclass(subclass, rcls):
cls._abc_cache.add(subclass)
return True

# Check if it's a subclass of a subclass (recursive)
for scls in cls.__subclasses__():
if issubclass(subclass, scls):
cls._abc_cache.add(subclass)
# If inside recursive issubclass check, avoid adding classes to any cache because this
# may drastically increase memory usage.
# Unfortunately, issubclass/__subclasscheck__ don't accept third argument with context,
# so using global context within ABCMeta.
# This is done only on first method call, others will use cached result.
scls_is_abc = hasattr(scls, "_abc_issubclasscheck_recursive")
if scls_is_abc:
scls._abc_issubclasscheck_recursive = True

try:
result = issubclass(subclass, scls)
finally:
if scls_is_abc:
scls._abc_issubclasscheck_recursive = False

if result:
if not cls._abc_issubclasscheck_recursive:
cls._abc_cache.add(subclass)
return True
# No dice; update negative cache
cls._abc_negative_cache.add(subclass)

if not cls._abc_issubclasscheck_recursive:
cls._abc_negative_cache.add(subclass)
return False
164 changes: 134 additions & 30 deletions Lib/test/test_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,29 +270,100 @@ def x(self):
class C(metaclass=meta):
pass

def test_isinstance_direct_inheritance(self):
class A(metaclass=abc_ABCMeta):
pass
class B(A):
pass
class C(A):
pass
a = A()
b = B()
c = C()
# trigger caching
for _ in range(2):
self.assertIsInstance(a, A)
self.assertIsInstance(a, (A,))
Copy link
Member

Choose a reason for hiding this comment

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

AFAICT, tests are of the form:

isinstance(x, Y)  # optional for Y inheriting X
isinstance(x, (Y,) # optional
isinstance(x, X)
isinstance(x, (X,)

How about having a small helper method:

def check_isinstance(value, *classes):
    for cls in classes:
        self.assertIsInstance(value, cls)
        self.assertIsInstance(value, (cls,))

It would make the code way less vertical.

Copy link
Author

@dolfinus dolfinus Aug 4, 2025

Choose a reason for hiding this comment

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

Done. But I've added this helper only to new tests, existing ones are still using old form

self.assertNotIsInstance(a, B)
self.assertNotIsInstance(a, (B,))
self.assertNotIsInstance(a, C)
self.assertNotIsInstance(a, (C,))

self.assertIsInstance(b, B)
self.assertIsInstance(b, (B,))
self.assertIsInstance(b, A)
self.assertIsInstance(b, (A,))
self.assertNotIsInstance(b, C)
self.assertNotIsInstance(b, (C,))

self.assertIsInstance(c, C)
self.assertIsInstance(c, (C,))
self.assertIsInstance(c, A)
self.assertIsInstance(c, (A,))
self.assertNotIsInstance(c, B)
self.assertNotIsInstance(c, (B,))

self.assertIsSubclass(B, A)
self.assertIsSubclass(B, (A,))
self.assertIsSubclass(C, A)
self.assertIsSubclass(C, (A,))
self.assertNotIsSubclass(B, C)
self.assertNotIsSubclass(B, (C,))
self.assertNotIsSubclass(C, B)
self.assertNotIsSubclass(C, (B,))
self.assertNotIsSubclass(A, B)
self.assertNotIsSubclass(A, (B,))
self.assertNotIsSubclass(A, C)
self.assertNotIsSubclass(A, (C,))

def test_registration_basics(self):
class A(metaclass=abc_ABCMeta):
pass
class B(object):
pass
a = A()
b = B()
self.assertNotIsSubclass(B, A)
self.assertNotIsSubclass(B, (A,))
self.assertNotIsInstance(b, A)
self.assertNotIsInstance(b, (A,))

# trigger caching
for _ in range(2):
self.assertNotIsSubclass(B, A)
self.assertNotIsSubclass(B, (A,))
self.assertNotIsInstance(b, A)
self.assertNotIsInstance(b, (A,))

self.assertNotIsSubclass(A, B)
self.assertNotIsSubclass(A, (B,))
self.assertNotIsInstance(a, B)
self.assertNotIsInstance(a, (B,))

B1 = A.register(B)
self.assertIsSubclass(B, A)
self.assertIsSubclass(B, (A,))
self.assertIsInstance(b, A)
self.assertIsInstance(b, (A,))
self.assertIs(B1, B)
# trigger caching
for _ in range(2):
self.assertIsSubclass(B, A)
self.assertIsSubclass(B, (A,))
self.assertIsInstance(b, A)
self.assertIsInstance(b, (A,))
self.assertIs(B1, B)

self.assertNotIsSubclass(A, B)
self.assertNotIsSubclass(A, (B,))
self.assertNotIsInstance(a, B)
self.assertNotIsInstance(a, (B,))

class C(B):
pass
c = C()
self.assertIsSubclass(C, A)
self.assertIsSubclass(C, (A,))
self.assertIsInstance(c, A)
self.assertIsInstance(c, (A,))
# trigger caching
for _ in range(2):
self.assertIsSubclass(C, A)
self.assertIsSubclass(C, (A,))
self.assertIsInstance(c, A)
self.assertIsInstance(c, (A,))

self.assertNotIsSubclass(A, C)
self.assertNotIsSubclass(A, (C,))
self.assertNotIsInstance(a, C)
self.assertNotIsInstance(a, (C,))

def test_register_as_class_deco(self):
class A(metaclass=abc_ABCMeta):
Expand Down Expand Up @@ -377,39 +448,73 @@ class A(metaclass=abc_ABCMeta):
pass
self.assertIsSubclass(A, A)
self.assertIsSubclass(A, (A,))

class B(metaclass=abc_ABCMeta):
pass
self.assertNotIsSubclass(A, B)
self.assertNotIsSubclass(A, (B,))
self.assertNotIsSubclass(B, A)
self.assertNotIsSubclass(B, (A,))

class C(metaclass=abc_ABCMeta):
pass
A.register(B)
class B1(B):
pass
self.assertIsSubclass(B1, A)
self.assertIsSubclass(B1, (A,))
# trigger caching
for _ in range(2):
self.assertIsSubclass(B1, A)
self.assertIsSubclass(B1, (A,))

class C1(C):
pass
B1.register(C1)
self.assertNotIsSubclass(C, B)
self.assertNotIsSubclass(C, (B,))
self.assertNotIsSubclass(C, B1)
self.assertNotIsSubclass(C, (B1,))
self.assertIsSubclass(C1, A)
self.assertIsSubclass(C1, (A,))
self.assertIsSubclass(C1, B)
self.assertIsSubclass(C1, (B,))
self.assertIsSubclass(C1, B1)
self.assertIsSubclass(C1, (B1,))
# trigger caching
for _ in range(2):
self.assertNotIsSubclass(C, B)
self.assertNotIsSubclass(C, (B,))
self.assertNotIsSubclass(C, B1)
self.assertNotIsSubclass(C, (B1,))
self.assertIsSubclass(C1, A)
self.assertIsSubclass(C1, (A,))
self.assertIsSubclass(C1, B)
self.assertIsSubclass(C1, (B,))
self.assertIsSubclass(C1, B1)
self.assertIsSubclass(C1, (B1,))

C1.register(int)
class MyInt(int):
pass
self.assertIsSubclass(MyInt, A)
self.assertIsSubclass(MyInt, (A,))
self.assertIsInstance(42, A)
self.assertIsInstance(42, (A,))
# trigger caching
for _ in range(2):
self.assertIsSubclass(MyInt, A)
self.assertIsSubclass(MyInt, (A,))
self.assertIsInstance(42, A)
self.assertIsInstance(42, (A,))

def test_custom_subclasses(self):
class A: pass
class B: pass

class Parent1(metaclass=abc_ABCMeta):
@classmethod
def __subclasses__(cls):
return [A]

class Parent2(metaclass=abc_ABCMeta):
__subclasses__ = lambda: [A]

# trigger caching
for _ in range(2):
self.assertIsInstance(A(), Parent1)
self.assertIsSubclass(A, Parent1)
self.assertNotIsInstance(B(), Parent1)
self.assertNotIsSubclass(B, Parent1)

self.assertIsInstance(A(), Parent2)
self.assertIsSubclass(A, Parent2)
self.assertNotIsInstance(B(), Parent2)
self.assertNotIsSubclass(B, Parent2)

def test_issubclass_bad_arguments(self):
class A(metaclass=abc_ABCMeta):
Expand Down Expand Up @@ -522,7 +627,6 @@ def foo(self):
self.assertEqual(A.__abstractmethods__, set())
A()


def test_update_new_abstractmethods(self):
class A(metaclass=abc_ABCMeta):
@abc.abstractmethod
Expand Down
22 changes: 22 additions & 0 deletions Lib/test/test_isinstance.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,28 @@ class B:
with support.infinite_recursion(25):
self.assertRaises(RecursionError, issubclass, X(), int)

def test_custom_subclasses_are_ignored(self):
class A: pass
class B: pass

class Parent1:
@classmethod
def __subclasses__(cls):
return [A, B]

class Parent2:
__subclasses__ = lambda: [A, B]

self.assertNotIsInstance(A(), Parent1)
self.assertNotIsInstance(B(), Parent1)
self.assertNotIsSubclass(A, Parent1)
self.assertNotIsSubclass(B, Parent1)

self.assertNotIsInstance(A(), Parent2)
self.assertNotIsInstance(B(), Parent2)
self.assertNotIsSubclass(A, Parent2)
self.assertNotIsSubclass(B, Parent2)


def blowstack(fxn, arg, compare_to):
# Make sure that calling isinstance with a deeply nested tuple for its
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Reduce memory usage by :meth:`~type.__subclasscheck__`
Copy link
Member

Choose a reason for hiding this comment

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

I think this can be also added in What's New with concrete numbers (I think we have an optimizations section). The NEWS entry should be in Library though as it doesn't affect the builtins themselves.

Copy link
Author

Choose a reason for hiding this comment

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

I think this can be also added in What's New

Is there an example how this entry should be added? I'm new to CPython sources, and there can be some rules for this I'm not familiar with.

Copy link
Member

@picnixz picnixz Aug 4, 2025

Choose a reason for hiding this comment

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

Yes: open Doc/whatsnew/3.15.rst, and check if we have an optimization section in the document. Then just write something like:

Optimizations
=============

abc
---

* Reduce memory usage of `issubclass` checks for classes inheriting abstract classes.
  (Contributed by YOUR_NAME_OR_GITHUB_HANDLE in :gh:`92810`.)

You can add some relevant numbers if you want.

Copy link
Author

Choose a reason for hiding this comment

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

This requires rebasing or merging the main branch. Which option is preferred?

Copy link
Member

Choose a reason for hiding this comment

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

Just merge the main branch by hitting the "update" button and pull the remote changes (that way you can be sure that your local branch won't be desynced). We squash-merge commits at the end and we rewrite the commit message so it doesn't matter how messy your commit history is. It's better not to force-push because it breaks incremental GitHub reviews (there are only specific cases when you can force push but generally, we recommend avoiding force-pushing).

Copy link
Author

Choose a reason for hiding this comment

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

Added a What's New entry, but without any numbers, as they depend on the size of class tree

for :class:`abc.ABCMeta` and large class trees
Loading
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy