From 0cf31cb0bab817149eceed394fd07e227f8b050d Mon Sep 17 00:00:00 2001 From: Martynov Maxim Date: Fri, 7 Nov 2025 00:55:05 +0300 Subject: [PATCH 1/4] gh-92810: Add atomic FT wrappers for uint64_t --- .../internal/pycore_pyatomic_ft_wrappers.h | 9 +++++++ Modules/_abc.c | 24 ++++--------------- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/Include/internal/pycore_pyatomic_ft_wrappers.h b/Include/internal/pycore_pyatomic_ft_wrappers.h index 2ae0185226f847..44b2400eff49da 100644 --- a/Include/internal/pycore_pyatomic_ft_wrappers.h +++ b/Include/internal/pycore_pyatomic_ft_wrappers.h @@ -45,6 +45,8 @@ extern "C" { _Py_atomic_load_uint16_relaxed(&value) #define FT_ATOMIC_LOAD_UINT32_RELAXED(value) \ _Py_atomic_load_uint32_relaxed(&value) +#define FT_ATOMIC_LOAD_UINT64_RELAXED(value) \ + _Py_atomic_load_uint64_relaxed(&value) #define FT_ATOMIC_LOAD_ULONG_RELAXED(value) \ _Py_atomic_load_ulong_relaxed(&value) #define FT_ATOMIC_STORE_PTR_RELAXED(value, new_value) \ @@ -61,6 +63,8 @@ extern "C" { _Py_atomic_store_uint16_relaxed(&value, new_value) #define FT_ATOMIC_STORE_UINT32_RELAXED(value, new_value) \ _Py_atomic_store_uint32_relaxed(&value, new_value) +#define FT_ATOMIC_STORE_UINT64_RELAXED(value, new_value) \ + _Py_atomic_store_uint64_relaxed(&value, new_value) #define FT_ATOMIC_STORE_CHAR_RELAXED(value, new_value) \ _Py_atomic_store_char_relaxed(&value, new_value) #define FT_ATOMIC_LOAD_CHAR_RELAXED(value) \ @@ -115,6 +119,8 @@ extern "C" { _Py_atomic_load_ullong_relaxed(&value) #define FT_ATOMIC_ADD_SSIZE(value, new_value) \ (void)_Py_atomic_add_ssize(&value, new_value) +#define FT_ATOMIC_ADD_UINT64(value, new_value) \ + (void)_Py_atomic_add_uint64(&value, new_value) #define FT_MUTEX_LOCK(lock) PyMutex_Lock(lock) #define FT_MUTEX_UNLOCK(lock) PyMutex_Unlock(lock) @@ -132,6 +138,7 @@ extern "C" { #define FT_ATOMIC_LOAD_UINT8_RELAXED(value) value #define FT_ATOMIC_LOAD_UINT16_RELAXED(value) value #define FT_ATOMIC_LOAD_UINT32_RELAXED(value) value +#define FT_ATOMIC_LOAD_UINT64_RELAXED(value) value #define FT_ATOMIC_LOAD_ULONG_RELAXED(value) value #define FT_ATOMIC_STORE_PTR_RELAXED(value, new_value) value = new_value #define FT_ATOMIC_STORE_PTR_RELEASE(value, new_value) value = new_value @@ -140,6 +147,7 @@ extern "C" { #define FT_ATOMIC_STORE_UINT8_RELAXED(value, new_value) value = new_value #define FT_ATOMIC_STORE_UINT16_RELAXED(value, new_value) value = new_value #define FT_ATOMIC_STORE_UINT32_RELAXED(value, new_value) value = new_value +#define FT_ATOMIC_STORE_UINT64_RELAXED(value, new_value) value = new_value #define FT_ATOMIC_LOAD_CHAR_RELAXED(value) value #define FT_ATOMIC_STORE_CHAR_RELAXED(value, new_value) value = new_value #define FT_ATOMIC_LOAD_UCHAR_RELAXED(value) value @@ -167,6 +175,7 @@ extern "C" { #define FT_ATOMIC_LOAD_ULLONG_RELAXED(value) value #define FT_ATOMIC_STORE_ULLONG_RELAXED(value, new_value) value = new_value #define FT_ATOMIC_ADD_SSIZE(value, new_value) (void)(value += new_value) +#define FT_ATOMIC_ADD_UINT64(value, new_value) (void)(value += new_value) #define FT_MUTEX_LOCK(lock) do {} while (0) #define FT_MUTEX_UNLOCK(lock) do {} while (0) diff --git a/Modules/_abc.c b/Modules/_abc.c index f87a5c702946bc..ccd9f6bb693bf4 100644 --- a/Modules/_abc.c +++ b/Modules/_abc.c @@ -35,21 +35,13 @@ get_abc_state(PyObject *module) static inline uint64_t get_invalidation_counter(_abcmodule_state *state) { -#ifdef Py_GIL_DISABLED - return _Py_atomic_load_uint64(&state->abc_invalidation_counter); -#else - return state->abc_invalidation_counter; -#endif + return FT_ATOMIC_LOAD_UINT64_RELAXED(state->abc_invalidation_counter); } static inline void increment_invalidation_counter(_abcmodule_state *state) { -#ifdef Py_GIL_DISABLED - _Py_atomic_add_uint64(&state->abc_invalidation_counter, 1); -#else - state->abc_invalidation_counter++; -#endif + FT_ATOMIC_ADD_UINT64(state->abc_invalidation_counter, 1); } /* This object stores internal state for ABCs. @@ -72,21 +64,13 @@ typedef struct { static inline uint64_t get_cache_version(_abc_data *impl) { -#ifdef Py_GIL_DISABLED - return _Py_atomic_load_uint64(&impl->_abc_negative_cache_version); -#else - return impl->_abc_negative_cache_version; -#endif + return FT_ATOMIC_LOAD_UINT64_RELAXED(impl->_abc_negative_cache_version); } static inline void set_cache_version(_abc_data *impl, uint64_t version) { -#ifdef Py_GIL_DISABLED - _Py_atomic_store_uint64(&impl->_abc_negative_cache_version, version); -#else - impl->_abc_negative_cache_version = version; -#endif + FT_ATOMIC_STORE_UINT64_RELAXED(impl->_abc_negative_cache_version, version); } static int From b1a574fe882aef1597c8863739155162107c34d1 Mon Sep 17 00:00:00 2001 From: Martynov Maxim Date: Fri, 7 Nov 2025 00:55:52 +0300 Subject: [PATCH 2/4] gh-92810: Add more tests for ABC inheritance & registration --- Lib/test/test_abc.py | 226 +++++++++++++++++++++++++++++++----- Lib/test/test_isinstance.py | 22 ++++ 2 files changed, 217 insertions(+), 31 deletions(-) diff --git a/Lib/test/test_abc.py b/Lib/test/test_abc.py index 80ee9e0ba56e75..99b7f0ae0866af 100644 --- a/Lib/test/test_abc.py +++ b/Lib/test/test_abc.py @@ -70,6 +70,25 @@ def foo(): return 4 class TestABC(unittest.TestCase): + def check_isinstance(self, obj, target_class): + self.assertIsInstance(obj, target_class) + self.assertIsInstance(obj, (target_class,)) + self.assertIsInstance(obj, target_class | target_class) + + def check_not_isinstance(self, obj, target_class): + self.assertNotIsInstance(obj, target_class) + self.assertNotIsInstance(obj, (target_class,)) + self.assertNotIsInstance(obj, target_class | target_class) + + def check_issubclass(self, klass, target_class): + self.assertIsSubclass(klass, target_class) + self.assertIsSubclass(klass, (target_class,)) + self.assertIsSubclass(klass, target_class | target_class) + + def check_not_issubclass(self, klass, target_class): + self.assertNotIsSubclass(klass, target_class) + self.assertNotIsSubclass(klass, (target_class,)) + self.assertNotIsSubclass(klass, target_class | target_class) def test_ABC_helper(self): # create an ABC using the helper class and perform basic checks @@ -270,29 +289,75 @@ 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.check_isinstance(a, A) + self.check_not_isinstance(a, B) + self.check_not_isinstance(a, C) + + self.check_isinstance(b, B) + self.check_isinstance(b, A) + self.check_not_isinstance(b, C) + + self.check_isinstance(c, C) + self.check_isinstance(c, A) + self.check_not_isinstance(c, B) + + self.check_issubclass(B, A) + self.check_issubclass(C, A) + self.check_not_issubclass(B, C) + self.check_not_issubclass(C, B) + self.check_not_issubclass(A, B) + self.check_not_issubclass(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.check_not_issubclass(B, A) + self.check_not_isinstance(b, A) + + self.check_not_issubclass(A, B) + self.check_not_isinstance(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.check_issubclass(B, A) + self.check_isinstance(b, A) + self.assertIs(B1, B) + + self.check_not_issubclass(A, B) + self.check_not_isinstance(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.check_issubclass(C, A) + self.check_isinstance(c, A) + + self.check_not_issubclass(A, C) + self.check_not_isinstance(a, C) def test_register_as_class_deco(self): class A(metaclass=abc_ABCMeta): @@ -377,39 +442,95 @@ 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(A): pass + + class C: pass + class D(C): pass + + class Root(metaclass=abc_ABCMeta): pass + + class Parent1(Root): + @classmethod + def __subclasses__(cls): + return [A] + + class Parent2(Root): + __subclasses__ = lambda: [A] + + # trigger caching + for _ in range(2): + self.check_isinstance(A(), Parent1) + self.check_isinstance(B(), Parent1) + self.check_issubclass(A, Parent1) + self.check_issubclass(B, Parent1) + self.check_not_isinstance(C(), Parent1) + self.check_not_isinstance(D(), Parent1) + self.check_not_issubclass(C, Parent1) + self.check_not_issubclass(D, Parent1) + + self.check_isinstance(A(), Parent2) + self.check_isinstance(B(), Parent2) + self.check_issubclass(A, Parent2) + self.check_issubclass(B, Parent2) + self.check_not_isinstance(C(), Parent2) + self.check_not_isinstance(D(), Parent2) + self.check_not_issubclass(C, Parent2) + self.check_not_issubclass(D, Parent2) + + self.check_isinstance(A(), Root) + self.check_isinstance(B(), Root) + self.check_issubclass(A, Root) + self.check_issubclass(B, Root) + self.check_not_isinstance(C(), Root) + self.check_not_isinstance(D(), Root) + self.check_not_issubclass(C, Root) + self.check_not_issubclass(D, Root) def test_issubclass_bad_arguments(self): class A(metaclass=abc_ABCMeta): @@ -460,8 +581,32 @@ class S(metaclass=abc_ABCMeta): with self.assertRaisesRegex(CustomError, exc_msg): issubclass(int, S) - def test_subclasshook(self): + def test_issubclass_bad_class(self): class A(metaclass=abc.ABCMeta): + pass + + A._abc_impl = 1 + error_msg = "_abc_impl is set to a wrong type" + with self.assertRaisesRegex(TypeError, error_msg): + issubclass(A, A) + + class B(metaclass=_py_abc.ABCMeta): + pass + + B._abc_cache = 1 + error_msg = "argument of type 'int' is not a container or iterable" + with self.assertRaisesRegex(TypeError, error_msg): + issubclass(B, B) + + class C(metaclass=_py_abc.ABCMeta): + pass + + C._abc_negative_cache = 1 + with self.assertRaisesRegex(TypeError, error_msg): + issubclass(C, C) + + def test_subclasshook(self): + class A(metaclass=abc_ABCMeta): @classmethod def __subclasshook__(cls, C): if cls is A: @@ -478,6 +623,26 @@ class C: self.assertNotIsSubclass(C, A) self.assertNotIsSubclass(C, (A,)) + def test_subclasshook_exception(self): + # Check that issubclass() propagates exceptions raised by + # __subclasshook__. + class CustomError(Exception): ... + exc_msg = "exception from __subclasshook__" + class A(metaclass=abc_ABCMeta): + @classmethod + def __subclasshook__(cls, C): + raise CustomError(exc_msg) + with self.assertRaisesRegex(CustomError, exc_msg): + issubclass(A, A) + class B(A): + pass + with self.assertRaisesRegex(CustomError, exc_msg): + issubclass(B, A) + class C: + pass + with self.assertRaisesRegex(CustomError, exc_msg): + issubclass(C, A) + def test_all_new_methods_are_called(self): class A(metaclass=abc_ABCMeta): pass @@ -522,7 +687,6 @@ def foo(self): self.assertEqual(A.__abstractmethods__, set()) A() - def test_update_new_abstractmethods(self): class A(metaclass=abc_ABCMeta): @abc.abstractmethod diff --git a/Lib/test/test_isinstance.py b/Lib/test/test_isinstance.py index f440fc28ee7b7d..1786f052f37923 100644 --- a/Lib/test/test_isinstance.py +++ b/Lib/test/test_isinstance.py @@ -353,6 +353,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 From 295ccb50bca091ac01c4d9447c44af7bad3696f8 Mon Sep 17 00:00:00 2001 From: Maxim Martynov Date: Sun, 30 Nov 2025 23:58:29 +0300 Subject: [PATCH 3/4] gh-92810: Reduce memory usage by ABCMeta.__subclasscheck__ --- Lib/_py_abc.py | 35 ++++++- Lib/abc.py | 2 +- Modules/_abc.c | 203 ++++++++++++++++++++++++++++++---------- Modules/clinic/_abc.c.h | 38 +++++++- 4 files changed, 220 insertions(+), 58 deletions(-) diff --git a/Lib/_py_abc.py b/Lib/_py_abc.py index c870ae9048b4f1..ee1d5a13c00f94 100644 --- a/Lib/_py_abc.py +++ b/Lib/_py_abc.py @@ -49,6 +49,14 @@ 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 + + # Performance optimization for common case + cls._abc_should_check_subclasses = False + if "__subclasses__" in namespace: + cls._abc_should_check_subclasses = True + for base in bases: + if hasattr(base, "_abc_should_check_subclasses"): + base._abc_should_check_subclasses = True return cls def register(cls, subclass): @@ -65,8 +73,20 @@ def register(cls, subclass): if issubclass(cls, subclass): # This would create a cycle, which is bad for the algorithm below raise RuntimeError("Refusing to create an inheritance cycle") + # Add registry entry cls._abc_registry.add(subclass) ABCMeta._abc_invalidation_counter += 1 # Invalidate negative cache + # Recursively register the subclass in all ABC bases, + # to avoid recursive lookups down the class tree. + # >>> class Ancestor1(ABC): pass + # >>> class Ancestor2(Ancestor1): pass + # >>> class Other: pass + # >>> Ancestor2.register(Other) # calls Ancestor1.register(Other) + # >>> issubclass(Other, Ancestor2) is True + # >>> issubclass(Other, Ancestor1) is True # already in registry + for base in cls.__bases__: + if hasattr(base, "_abc_registry"): + base.register(subclass) return subclass def _dump_registry(cls, file=None): @@ -137,11 +157,16 @@ 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) - return True + # Check if it's a subclass of a subclass (recursive). + # If __subclasses__ contain only ABCs, + # calling issubclass(...) will trigger the same __subclasscheck__ + # on *every* element of class inheritance tree. + # Performing that only in resence of `def __subclasses__()` classmethod + if cls._abc_should_check_subclasses: + for scls in cls.__subclasses__(): + if issubclass(subclass, scls): + cls._abc_cache.add(subclass) + return True # No dice; update negative cache cls._abc_negative_cache.add(subclass) return False diff --git a/Lib/abc.py b/Lib/abc.py index f8a4e11ce9c3b1..dde28a0e5c58ee 100644 --- a/Lib/abc.py +++ b/Lib/abc.py @@ -104,7 +104,7 @@ class ABCMeta(type): """ def __new__(mcls, name, bases, namespace, /, **kwargs): cls = super().__new__(mcls, name, bases, namespace, **kwargs) - _abc_init(cls) + _abc_init(cls, bases, namespace) return cls def register(cls, subclass): diff --git a/Modules/_abc.c b/Modules/_abc.c index ccd9f6bb693bf4..ab7e6e61380f76 100644 --- a/Modules/_abc.c +++ b/Modules/_abc.c @@ -57,6 +57,7 @@ typedef struct { PyObject *_abc_cache; PyObject *_abc_negative_cache; uint64_t _abc_negative_cache_version; + uint8_t _abc_should_check_subclasses; } _abc_data; #define _abc_data_CAST(op) ((_abc_data *)(op)) @@ -73,6 +74,18 @@ set_cache_version(_abc_data *impl, uint64_t version) FT_ATOMIC_STORE_UINT64_RELAXED(impl->_abc_negative_cache_version, version); } +static inline uint8_t +get_should_check_subclasses(_abc_data *impl) +{ + return FT_ATOMIC_LOAD_UINT8_RELAXED(impl->_abc_should_check_subclasses); +} + +static inline void +set_should_check_subclasses(_abc_data *impl) +{ + FT_ATOMIC_STORE_UINT8_RELAXED(impl->_abc_should_check_subclasses, 1); +} + static int abc_data_traverse(PyObject *op, visitproc visit, void *arg) { @@ -123,6 +136,7 @@ abc_data_new(PyTypeObject *type, PyObject *args, PyObject *kwds) self->_abc_cache = NULL; self->_abc_negative_cache = NULL; self->_abc_negative_cache_version = get_invalidation_counter(state); + self->_abc_should_check_subclasses = 0; return (PyObject *) self; } @@ -161,6 +175,30 @@ _get_impl(PyObject *module, PyObject *self) return (_abc_data *)impl; } +/* If class is inherited from ABC, set data to point to internal ABC state of class, and return 1. + If object is not inherited from ABC, return 0. + If error is encountered, return -1. + */ +static int +_get_optional_impl(_abcmodule_state *state, PyObject *self, _abc_data **data) +{ + assert(data != NULL); + PyObject *impl = NULL; + int res = PyObject_GetOptionalAttr(self, &_Py_ID(_abc_impl), &impl); + if (res <= 0) { + *data = NULL; + return res; + } + if (!Py_IS_TYPE(impl, state->_abc_data_type)) { + PyErr_SetString(PyExc_TypeError, "_abc_impl is set to a wrong type"); + Py_DECREF(impl); + *data = NULL; + return -1; + } + *data = (_abc_data *)impl; + return 1; +} + static int _in_weak_set(_abc_data *impl, PyObject **pset, PyObject *obj) { @@ -331,11 +369,12 @@ _abc__get_dump(PyObject *module, PyObject *self) } PyObject *res; Py_BEGIN_CRITICAL_SECTION(impl); - res = Py_BuildValue("NNNK", + res = Py_BuildValue("NNNKK", PySet_New(impl->_abc_registry), PySet_New(impl->_abc_cache), PySet_New(impl->_abc_negative_cache), - get_cache_version(impl)); + get_cache_version(impl), + get_should_check_subclasses(impl)); Py_END_CRITICAL_SECTION(); Py_DECREF(impl); return res; @@ -343,7 +382,7 @@ _abc__get_dump(PyObject *module, PyObject *self) // Compute set of abstract method names. static int -compute_abstract_methods(PyObject *self) +compute_abstract_methods(PyObject *self, PyObject *bases, PyObject *ns) { int ret = -1; PyObject *abstracts = PyFrozenSet_New(NULL); @@ -351,19 +390,13 @@ compute_abstract_methods(PyObject *self) return -1; } - PyObject *ns = NULL, *items = NULL, *bases = NULL; // Py_XDECREF()ed on error. - /* Stage 1: direct abstract methods. */ - ns = PyObject_GetAttr(self, &_Py_ID(__dict__)); - if (!ns) { - goto error; - } - // We can't use PyDict_Next(ns) even when ns is dict because // _PyObject_IsAbstract() can mutate ns. - items = PyMapping_Items(ns); + PyObject *items = PyMapping_Items(ns); if (!items) { - goto error; + Py_DECREF(abstracts); + return -1; } assert(PyList_Check(items)); for (Py_ssize_t pos = 0; pos < PyList_GET_SIZE(items); pos++) { @@ -398,15 +431,6 @@ compute_abstract_methods(PyObject *self) } /* Stage 2: inherited abstract methods. */ - bases = PyObject_GetAttr(self, &_Py_ID(__bases__)); - if (!bases) { - goto error; - } - if (!PyTuple_Check(bases)) { - PyErr_SetString(PyExc_TypeError, "__bases__ is not tuple"); - goto error; - } - for (Py_ssize_t pos = 0; pos < PyTuple_GET_SIZE(bases); pos++) { PyObject *item = PyTuple_GET_ITEM(bases, pos); // borrowed PyObject *base_abstracts, *iter; @@ -459,12 +483,36 @@ compute_abstract_methods(PyObject *self) ret = 0; error: Py_DECREF(abstracts); - Py_XDECREF(ns); - Py_XDECREF(items); - Py_XDECREF(bases); + Py_DECREF(items); return ret; } +/* + * Notify base classes that child one has __subclasses__ overriden. + * Used as performance optimization in __subclasscheck__ + */ +static int +_abc_notify_subclasses_override(_abcmodule_state *state, PyObject *data, PyObject *bases) +{ + set_should_check_subclasses((_abc_data*) data); + + for (Py_ssize_t pos = 0; pos < PyTuple_GET_SIZE(bases); pos++) { + PyObject *base_class = PyTuple_GET_ITEM(bases, pos); // borrowed + _abc_data *base_impl = NULL; + int base_is_abc = _get_optional_impl(state, base_class, &base_impl); + if (base_is_abc < 0) { + return -1; + } + if (base_is_abc == 0) { + continue; + } + set_should_check_subclasses(base_impl); + Py_DECREF(base_impl); + } + + return 0; +} + #define COLLECTION_FLAGS (Py_TPFLAGS_SEQUENCE | Py_TPFLAGS_MAPPING) /*[clinic input] @@ -472,18 +520,21 @@ compute_abstract_methods(PyObject *self) _abc._abc_init self: object + bases: object(subclass_of="&PyTuple_Type") + namespace: object(subclass_of="&PyDict_Type") / Internal ABC helper for class set-up. Should be never used outside abc module. [clinic start generated code]*/ static PyObject * -_abc__abc_init(PyObject *module, PyObject *self) -/*[clinic end generated code: output=594757375714cda1 input=0b3513f947736d39]*/ +_abc__abc_init_impl(PyObject *module, PyObject *self, PyObject *bases, + PyObject *namespace) +/*[clinic end generated code: output=a410180fefc86056 input=a984e4f7d36d6298]*/ { _abcmodule_state *state = get_abc_state(module); PyObject *data; - if (compute_abstract_methods(self) < 0) { + if (compute_abstract_methods(self, bases, namespace) < 0) { return NULL; } @@ -492,6 +543,12 @@ _abc__abc_init(PyObject *module, PyObject *self) if (data == NULL) { return NULL; } + if (PyDict_ContainsString(namespace, "__subclasses__")) { + if (_abc_notify_subclasses_override(state, data, bases) < 0) { + Py_DECREF(data); + return NULL; + } + } if (PyObject_SetAttr(self, &_Py_ID(_abc_impl), data) < 0) { Py_DECREF(data); return NULL; @@ -564,6 +621,7 @@ _abc__abc_register_impl(PyObject *module, PyObject *self, PyObject *subclass) if (result < 0) { return NULL; } + /* Add registry entry */ _abc_data *impl = _get_impl(module, self); if (impl == NULL) { return NULL; @@ -575,7 +633,43 @@ _abc__abc_register_impl(PyObject *module, PyObject *self, PyObject *subclass) Py_DECREF(impl); /* Invalidate negative cache */ - increment_invalidation_counter(get_abc_state(module)); + _abcmodule_state *state = get_abc_state(module); + increment_invalidation_counter(state); + + /* + * Recursively register the subclass in all ABC bases, + * to avoid recursive lookups down the class tree. + * >>> class Ancestor1(ABC): pass + * >>> class Ancestor2(Ancestor1): pass + * >>> class Other: pass + * >>> Ancestor2.register(Other) # calls Ancestor1.register(Other) + * >>> issubclass(Other, Ancestor2) is True + * >>> issubclass(Other, Ancestor1) is True # already in registry + */ + PyObject *bases = PyObject_GetAttr(self, &_Py_ID(__bases__)); + if (!bases) { + return NULL; + } + if (!PyTuple_Check(bases)) { + PyErr_SetString(PyExc_TypeError, "__bases__ is not tuple"); + goto error; + } + for (Py_ssize_t pos = 0; pos < PyTuple_GET_SIZE(bases); pos++) { + PyObject *base = PyTuple_GET_ITEM(bases, pos); // borrowed + int base_is_abc = PyObject_HasAttrWithError(base, &_Py_ID(_abc_impl)); + if (base_is_abc < 0) { + goto error; + } + if (base_is_abc == 0) { + continue; + } + PyObject *res = PyObject_CallMethod(base, "register", "O", subclass); + Py_XDECREF(res); + if (!res) { + goto error; + } + } + Py_DECREF(bases); /* Set Py_TPFLAGS_SEQUENCE or Py_TPFLAGS_MAPPING flag */ if (PyType_Check(self)) { @@ -588,6 +682,10 @@ _abc__abc_register_impl(PyObject *module, PyObject *self, PyObject *subclass) } } return Py_NewRef(subclass); + +error: + Py_DECREF(bases); + return NULL; } @@ -788,31 +886,38 @@ _abc__abc_subclasscheck_impl(PyObject *module, PyObject *self, goto end; } - /* 6. Check if it's a subclass of a subclass (recursive). */ - subclasses = PyObject_CallMethod(self, "__subclasses__", NULL); - if (subclasses == NULL) { - goto end; - } - if (!PyList_Check(subclasses)) { - PyErr_SetString(PyExc_TypeError, "__subclasses__() must return a list"); - goto end; - } - for (pos = 0; pos < PyList_GET_SIZE(subclasses); pos++) { - PyObject *scls = PyList_GetItemRef(subclasses, pos); - if (scls == NULL) { + /* 6. Check if it's a subclass of a subclass (recursive). + * If __subclasses__ contain only ABCs, + * calling issubclass(...) will trigger the same __subclasscheck__ + * on *every* element of class inheritance tree. + * Performing that only in resence of `def __subclasses__()` classmethod + */ + if (get_should_check_subclasses(impl)) { + subclasses = PyObject_CallMethod(self, "__subclasses__", NULL); + if (subclasses == NULL) { goto end; } - int r = PyObject_IsSubclass(subclass, scls); - Py_DECREF(scls); - if (r > 0) { - if (_add_to_weak_set(impl, &impl->_abc_cache, subclass) < 0) { - goto end; - } - result = Py_True; + if (!PyList_Check(subclasses)) { + PyErr_SetString(PyExc_TypeError, "__subclasses__() must return a list"); goto end; } - if (r < 0) { - goto end; + for (pos = 0; pos < PyList_GET_SIZE(subclasses); pos++) { + PyObject *scls = PyList_GetItemRef(subclasses, pos); + if (scls == NULL) { + goto end; + } + int r = PyObject_IsSubclass(subclass, scls); + Py_DECREF(scls); + if (r > 0) { + if (_add_to_weak_set(impl, &impl->_abc_cache, subclass) < 0) { + goto end; + } + result = Py_True; + goto end; + } + if (r < 0) { + goto end; + } } } diff --git a/Modules/clinic/_abc.c.h b/Modules/clinic/_abc.c.h index 04681fa2206a2a..4335fbbc0b02f0 100644 --- a/Modules/clinic/_abc.c.h +++ b/Modules/clinic/_abc.c.h @@ -40,13 +40,45 @@ PyDoc_STRVAR(_abc__get_dump__doc__, {"_get_dump", (PyCFunction)_abc__get_dump, METH_O, _abc__get_dump__doc__}, PyDoc_STRVAR(_abc__abc_init__doc__, -"_abc_init($module, self, /)\n" +"_abc_init($module, self, bases, namespace, /)\n" "--\n" "\n" "Internal ABC helper for class set-up. Should be never used outside abc module."); #define _ABC__ABC_INIT_METHODDEF \ - {"_abc_init", (PyCFunction)_abc__abc_init, METH_O, _abc__abc_init__doc__}, + {"_abc_init", _PyCFunction_CAST(_abc__abc_init), METH_FASTCALL, _abc__abc_init__doc__}, + +static PyObject * +_abc__abc_init_impl(PyObject *module, PyObject *self, PyObject *bases, + PyObject *namespace); + +static PyObject * +_abc__abc_init(PyObject *module, PyObject *const *args, Py_ssize_t nargs) +{ + PyObject *return_value = NULL; + PyObject *self; + PyObject *bases; + PyObject *namespace; + + if (!_PyArg_CheckPositional("_abc_init", nargs, 3, 3)) { + goto exit; + } + self = args[0]; + if (!PyTuple_Check(args[1])) { + _PyArg_BadArgument("_abc_init", "argument 2", "tuple", args[1]); + goto exit; + } + bases = args[1]; + if (!PyDict_Check(args[2])) { + _PyArg_BadArgument("_abc_init", "argument 3", "dict", args[2]); + goto exit; + } + namespace = args[2]; + return_value = _abc__abc_init_impl(module, self, bases, namespace); + +exit: + return return_value; +} PyDoc_STRVAR(_abc__abc_register__doc__, "_abc_register($module, self, subclass, /)\n" @@ -161,4 +193,4 @@ _abc_get_cache_token(PyObject *module, PyObject *Py_UNUSED(ignored)) { return _abc_get_cache_token_impl(module); } -/*[clinic end generated code: output=1989b6716c950e17 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=9fa68621578b46d0 input=a9049054013a1b77]*/ From a7ef5b03dd8db0ec238db5c345b01f3726aa3e81 Mon Sep 17 00:00:00 2001 From: Maxim Martynov Date: Sun, 30 Nov 2025 23:59:29 +0300 Subject: [PATCH 4/4] gh-92810: Add What's New entry --- Doc/whatsnew/3.15.rst | 27 +++++++++++++++++++ ...5-11-07-01-36-39.gh-issue-92810.H765mk.rst | 2 ++ 2 files changed, 29 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-11-07-01-36-39.gh-issue-92810.H765mk.rst diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 4882ddb4310fc2..aead1c6a50daab 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -413,6 +413,33 @@ arguments (:pep:`791`). Improved modules ================ +abc +--- + +* Reduce memory usage of :func:`issubclass` checks for :class:`abc.ABCMeta` subclasses. + + :class:`abc.ABCMeta` subclasses can trigger downstream checks: + + ``issubclass(Some, Class)`` -> ``issubclass(Some, Parent)`` -> ``issubclass(Some, Top)`` + (nothing found) -> ``issubclass(Some, Class.__subclasses__)`` -> ``issubclass(Some, SubClass)`` + -> ``issubclass(Some, SubClass.__subclasses__)`` -> ... + + Due to caching of ``issubclass`` result within each ABC class, + this could lead to memory bloat in large class trees, e.g. thousands of subclasses. + + Now :meth:`!abc.ABCMeta.register` recursively calls ``Parent.register(subclass)``, + ``Top.register(subclass)`` and so on for all base classes, so downstream checks are not needed. + Also :meth:`!abc.ABCMeta.__new__` checks if ``__subclasses__`` method is present within class, + and if not, disables recursive downstream checks in :func:`issubclass` + for specific abstract class and all its children. + + This reduces both the number of checks, and the RAM usage by internal caches: + + ``issubclass(Some, Class)`` -> ``issubclass(Some, Parent)`` -> ``issubclass(Some, Top)`` + (nothing found, stops here) + + (Contributed by Maxim Martynov in :gh:`92810`.) + argparse -------- diff --git a/Misc/NEWS.d/next/Library/2025-11-07-01-36-39.gh-issue-92810.H765mk.rst b/Misc/NEWS.d/next/Library/2025-11-07-01-36-39.gh-issue-92810.H765mk.rst new file mode 100644 index 00000000000000..06b3392869d7df --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-11-07-01-36-39.gh-issue-92810.H765mk.rst @@ -0,0 +1,2 @@ +Reduce memory usage by :meth:`~type.__subclasscheck__` for +:class:`abc.ABCMeta` with large class trees.