From 0ef8d921f5c6945aa8f386e472c4110b81ac773d Mon Sep 17 00:00:00 2001 From: Yurii Karabas <1998uriyyo@gmail.com> Date: Thu, 28 Apr 2022 18:24:19 +0300 Subject: [PATCH] gh-91603: Speed up isinstance/issubclass on union types (GH-91631) Co-authored-by: Jelle Zijlstra Co-authored-by: Shantanu <12621235+hauntsaninja@users.noreply.github.com> --- Doc/library/functions.rst | 3 +- Include/internal/pycore_unionobject.h | 1 + Lib/test/test_isinstance.py | 2 +- Lib/test/test_types.py | 6 +- ...2-04-17-11-03-45.gh-issue-91603.hYw1Lv.rst | 2 + Objects/abstract.c | 8 ++ Objects/unionobject.c | 81 ++----------------- 7 files changed, 24 insertions(+), 79 deletions(-) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2022-04-17-11-03-45.gh-issue-91603.hYw1Lv.rst diff --git a/Doc/library/functions.rst b/Doc/library/functions.rst index f3b8e40babb..394281462de 100644 --- a/Doc/library/functions.rst +++ b/Doc/library/functions.rst @@ -905,7 +905,8 @@ are always available. They are listed here in alphabetical order. tuples) or a :ref:`types-union` of multiple types, return ``True`` if *object* is an instance of any of the types. If *classinfo* is not a type or tuple of types and such tuples, - a :exc:`TypeError` exception is raised. + a :exc:`TypeError` exception is raised. :exc:`TypeError` may not be + raised for an invalid type if an earlier check succeeds. .. versionchanged:: 3.10 *classinfo* can be a :ref:`types-union`. diff --git a/Include/internal/pycore_unionobject.h b/Include/internal/pycore_unionobject.h index 9962f576103..a9ed5651a41 100644 --- a/Include/internal/pycore_unionobject.h +++ b/Include/internal/pycore_unionobject.h @@ -15,6 +15,7 @@ extern PyObject *_Py_union_type_or(PyObject *, PyObject *); #define _PyGenericAlias_Check(op) PyObject_TypeCheck(op, &Py_GenericAliasType) extern PyObject *_Py_subs_parameters(PyObject *, PyObject *, PyObject *, PyObject *); extern PyObject *_Py_make_parameters(PyObject *); +extern PyObject *_Py_union_args(PyObject *self); #ifdef __cplusplus } diff --git a/Lib/test/test_isinstance.py b/Lib/test/test_isinstance.py index 9d37cff9903..a0974640bc1 100644 --- a/Lib/test/test_isinstance.py +++ b/Lib/test/test_isinstance.py @@ -225,7 +225,7 @@ def test_isinstance_with_or_union(self): with self.assertRaises(TypeError): isinstance(2, list[int] | int) with self.assertRaises(TypeError): - isinstance(2, int | str | list[int] | float) + isinstance(2, float | str | list[int] | int) diff --git a/Lib/test/test_types.py b/Lib/test/test_types.py index 42fd4f56235..cde9dadc5e9 100644 --- a/Lib/test/test_types.py +++ b/Lib/test/test_types.py @@ -951,9 +951,9 @@ def __eq__(self, other): with self.assertRaises(ZeroDivisionError): list[int] | list[bt] - union_ga = (int | list[str], int | collections.abc.Callable[..., str], - int | d) - # Raise error when isinstance(type, type | genericalias) + union_ga = (list[str] | int, collections.abc.Callable[..., str] | int, + d | int) + # Raise error when isinstance(type, genericalias | type) for type_ in union_ga: with self.subTest(f"check isinstance/issubclass is invalid for {type_}"): with self.assertRaises(TypeError): diff --git a/Misc/NEWS.d/next/Core and Builtins/2022-04-17-11-03-45.gh-issue-91603.hYw1Lv.rst b/Misc/NEWS.d/next/Core and Builtins/2022-04-17-11-03-45.gh-issue-91603.hYw1Lv.rst new file mode 100644 index 00000000000..957bd5ea09b --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2022-04-17-11-03-45.gh-issue-91603.hYw1Lv.rst @@ -0,0 +1,2 @@ +Speed up :func:`isinstance` and :func:`issubclass` checks for :class:`types.UnionType`. +Patch by Yurii Karabas. diff --git a/Objects/abstract.c b/Objects/abstract.c index 79f5a5f760f..cfb0edcab0e 100644 --- a/Objects/abstract.c +++ b/Objects/abstract.c @@ -2625,6 +2625,10 @@ object_recursive_isinstance(PyThreadState *tstate, PyObject *inst, PyObject *cls return object_isinstance(inst, cls); } + if (_PyUnion_Check(cls)) { + cls = _Py_union_args(cls); + } + if (PyTuple_Check(cls)) { /* Not a general sequence -- that opens up the road to recursion and stack overflow. */ @@ -2714,6 +2718,10 @@ object_issubclass(PyThreadState *tstate, PyObject *derived, PyObject *cls) return recursive_issubclass(derived, cls); } + if (_PyUnion_Check(cls)) { + cls = _Py_union_args(cls); + } + if (PyTuple_Check(cls)) { if (_Py_EnterRecursiveCall(tstate, " in __subclasscheck__")) { diff --git a/Objects/unionobject.c b/Objects/unionobject.c index 36b032c0c5c..5eee27c08fa 100644 --- a/Objects/unionobject.c +++ b/Objects/unionobject.c @@ -48,73 +48,6 @@ union_hash(PyObject *self) return hash; } -static int -is_generic_alias_in_args(PyObject *args) -{ - Py_ssize_t nargs = PyTuple_GET_SIZE(args); - for (Py_ssize_t iarg = 0; iarg < nargs; iarg++) { - PyObject *arg = PyTuple_GET_ITEM(args, iarg); - if (_PyGenericAlias_Check(arg)) { - return 0; - } - } - return 1; -} - -static PyObject * -union_instancecheck(PyObject *self, PyObject *instance) -{ - unionobject *alias = (unionobject *) self; - Py_ssize_t nargs = PyTuple_GET_SIZE(alias->args); - if (!is_generic_alias_in_args(alias->args)) { - PyErr_SetString(PyExc_TypeError, - "isinstance() argument 2 cannot contain a parameterized generic"); - return NULL; - } - for (Py_ssize_t iarg = 0; iarg < nargs; iarg++) { - PyObject *arg = PyTuple_GET_ITEM(alias->args, iarg); - if (PyType_Check(arg)) { - int res = PyObject_IsInstance(instance, arg); - if (res < 0) { - return NULL; - } - if (res) { - Py_RETURN_TRUE; - } - } - } - Py_RETURN_FALSE; -} - -static PyObject * -union_subclasscheck(PyObject *self, PyObject *instance) -{ - if (!PyType_Check(instance)) { - PyErr_SetString(PyExc_TypeError, "issubclass() arg 1 must be a class"); - return NULL; - } - unionobject *alias = (unionobject *)self; - if (!is_generic_alias_in_args(alias->args)) { - PyErr_SetString(PyExc_TypeError, - "issubclass() argument 2 cannot contain a parameterized generic"); - return NULL; - } - Py_ssize_t nargs = PyTuple_GET_SIZE(alias->args); - for (Py_ssize_t iarg = 0; iarg < nargs; iarg++) { - PyObject *arg = PyTuple_GET_ITEM(alias->args, iarg); - if (PyType_Check(arg)) { - int res = PyObject_IsSubclass(instance, arg); - if (res < 0) { - return NULL; - } - if (res) { - Py_RETURN_TRUE; - } - } - } - Py_RETURN_FALSE; -} - static PyObject * union_richcompare(PyObject *a, PyObject *b, int op) { @@ -342,12 +275,6 @@ static PyMemberDef union_members[] = { {0} }; -static PyMethodDef union_methods[] = { - {"__instancecheck__", union_instancecheck, METH_O}, - {"__subclasscheck__", union_subclasscheck, METH_O}, - {0}}; - - static PyObject * union_getitem(PyObject *self, PyObject *item) { @@ -434,6 +361,13 @@ union_getattro(PyObject *self, PyObject *name) return PyObject_GenericGetAttr(self, name); } +PyObject * +_Py_union_args(PyObject *self) +{ + assert(_PyUnion_Check(self)); + return ((unionobject *) self)->args; +} + PyTypeObject _PyUnion_Type = { PyVarObject_HEAD_INIT(&PyType_Type, 0) .tp_name = "types.UnionType", @@ -449,7 +383,6 @@ PyTypeObject _PyUnion_Type = { .tp_hash = union_hash, .tp_getattro = union_getattro, .tp_members = union_members, - .tp_methods = union_methods, .tp_richcompare = union_richcompare, .tp_as_mapping = &union_as_mapping, .tp_as_number = &union_as_number,